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