Coverage for sources/dynadoc/assembly.py: 100%
151 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-02 23:37 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-02 23:37 +0000
1# vim: set filetype=python fileencoding=utf-8:
2# -*- coding: utf-8 -*-
4#============================================================================#
5# #
6# Licensed under the Apache License, Version 2.0 (the "License"); #
7# you may not use this file except in compliance with the License. #
8# You may obtain a copy of the License at #
9# #
10# http://www.apache.org/licenses/LICENSE-2.0 #
11# #
12# Unless required by applicable law or agreed to in writing, software #
13# distributed under the License is distributed on an "AS IS" BASIS, #
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
15# See the License for the specific language governing permissions and #
16# limitations under the License. #
17# #
18#============================================================================#
21''' Docstring assembly and decoration. '''
22# TODO? with_docstring_defer
23# Registers with_docstring partial function in registry.
24# Registry can be executed after modules are loaded and all string
25# annotations should be resolvable.
28from . import __
29from . import factories as _factories
30from . import renderers as _renderers
31from . import xtnsapi as _xtnsapi
34_visitees: __.weakref.WeakSet[ _xtnsapi.Documentable ] = __.weakref.WeakSet( )
37context_default: __.typx.Annotated[
38 _xtnsapi.Context,
39 _xtnsapi.Doc(
40 ''' Default context for introspection and rendering. ''' ),
41 _xtnsapi.Fname( 'context' ),
42 _xtnsapi.Default( mode = _xtnsapi.ValuationModes.Suppress ),
43] = _factories.produce_context( )
44introspection_default: __.typx.Annotated[
45 _xtnsapi.IntrospectionControl,
46 _xtnsapi.Doc( ''' Default introspection control. ''' ),
47 _xtnsapi.Fname( 'introspection' ),
48 _xtnsapi.Default( mode = _xtnsapi.ValuationModes.Suppress ),
49] = _xtnsapi.IntrospectionControl( )
50renderer_default: __.typx.Annotated[
51 _xtnsapi.Renderer,
52 _xtnsapi.Doc( ''' Default renderer for docstring fragments. ''' ),
53 _xtnsapi.Fname( 'renderer' ),
54 _xtnsapi.Default( mode = _xtnsapi.ValuationModes.Suppress ),
55] = _renderers.sphinxad.produce_fragment
58def assign_module_docstring( # noqa: PLR0913
59 module: _xtnsapi.Module, /,
60 *fragments: _xtnsapi.FragmentsArgumentMultivalent,
61 context: _xtnsapi.ContextArgument = context_default,
62 introspection: _xtnsapi.IntrospectionArgument = introspection_default,
63 preserve: _xtnsapi.PreserveArgument = True,
64 renderer: _xtnsapi.RendererArgument = renderer_default,
65 table: _xtnsapi.FragmentsTableArgument = __.dictproxy_empty,
66) -> None:
67 ''' Assembles docstring from fragments and assigns it to module. '''
68 if isinstance( module, str ):
69 module = __.sys.modules[ module ]
70 _decorate(
71 module,
72 context = context,
73 introspection = introspection,
74 preserve = preserve,
75 renderer = renderer,
76 fragments = fragments,
77 table = table )
80def with_docstring(
81 *fragments: _xtnsapi.FragmentsArgumentMultivalent,
82 context: _xtnsapi.ContextArgument = context_default,
83 introspection: _xtnsapi.IntrospectionArgument = introspection_default,
84 preserve: _xtnsapi.PreserveArgument = True,
85 renderer: _xtnsapi.RendererArgument = renderer_default,
86 table: _xtnsapi.FragmentsTableArgument = __.dictproxy_empty,
87) -> _xtnsapi.Decorator[ _xtnsapi.D ]:
88 ''' Assembles docstring from fragments and decorates object with it. '''
89 def decorate( objct: _xtnsapi.D ) -> _xtnsapi.D:
90 _decorate(
91 objct,
92 context = context,
93 introspection = introspection,
94 preserve = preserve,
95 renderer = renderer,
96 fragments = fragments,
97 table = table )
98 return objct
100 return decorate
103def _check_module_recursion(
104 objct: object, /,
105 introspection: _xtnsapi.IntrospectionControl,
106 mname: str
107) -> __.typx.TypeIs[ __.types.ModuleType ]:
108 ''' Checks if a module should be recursively documented.
110 Returns True if the object is a module that should be recursively
111 documented based on the introspection control and module name prefix.
112 '''
113 if ( introspection.targets & _xtnsapi.IntrospectionTargets.Module
114 and __.inspect.ismodule( objct )
115 ): return objct.__name__.startswith( f"{mname}." )
116 return False
119def _collect_fragments(
120 objct: _xtnsapi.Documentable, /, context: _xtnsapi.Context, fqname: str
121) -> _xtnsapi.Fragments:
122 ''' Collects docstring fragments from an object.
124 Retrieves the sequence of fragments stored on the object using the
125 fragments_name from the context. Validates that the fragments are
126 of the expected types.
127 '''
128 fragments: _xtnsapi.Fragments = (
129 getattr( objct, context.fragments_name, ( ) ) )
130 if ( isinstance( fragments, ( bytes, str ) )
131 or not isinstance( fragments, __.cabc.Sequence )
132 ):
133 emessage = f"Invalid fragments sequence on {fqname}: {fragments!r}"
134 context.notifier( 'error', emessage )
135 fragments = ( )
136 for fragment in fragments:
137 if not isinstance( fragment, ( str, _xtnsapi.Doc ) ):
138 emessage = f"Invalid fragment on {fqname}: {fragment!r}"
139 context.notifier( 'error', emessage )
140 return fragments
143def _consider_class_attribute( # noqa: C901,PLR0913
144 attribute: object, /,
145 context: _xtnsapi.Context,
146 introspection: _xtnsapi.IntrospectionControl,
147 pmname: str, pqname: str, aname: str,
148) -> tuple[ __.typx.Optional[ _xtnsapi.Documentable ], bool ]:
149 ''' Considers whether a class attribute should be documented.
151 Examines a class attribute to determine if it should be included
152 in the documentation process based on introspection targets and
153 class ownership. Returns the documentable attribute and a flag
154 indicating whether the surface attribute needs updating.
155 '''
156 if _check_module_recursion( attribute, introspection, pmname ):
157 return attribute, False
158 attribute_ = None
159 update_surface = False
160 if ( not attribute_
161 and introspection.targets & _xtnsapi.IntrospectionTargets.Class
162 and __.inspect.isclass( attribute )
163 ): attribute_ = attribute
164 if ( not attribute_
165 and introspection.targets & _xtnsapi.IntrospectionTargets.Descriptor
166 ):
167 if isinstance( attribute, property ) and attribute.fget:
168 # Examine docstring and signature of getter method on property.
169 attribute_ = attribute.fget
170 update_surface = True
171 # TODO: Apply custom processors from context.
172 elif __.inspect.isdatadescriptor( attribute ):
173 # Ignore descriptors which we do not know how to handle.
174 return None, False
175 if ( not attribute_
176 and introspection.targets & _xtnsapi.IntrospectionTargets.Function
177 ):
178 if __.inspect.ismethod( attribute ):
179 # Methods proxy docstrings from their core functions.
180 attribute_ = attribute.__func__
181 elif __.inspect.isfunction( attribute ) and aname != '<lambda>':
182 attribute_ = attribute
183 if attribute_:
184 mname = getattr( attribute_, '__module__', None )
185 if not mname or mname != pmname:
186 attribute_ = None
187 if attribute_:
188 qname = getattr( attribute_, '__qualname__', None )
189 if not qname or not qname.startswith( f"{pqname}." ):
190 attribute_ = None
191 return attribute_, update_surface
194def _consider_module_attribute(
195 attribute: object, /,
196 context: _xtnsapi.Context,
197 introspection: _xtnsapi.IntrospectionControl,
198 pmname: str, aname: str,
199) -> tuple[ __.typx.Optional[ _xtnsapi.Documentable ], bool ]:
200 ''' Considers whether a module attribute should be documented.
202 Examines a module attribute to determine if it should be included
203 in the documentation process based on introspection targets and
204 module ownership. Returns the documentable attribute and a flag
205 indicating whether the surface attribute needs updating.
206 '''
207 if _check_module_recursion( attribute, introspection, pmname ):
208 return attribute, False
209 attribute_ = None
210 update_surface = False
211 if ( not attribute_
212 and introspection.targets & _xtnsapi.IntrospectionTargets.Class
213 and __.inspect.isclass( attribute )
214 ): attribute_ = attribute
215 if ( not attribute_
216 and introspection.targets & _xtnsapi.IntrospectionTargets.Function
217 and __.inspect.isfunction( attribute ) and aname != '<lambda>'
218 ): attribute_ = attribute
219 if attribute_:
220 mname = getattr( attribute_, '__module__', None )
221 if not mname or mname != pmname:
222 attribute_ = None
223 return attribute_, update_surface
226def _decorate( # noqa: PLR0913
227 objct: _xtnsapi.Documentable, /,
228 context: _xtnsapi.Context,
229 introspection: _xtnsapi.IntrospectionControl,
230 preserve: bool,
231 renderer: _xtnsapi.Renderer,
232 fragments: _xtnsapi.Fragments,
233 table: _xtnsapi.FragmentsTable,
234) -> None:
235 ''' Decorates an object with assembled docstring.
237 Handles core docstring decoration and potentially recursive decoration
238 of the object's attributes based on introspection control settings.
239 Prevents multiple decoration of the same object.
240 '''
241 if objct in _visitees: return # Prevent multiple decoration.
242 _visitees.add( objct )
243 if introspection.targets:
244 if __.inspect.isclass( objct ):
245 _decorate_class_attributes(
246 objct,
247 context = context,
248 introspection = introspection,
249 preserve = preserve,
250 renderer = renderer,
251 table = table )
252 elif __.inspect.ismodule( objct ):
253 _decorate_module_attributes(
254 objct,
255 context = context,
256 introspection = introspection,
257 preserve = preserve,
258 renderer = renderer,
259 table = table )
260 if __.inspect.ismodule( objct ): fqname = objct.__name__
261 else: fqname = f"{objct.__module__}.{objct.__qualname__}"
262 fragments_ = _collect_fragments( objct, context, fqname )
263 if not fragments_: fragments_ = fragments
264 _decorate_core(
265 objct,
266 context = context,
267 introspection = introspection,
268 preserve = preserve,
269 renderer = renderer,
270 fragments = fragments_,
271 table = table )
274def _decorate_core( # noqa: PLR0913
275 objct: _xtnsapi.Documentable, /,
276 context: _xtnsapi.Context,
277 introspection: _xtnsapi.IntrospectionControl,
278 preserve: bool,
279 renderer: _xtnsapi.Renderer,
280 fragments: _xtnsapi.Fragments,
281 table: _xtnsapi.FragmentsTable,
282) -> None:
283 ''' Core implementation of docstring decoration.
285 Assembles a docstring from fragments, existing docstring (if
286 preserved), and introspection results. Assigns the assembled docstring
287 to the object.
288 '''
289 fragments_: list[ str ] = [ ]
290 if preserve and ( fragment := getattr( objct, '__doc__', None ) ):
291 fragments_.append( context.fragment_rectifier(
292 fragment, source = _xtnsapi.FragmentSources.Docstring ) )
293 fragments_.extend(
294 _process_fragments_argument( context, fragments, table ) )
295 if introspection.enable:
296 cache = _xtnsapi.AnnotationsCache( )
297 informations = (
298 _xtnsapi.introspect(
299 objct,
300 context = context, introspection = introspection,
301 cache = cache, table = table ) )
302 fragments_.append( context.fragment_rectifier(
303 renderer( objct, informations, context = context ),
304 source = _xtnsapi.FragmentSources.Renderer ) )
305 docstring = '\n\n'.join(
306 fragment for fragment in filter( None, fragments_ ) ).rstrip( )
307 objct.__doc__ = docstring if docstring else None
310def _decorate_class_attributes( # noqa: PLR0913
311 objct: type, /,
312 context: _xtnsapi.Context,
313 introspection: _xtnsapi.IntrospectionControl,
314 preserve: bool,
315 renderer: _xtnsapi.Renderer,
316 table: _xtnsapi.FragmentsTable,
317) -> None:
318 ''' Decorates attributes of a class with assembled docstrings.
320 Iterates through relevant class attributes, collects fragments,
321 and applies appropriate docstring decoration to each attribute.
322 '''
323 pmname = objct.__module__
324 pqname = objct.__qualname__
325 for aname, attribute, surface_attribute in (
326 _survey_class_attributes( objct, context, introspection )
327 ):
328 fqname = f"{pmname}.{pqname}.{aname}"
329 introspection_ = _limit_introspection(
330 attribute, context, introspection, fqname )
331 introspection_ = introspection_.evaluate_limits_for( attribute )
332 if not introspection_.enable: continue
333 _decorate(
334 attribute,
335 context = context,
336 introspection = introspection_,
337 preserve = preserve,
338 renderer = renderer,
339 fragments = ( ),
340 table = table )
341 if attribute is not surface_attribute:
342 surface_attribute.__doc__ = attribute.__doc__
345def _decorate_module_attributes( # noqa: PLR0913
346 module: __.types.ModuleType, /,
347 context: _xtnsapi.Context,
348 introspection: _xtnsapi.IntrospectionControl,
349 preserve: bool,
350 renderer: _xtnsapi.Renderer,
351 table: _xtnsapi.FragmentsTable,
352) -> None:
353 ''' Decorates attributes of a module with assembled docstrings.
355 Iterates through relevant module attributes, collects fragments,
356 and applies appropriate docstring decoration to each attribute.
357 '''
358 pmname = module.__name__
359 for aname, attribute, surface_attribute in (
360 _survey_module_attributes( module, context, introspection )
361 ):
362 fqname = f"{pmname}.{aname}"
363 introspection_ = _limit_introspection(
364 attribute, context, introspection, fqname )
365 introspection_ = introspection_.evaluate_limits_for( attribute )
366 if not introspection_.enable: continue
367 _decorate(
368 attribute,
369 context = context,
370 introspection = introspection_,
371 preserve = preserve,
372 renderer = renderer,
373 fragments = ( ),
374 table = table )
375 if attribute is not surface_attribute: # pragma: no cover
376 surface_attribute.__doc__ = attribute.__doc__
379def _limit_introspection(
380 objct: _xtnsapi.Documentable, /,
381 context: _xtnsapi.Context,
382 introspection: _xtnsapi.IntrospectionControl,
383 fqname: str,
384) -> _xtnsapi.IntrospectionControl:
385 ''' Limits introspection based on object-specific constraints.
387 Returns a new IntrospectionControl that respects the limits
388 specified by the object being documented. This allows objects
389 to control how deeply they are introspected.
390 '''
391 limit: _xtnsapi.IntrospectionLimit = (
392 getattr(
393 objct,
394 context.introspection_limit_name,
395 _xtnsapi.IntrospectionLimit( ) ) )
396 if not isinstance( limit, _xtnsapi.IntrospectionLimit ):
397 emessage = f"Invalid introspection limit on {fqname}: {limit!r}"
398 context.notifier( 'error', emessage )
399 return introspection
400 return introspection.with_limit( limit )
403def _process_fragments_argument(
404 context: _xtnsapi.Context,
405 fragments: _xtnsapi.Fragments,
406 table: _xtnsapi.FragmentsTable,
407) -> __.cabc.Sequence[ str ]:
408 ''' Processes fragments argument into a sequence of string fragments.
410 Converts Doc objects to their documentation strings and resolves
411 string references to the fragments table. Returns a sequence of
412 rectified fragment strings.
413 '''
414 fragments_: list[ str ] = [ ]
415 for fragment in fragments:
416 if isinstance( fragment, _xtnsapi.Doc ):
417 fragment_r = fragment.documentation
418 elif isinstance( fragment, str ):
419 if fragment not in table:
420 emessage = f"Fragment '{fragment}' not in provided table."
421 context.notifier( 'error', emessage )
422 continue
423 fragment_r = table[ fragment ]
424 else:
425 emessage = f"Fragment {fragment!r} is invalid. Must be Doc or str."
426 context.notifier( 'error', emessage )
427 continue
428 fragments_.append( context.fragment_rectifier(
429 fragment_r, source = _xtnsapi.FragmentSources.Argument ) )
430 return fragments_
433def _survey_class_attributes(
434 possessor: type, /,
435 context: _xtnsapi.Context,
436 introspection: _xtnsapi.IntrospectionControl,
437) -> __.cabc.Iterator[ tuple[ str, _xtnsapi.Documentable, object ] ]:
438 ''' Surveys attributes of a class for documentation.
440 Yields a sequence of (name, attribute, surface_attribute) tuples
441 representing documentable attributes of the class. The surface
442 attribute may differ from attribute in cases like properties where the
443 attribute's getter method holds the documentation.
444 '''
445 pmname = possessor.__module__
446 pqname = possessor.__qualname__
447 for aname, attribute in __.inspect.getmembers( possessor ):
448 attribute_, update_surface = (
449 _consider_class_attribute(
450 attribute, context, introspection, pmname, pqname, aname ) )
451 if attribute_ is None: continue
452 if update_surface:
453 yield aname, attribute_, attribute
454 continue
455 yield aname, attribute_, attribute_
458def _survey_module_attributes(
459 possessor: __.types.ModuleType, /,
460 context: _xtnsapi.Context,
461 introspection: _xtnsapi.IntrospectionControl,
462) -> __.cabc.Iterator[ tuple[ str, _xtnsapi.Documentable, object ] ]:
463 ''' Surveys attributes of a module for documentation.
465 Yields a sequence of (name, attribute, surface_attribute) tuples
466 representing documentable attributes of the module. The surface
467 attribute may differ from attribute in cases where the actual
468 documented object is not directly accessible.
469 '''
470 pmname = possessor.__name__
471 for aname, attribute in __.inspect.getmembers( possessor ):
472 attribute_, update_surface = (
473 _consider_module_attribute(
474 attribute, context, introspection, pmname, aname ) )
475 if attribute_ is None: continue
476 if update_surface: # pragma: no cover
477 yield aname, attribute_, attribute
478 continue
479 yield aname, attribute_, attribute_