Coverage for sources/dynadoc/assembly.py: 100%
154 statements
« prev ^ index » next coverage.py v7.10.1, created at 2025-07-29 05:16 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2025-07-29 05:16 +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 exclude( objct: _xtnsapi.D ) -> _xtnsapi.D:
81 ''' Excludes object from docstring updates. '''
82 _visitees.add( objct )
83 return objct
86def with_docstring(
87 *fragments: _xtnsapi.FragmentsArgumentMultivalent,
88 context: _xtnsapi.ContextArgument = context_default,
89 introspection: _xtnsapi.IntrospectionArgument = introspection_default,
90 preserve: _xtnsapi.PreserveArgument = True,
91 renderer: _xtnsapi.RendererArgument = renderer_default,
92 table: _xtnsapi.FragmentsTableArgument = __.dictproxy_empty,
93) -> _xtnsapi.Decorator[ _xtnsapi.D ]:
94 ''' Assembles docstring from fragments and decorates object with it. '''
95 def decorate( objct: _xtnsapi.D ) -> _xtnsapi.D:
96 _decorate(
97 objct,
98 context = context,
99 introspection = introspection,
100 preserve = preserve,
101 renderer = renderer,
102 fragments = fragments,
103 table = table )
104 return objct
106 return decorate
109def _check_module_recursion(
110 objct: object, /,
111 introspection: _xtnsapi.IntrospectionControl,
112 mname: str
113) -> __.typx.TypeIs[ __.types.ModuleType ]:
114 ''' Checks if a module should be recursively documented.
116 Returns True if the object is a module that should be recursively
117 documented based on the introspection control and module name prefix.
118 '''
119 if ( introspection.targets & _xtnsapi.IntrospectionTargets.Module
120 and __.inspect.ismodule( objct )
121 ): return objct.__name__.startswith( f"{mname}." )
122 return False
125def _collect_fragments(
126 objct: _xtnsapi.Documentable, /, context: _xtnsapi.Context, fqname: str
127) -> _xtnsapi.Fragments:
128 ''' Collects docstring fragments from an object.
130 Retrieves the sequence of fragments stored on the object using the
131 fragments_name from the context. Validates that the fragments are
132 of the expected types.
133 '''
134 fragments: _xtnsapi.Fragments = (
135 # Fragments can come from base class or metaclass.
136 # We only care about fragments on class itself.
137 objct.__dict__.get( context.fragments_name, ( ) )
138 if __.inspect.isclass( objct )
139 else getattr( objct, context.fragments_name, ( ) ) )
140 if ( isinstance( fragments, ( bytes, str ) )
141 or not isinstance( fragments, __.cabc.Sequence )
142 ):
143 emessage = f"Invalid fragments sequence on {fqname}: {fragments!r}"
144 context.notifier( 'error', emessage )
145 fragments = ( )
146 for fragment in fragments:
147 if not isinstance( fragment, ( str, _xtnsapi.Doc ) ):
148 emessage = f"Invalid fragment on {fqname}: {fragment!r}"
149 context.notifier( 'error', emessage )
150 return fragments
153def _consider_class_attribute( # noqa: C901,PLR0913
154 attribute: object, /,
155 context: _xtnsapi.Context,
156 introspection: _xtnsapi.IntrospectionControl,
157 pmname: str, pqname: str, aname: str,
158) -> tuple[ __.typx.Optional[ _xtnsapi.Documentable ], bool ]:
159 ''' Considers whether a class attribute should be documented.
161 Examines a class attribute to determine if it should be included
162 in the documentation process based on introspection targets and
163 class ownership. Returns the documentable attribute and a flag
164 indicating whether the surface attribute needs updating.
165 '''
166 if _check_module_recursion( attribute, introspection, pmname ):
167 return attribute, False
168 attribute_ = None
169 update_surface = False
170 if ( not attribute_
171 and introspection.targets & _xtnsapi.IntrospectionTargets.Class
172 and __.inspect.isclass( attribute )
173 ): attribute_ = attribute
174 if ( not attribute_
175 and introspection.targets & _xtnsapi.IntrospectionTargets.Descriptor
176 ):
177 if isinstance( attribute, property ) and attribute.fget:
178 # Examine docstring and signature of getter method on property.
179 attribute_ = attribute.fget
180 update_surface = True
181 # TODO: Apply custom processors from context.
182 elif __.inspect.isdatadescriptor( attribute ):
183 # Ignore descriptors which we do not know how to handle.
184 return None, False
185 if ( not attribute_
186 and introspection.targets & _xtnsapi.IntrospectionTargets.Function
187 ):
188 if __.inspect.ismethod( attribute ):
189 # Methods proxy docstrings from their core functions.
190 attribute_ = attribute.__func__
191 elif __.inspect.isfunction( attribute ) and aname != '<lambda>':
192 attribute_ = attribute
193 if attribute_:
194 mname = getattr( attribute_, '__module__', None )
195 if not mname or mname != pmname:
196 attribute_ = None
197 if attribute_:
198 qname = getattr( attribute_, '__qualname__', None )
199 if not qname or not qname.startswith( f"{pqname}." ):
200 attribute_ = None
201 return attribute_, update_surface
204def _consider_module_attribute(
205 attribute: object, /,
206 context: _xtnsapi.Context,
207 introspection: _xtnsapi.IntrospectionControl,
208 pmname: str, aname: str,
209) -> tuple[ __.typx.Optional[ _xtnsapi.Documentable ], bool ]:
210 ''' Considers whether a module attribute should be documented.
212 Examines a module attribute to determine if it should be included
213 in the documentation process based on introspection targets and
214 module ownership. Returns the documentable attribute and a flag
215 indicating whether the surface attribute needs updating.
216 '''
217 if _check_module_recursion( attribute, introspection, pmname ):
218 return attribute, False
219 attribute_ = None
220 update_surface = False
221 if ( not attribute_
222 and introspection.targets & _xtnsapi.IntrospectionTargets.Class
223 and __.inspect.isclass( attribute )
224 ): attribute_ = attribute
225 if ( not attribute_
226 and introspection.targets & _xtnsapi.IntrospectionTargets.Function
227 and __.inspect.isfunction( attribute ) and aname != '<lambda>'
228 ): attribute_ = attribute
229 if attribute_:
230 mname = getattr( attribute_, '__module__', None )
231 if not mname or mname != pmname:
232 attribute_ = None
233 return attribute_, update_surface
236def _decorate( # noqa: PLR0913
237 objct: _xtnsapi.Documentable, /,
238 context: _xtnsapi.Context,
239 introspection: _xtnsapi.IntrospectionControl,
240 preserve: bool,
241 renderer: _xtnsapi.Renderer,
242 fragments: _xtnsapi.Fragments,
243 table: _xtnsapi.FragmentsTable,
244) -> None:
245 ''' Decorates an object with assembled docstring.
247 Handles core docstring decoration and potentially recursive decoration
248 of the object's attributes based on introspection control settings.
249 Prevents multiple decoration of the same object.
250 '''
251 if objct in _visitees: return # Prevent multiple decoration.
252 _visitees.add( objct )
253 if introspection.targets:
254 if __.inspect.isclass( objct ):
255 _decorate_class_attributes(
256 objct,
257 context = context,
258 introspection = introspection,
259 preserve = preserve,
260 renderer = renderer,
261 table = table )
262 elif __.inspect.ismodule( objct ):
263 _decorate_module_attributes(
264 objct,
265 context = context,
266 introspection = introspection,
267 preserve = preserve,
268 renderer = renderer,
269 table = table )
270 if __.inspect.ismodule( objct ): fqname = objct.__name__
271 else: fqname = f"{objct.__module__}.{objct.__qualname__}"
272 fragments_ = _collect_fragments( objct, context, fqname )
273 if not fragments_: fragments_ = fragments
274 _decorate_core(
275 objct,
276 context = context,
277 introspection = introspection,
278 preserve = preserve,
279 renderer = renderer,
280 fragments = fragments_,
281 table = table )
284def _decorate_core( # noqa: PLR0913
285 objct: _xtnsapi.Documentable, /,
286 context: _xtnsapi.Context,
287 introspection: _xtnsapi.IntrospectionControl,
288 preserve: bool,
289 renderer: _xtnsapi.Renderer,
290 fragments: _xtnsapi.Fragments,
291 table: _xtnsapi.FragmentsTable,
292) -> None:
293 ''' Core implementation of docstring decoration.
295 Assembles a docstring from fragments, existing docstring (if
296 preserved), and introspection results. Assigns the assembled docstring
297 to the object.
298 '''
299 fragments_: list[ str ] = [ ]
300 if preserve and ( fragment := getattr( objct, '__doc__', None ) ):
301 fragments_.append( context.fragment_rectifier(
302 fragment, source = _xtnsapi.FragmentSources.Docstring ) )
303 fragments_.extend(
304 _process_fragments_argument( context, fragments, table ) )
305 if introspection.enable:
306 cache = _xtnsapi.AnnotationsCache( )
307 informations = (
308 _xtnsapi.introspect(
309 objct,
310 context = context, introspection = introspection,
311 cache = cache, table = table ) )
312 fragments_.append( context.fragment_rectifier(
313 renderer( objct, informations, context = context ),
314 source = _xtnsapi.FragmentSources.Renderer ) )
315 docstring = '\n\n'.join(
316 fragment for fragment in filter( None, fragments_ ) ).rstrip( )
317 objct.__doc__ = docstring if docstring else None
320def _decorate_class_attributes( # noqa: PLR0913
321 objct: type, /,
322 context: _xtnsapi.Context,
323 introspection: _xtnsapi.IntrospectionControl,
324 preserve: bool,
325 renderer: _xtnsapi.Renderer,
326 table: _xtnsapi.FragmentsTable,
327) -> None:
328 ''' Decorates attributes of a class with assembled docstrings.
330 Iterates through relevant class attributes, collects fragments,
331 and applies appropriate docstring decoration to each attribute.
332 '''
333 pmname = objct.__module__
334 pqname = objct.__qualname__
335 for aname, attribute, surface_attribute in (
336 _survey_class_attributes( objct, context, introspection )
337 ):
338 fqname = f"{pmname}.{pqname}.{aname}"
339 introspection_ = _limit_introspection(
340 attribute, context, introspection, fqname )
341 introspection_ = introspection_.evaluate_limits_for( attribute )
342 if not introspection_.enable: continue
343 _decorate(
344 attribute,
345 context = context,
346 introspection = introspection_,
347 preserve = preserve,
348 renderer = renderer,
349 fragments = ( ),
350 table = table )
351 if attribute is not surface_attribute:
352 surface_attribute.__doc__ = attribute.__doc__
355def _decorate_module_attributes( # noqa: PLR0913
356 module: __.types.ModuleType, /,
357 context: _xtnsapi.Context,
358 introspection: _xtnsapi.IntrospectionControl,
359 preserve: bool,
360 renderer: _xtnsapi.Renderer,
361 table: _xtnsapi.FragmentsTable,
362) -> None:
363 ''' Decorates attributes of a module with assembled docstrings.
365 Iterates through relevant module attributes, collects fragments,
366 and applies appropriate docstring decoration to each attribute.
367 '''
368 pmname = module.__name__
369 for aname, attribute, surface_attribute in (
370 _survey_module_attributes( module, context, introspection )
371 ):
372 fqname = f"{pmname}.{aname}"
373 introspection_ = _limit_introspection(
374 attribute, context, introspection, fqname )
375 introspection_ = introspection_.evaluate_limits_for( attribute )
376 if not introspection_.enable: continue
377 _decorate(
378 attribute,
379 context = context,
380 introspection = introspection_,
381 preserve = preserve,
382 renderer = renderer,
383 fragments = ( ),
384 table = table )
385 if attribute is not surface_attribute: # pragma: no cover
386 surface_attribute.__doc__ = attribute.__doc__
389def _limit_introspection(
390 objct: _xtnsapi.Documentable, /,
391 context: _xtnsapi.Context,
392 introspection: _xtnsapi.IntrospectionControl,
393 fqname: str,
394) -> _xtnsapi.IntrospectionControl:
395 ''' Limits introspection based on object-specific constraints.
397 Returns a new IntrospectionControl that respects the limits
398 specified by the object being documented. This allows objects
399 to control how deeply they are introspected.
400 '''
401 limit: _xtnsapi.IntrospectionLimit = (
402 getattr(
403 objct,
404 context.introspection_limit_name,
405 _xtnsapi.IntrospectionLimit( ) ) )
406 if not isinstance( limit, _xtnsapi.IntrospectionLimit ):
407 emessage = f"Invalid introspection limit on {fqname}: {limit!r}"
408 context.notifier( 'error', emessage )
409 return introspection
410 return introspection.with_limit( limit )
413def _process_fragments_argument(
414 context: _xtnsapi.Context,
415 fragments: _xtnsapi.Fragments,
416 table: _xtnsapi.FragmentsTable,
417) -> __.cabc.Sequence[ str ]:
418 ''' Processes fragments argument into a sequence of string fragments.
420 Converts Doc objects to their documentation strings and resolves
421 string references to the fragments table. Returns a sequence of
422 rectified fragment strings.
423 '''
424 fragments_: list[ str ] = [ ]
425 for fragment in fragments:
426 if isinstance( fragment, _xtnsapi.Doc ):
427 fragment_r = fragment.documentation
428 elif isinstance( fragment, str ):
429 if fragment not in table:
430 emessage = f"Fragment '{fragment}' not in provided table."
431 context.notifier( 'error', emessage )
432 continue
433 fragment_r = table[ fragment ]
434 else:
435 emessage = f"Fragment {fragment!r} is invalid. Must be Doc or str."
436 context.notifier( 'error', emessage )
437 continue
438 fragments_.append( context.fragment_rectifier(
439 fragment_r, source = _xtnsapi.FragmentSources.Argument ) )
440 return fragments_
443def _survey_class_attributes(
444 possessor: type, /,
445 context: _xtnsapi.Context,
446 introspection: _xtnsapi.IntrospectionControl,
447) -> __.cabc.Iterator[ tuple[ str, _xtnsapi.Documentable, object ] ]:
448 ''' Surveys attributes of a class for documentation.
450 Yields a sequence of (name, attribute, surface_attribute) tuples
451 representing documentable attributes of the class. The surface
452 attribute may differ from attribute in cases like properties where the
453 attribute's getter method holds the documentation.
454 '''
455 pmname = possessor.__module__
456 pqname = possessor.__qualname__
457 for aname, attribute in __.inspect.getmembers( possessor ):
458 attribute_, update_surface = (
459 _consider_class_attribute(
460 attribute, context, introspection, pmname, pqname, aname ) )
461 if attribute_ is None: continue
462 if update_surface:
463 yield aname, attribute_, attribute
464 continue
465 yield aname, attribute_, attribute_
468def _survey_module_attributes(
469 possessor: __.types.ModuleType, /,
470 context: _xtnsapi.Context,
471 introspection: _xtnsapi.IntrospectionControl,
472) -> __.cabc.Iterator[ tuple[ str, _xtnsapi.Documentable, object ] ]:
473 ''' Surveys attributes of a module for documentation.
475 Yields a sequence of (name, attribute, surface_attribute) tuples
476 representing documentable attributes of the module. The surface
477 attribute may differ from attribute in cases where the actual
478 documented object is not directly accessible.
479 '''
480 pmname = possessor.__name__
481 for aname, attribute in __.inspect.getmembers( possessor ):
482 attribute_, update_surface = (
483 _consider_module_attribute(
484 attribute, context, introspection, pmname, aname ) )
485 if attribute_ is None: continue
486 if update_surface: # pragma: no cover
487 yield aname, attribute_, attribute
488 continue
489 yield aname, attribute_, attribute_