Coverage for sources/dynadoc/assembly.py: 100%
149 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-05-30 03:09 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-05-30 03:09 +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 _decorate(
333 attribute,
334 context = context,
335 introspection = introspection_,
336 preserve = preserve,
337 renderer = renderer,
338 fragments = ( ),
339 table = table )
340 if attribute is not surface_attribute:
341 surface_attribute.__doc__ = attribute.__doc__
344def _decorate_module_attributes( # noqa: PLR0913
345 module: __.types.ModuleType, /,
346 context: _xtnsapi.Context,
347 introspection: _xtnsapi.IntrospectionControl,
348 preserve: bool,
349 renderer: _xtnsapi.Renderer,
350 table: _xtnsapi.FragmentsTable,
351) -> None:
352 ''' Decorates attributes of a module with assembled docstrings.
354 Iterates through relevant module attributes, collects fragments,
355 and applies appropriate docstring decoration to each attribute.
356 '''
357 pmname = module.__name__
358 for aname, attribute, surface_attribute in (
359 _survey_module_attributes( module, context, introspection )
360 ):
361 fqname = f"{pmname}.{aname}"
362 introspection_ = _limit_introspection(
363 attribute, context, introspection, fqname )
364 introspection_ = introspection_.evaluate_limits_for( attribute )
365 _decorate(
366 attribute,
367 context = context,
368 introspection = introspection_,
369 preserve = preserve,
370 renderer = renderer,
371 fragments = ( ),
372 table = table )
373 if attribute is not surface_attribute: # pragma: no cover
374 surface_attribute.__doc__ = attribute.__doc__
377def _limit_introspection(
378 objct: _xtnsapi.Documentable, /,
379 context: _xtnsapi.Context,
380 introspection: _xtnsapi.IntrospectionControl,
381 fqname: str,
382) -> _xtnsapi.IntrospectionControl:
383 ''' Limits introspection based on object-specific constraints.
385 Returns a new IntrospectionControl that respects the limits
386 specified by the object being documented. This allows objects
387 to control how deeply they are introspected.
388 '''
389 limit: _xtnsapi.IntrospectionLimit = (
390 getattr(
391 objct,
392 context.introspection_limit_name,
393 _xtnsapi.IntrospectionLimit( ) ) )
394 if not isinstance( limit, _xtnsapi.IntrospectionLimit ):
395 emessage = f"Invalid introspection limit on {fqname}: {limit!r}"
396 context.notifier( 'error', emessage )
397 return introspection
398 return introspection.with_limit( limit )
401def _process_fragments_argument(
402 context: _xtnsapi.Context,
403 fragments: _xtnsapi.Fragments,
404 table: _xtnsapi.FragmentsTable,
405) -> __.cabc.Sequence[ str ]:
406 ''' Processes fragments argument into a sequence of string fragments.
408 Converts Doc objects to their documentation strings and resolves
409 string references to the fragments table. Returns a sequence of
410 rectified fragment strings.
411 '''
412 fragments_: list[ str ] = [ ]
413 for fragment in fragments:
414 if isinstance( fragment, _xtnsapi.Doc ):
415 fragment_r = fragment.documentation
416 elif isinstance( fragment, str ):
417 if fragment not in table:
418 emessage = f"Fragment '{fragment}' not in provided table."
419 context.notifier( 'error', emessage )
420 continue
421 fragment_r = table[ fragment ]
422 else:
423 emessage = f"Fragment {fragment!r} is invalid. Must be Doc or str."
424 context.notifier( 'error', emessage )
425 continue
426 fragments_.append( context.fragment_rectifier(
427 fragment_r, source = _xtnsapi.FragmentSources.Argument ) )
428 return fragments_
431def _survey_class_attributes(
432 possessor: type, /,
433 context: _xtnsapi.Context,
434 introspection: _xtnsapi.IntrospectionControl,
435) -> __.cabc.Iterator[ tuple[ str, _xtnsapi.Documentable, object ] ]:
436 ''' Surveys attributes of a class for documentation.
438 Yields a sequence of (name, attribute, surface_attribute) tuples
439 representing documentable attributes of the class. The surface
440 attribute may differ from attribute in cases like properties where the
441 attribute's getter method holds the documentation.
442 '''
443 pmname = possessor.__module__
444 pqname = possessor.__qualname__
445 for aname, attribute in __.inspect.getmembers( possessor ):
446 attribute_, update_surface = (
447 _consider_class_attribute(
448 attribute, context, introspection, pmname, pqname, aname ) )
449 if attribute_ is None: continue
450 if update_surface:
451 yield aname, attribute_, attribute
452 continue
453 yield aname, attribute_, attribute_
456def _survey_module_attributes(
457 possessor: __.types.ModuleType, /,
458 context: _xtnsapi.Context,
459 introspection: _xtnsapi.IntrospectionControl,
460) -> __.cabc.Iterator[ tuple[ str, _xtnsapi.Documentable, object ] ]:
461 ''' Surveys attributes of a module for documentation.
463 Yields a sequence of (name, attribute, surface_attribute) tuples
464 representing documentable attributes of the module. The surface
465 attribute may differ from attribute in cases where the actual
466 documented object is not directly accessible.
467 '''
468 pmname = possessor.__name__
469 for aname, attribute in __.inspect.getmembers( possessor ):
470 attribute_, update_surface = (
471 _consider_module_attribute(
472 attribute, context, introspection, pmname, aname ) )
473 if attribute_ is None: continue
474 if update_surface: # pragma: no cover
475 yield aname, attribute_, attribute
476 continue
477 yield aname, attribute_, attribute_