Coverage for sources/dynadoc/introspection.py: 100%
211 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''' Introspection of argument, attribute, and return annotations. '''
24from . import __
25from . import context as _context
26from . import interfaces as _interfaces
27from . import nomina as _nomina
30_default_default = _interfaces.Default( )
31_default_suppress = _interfaces.Default(
32 mode = _interfaces.ValuationModes.Suppress )
35IntrospectIntrospectionArgument: __.typx.TypeAlias = __.typx.Annotated[
36 _context.IntrospectionControl,
37 _interfaces.Doc(
38 ''' Control settings for introspection behavior. ''' ),
39]
42def introspect(
43 possessor: _interfaces.PossessorArgument, /,
44 context: _context.ContextArgument,
45 introspection: _context.IntrospectionArgument,
46 cache: _interfaces.AnnotationsCacheArgument,
47 table: _interfaces.FragmentsTableArgument,
48) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
49 ''' Introspects object to extract documentable information.
51 Dispatches to appropriate introspection function based on the type
52 of the object being introspected (class, function, or module).
53 '''
54 if __.inspect.isclass( possessor ):
55 return _introspect_class(
56 possessor, context, introspection, cache, table )
57 if __.inspect.isfunction( possessor ) and possessor.__name__ != '<lambda>':
58 return _introspect_function(
59 possessor, context, cache, table )
60 if __.inspect.ismodule( possessor ):
61 return _introspect_module(
62 possessor, context, introspection, cache, table )
63 return ( )
66def introspect_special_classes( # noqa: PLR0913
67 possessor: _interfaces.PossessorClassArgument, /,
68 context: _context.ContextArgument,
69 introspection: _context.IntrospectionArgument,
70 annotations: _interfaces.AnnotationsArgument,
71 cache: _interfaces.AnnotationsCacheArgument,
72 table: _interfaces.FragmentsTableArgument,
73) -> __.typx.Optional[ _interfaces.Informations ]:
74 ''' Introspects special classes in Python standard library.
76 E.g., enum members are collected as class variables.
77 '''
78 informations: list[ _interfaces.InformationBase ] = [ ]
79 if isinstance( possessor, __.enum.EnumMeta ):
80 informations.extend(
81 _interfaces.AttributeInformation(
82 name = name,
83 annotation = possessor,
84 description = None,
85 association = _interfaces.AttributeAssociations.Class,
86 default = _default_suppress )
87 for name in possessor.__members__ )
88 return informations
89 return None
92def is_attribute_visible(
93 possessor: _interfaces.PossessorArgument,
94 name: str,
95 annotation: __.typx.Any,
96 description: __.typx.Optional[ str ],
97) -> bool:
98 ''' Determines if attribute should be visible in documentation.
100 Default visibility predicate that considers attribute with
101 description or public name (not starting with underscore) as visible.
103 If attribute possessor is module, then ``__all__`` is considered,
104 if it exists.
105 '''
106 if __.inspect.ismodule( possessor ):
107 publics = getattr( possessor, '__all__', None )
108 if publics is not None: return name in publics
109 return bool( description ) or not name.startswith( '_' )
112def reduce_annotation(
113 annotation: __.typx.Any,
114 context: _context.Context,
115 adjuncts: _interfaces.AdjunctsData,
116 cache: _interfaces.AnnotationsCache,
117) -> __.typx.Any:
118 ''' Reduces a complex type annotation to a simpler form.
120 Processes type annotations, extracting metadata from Annotated types
121 and simplifying complex generic types. Uses cache to avoid redundant
122 processing and prevent infinite recursion from reference cycles.
123 '''
124 annotation_r = cache.access( annotation )
125 # Avoid infinite recursion from reference cycles.
126 if annotation_r is _interfaces.incomplete:
127 emessage = (
128 f"Annotation with circular reference {annotation!r}; "
129 "returning Any." )
130 context.notifier( 'admonition', emessage )
131 return cache.enter( annotation, __.typx.Any )
132 # TODO: Short-circuit on cache hit.
133 # Need to ensure copy of adjuncts data is retrieved too.
134 # if annotation_r is not _interfaces.absent: return annotation_r
135 if isinstance( annotation, str ): # Cannot do much with unresolved strings.
136 return cache.enter( annotation, annotation )
137 if isinstance( annotation, __.typx.ForwardRef ): # Extract string.
138 return cache.enter( annotation, annotation.__forward_arg__ )
139 cache.enter( annotation ) # mark as incomplete
140 return cache.enter(
141 annotation,
142 _reduce_annotation_core( annotation, context, adjuncts, cache ) )
145def _access_annotations(
146 possessor: _nomina.Documentable, /, context: _context.Context
147) -> __.cabc.Mapping[ str, __.typx.Any ]:
148 # TODO? Option to attempt resolution of strings.
149 # Probably after retrieval of annotations dictionary
150 # to prevent 'NameError' from ruining everything.
151 # Would leave unresolvable strings as strings.
152 # TODO? Option 'strict' to force resolution of all strings.
153 # TODO: Switch to '__.typx.get_annotations'.
154 ''' Accesses annotations from documentable object. '''
155 # nomargs: _nomina.Variables = dict( eval_str = True )
156 # nomargs[ 'globals' ] = context.resolver_globals
157 # nomargs[ 'locals' ] = context.resolver_locals
158 try:
159 # return __.types.MappingProxyType(
160 # __.inspect.get_annotations( possessor, **nomargs ) )
161 return __.types.MappingProxyType(
162 __.inspect.get_annotations( possessor ) )
163 except ( NameError, TypeError ) as exc:
164 emessage = f"Cannot access annotations for {possessor!r}: {exc}"
165 context.notifier( 'error', emessage )
166 return __.dictproxy_empty
169def _classes_sequence_to_union(
170 annotation: type | __.cabc.Sequence[ type ]
171) -> __.typx.Any:
172 ''' Converts a sequence of exception classes to a Union type.
174 Used for Raises annotations to convert a sequence of exception
175 classes into a Union type for documentation.
176 '''
177 if not isinstance( annotation, __.cabc.Sequence ):
178 return annotation
179 return __.funct.reduce( __.operator.or_, annotation )
182def _compile_description(
183 context: _context.Context,
184 adjuncts: _interfaces.AdjunctsData,
185 table: _nomina.FragmentsTable,
186) -> str:
187 ''' Compiles a description from adjuncts data.
189 Processes Doc objects and Findex references in adjuncts data
190 to create a combined description string with proper formatting.
191 '''
192 fragments: list[ str ] = [ ]
193 for extra in adjuncts.extras:
194 if isinstance( extra, _interfaces.Doc ):
195 fragments.append( extra.documentation )
196 elif isinstance( extra, _interfaces.Fname ):
197 name = extra.name
198 if name not in table:
199 emessage = f"Fragment '{name}' not in provided table."
200 context.notifier( 'error', emessage )
201 else: fragments.append( table[ name ] )
202 return '\n\n'.join(
203 context.fragment_rectifier(
204 fragment, source = _interfaces.FragmentSources.Annotation )
205 for fragment in fragments )
208def _determine_default_valuator(
209 context: _context.Context,
210 adjuncts: _interfaces.AdjunctsData,
211) -> _interfaces.Default:
212 ''' Determines how default values should be handled.
214 Extracts the Default object from adjuncts data or falls back
215 to the default Default settings.
216 '''
217 return next(
218 ( extra for extra in adjuncts.extras
219 if isinstance( extra, _interfaces.Default ) ),
220 _default_default )
223def _filter_reconstitute_annotation(
224 origin: __.typx.Any,
225 arguments: __.cabc.Sequence[ __.typx.Any ],
226 context: _context.Context,
227 adjuncts: _interfaces.AdjunctsData,
228 cache: _interfaces.AnnotationsCache,
229) -> __.typx.Any:
230 ''' Filters and reconstitutes a generic type annotation.
232 After reducing the arguments of a generic type, this function
233 reconstitutes the type with the reduced arguments, potentially
234 applying transformations based on context.
236 Note that any type-adjacent information on arguments is not propagated
237 upwards, due to ambiguity in its insertion order relative to
238 type-adjacent information on the annotation origin.
239 '''
240 adjuncts.traits.add( origin.__name__ )
241 arguments_r: list[ __.typx.Any ] = [ ]
242 adjuncts_ = _interfaces.AdjunctsData( )
243 adjuncts_.traits.add( origin.__name__ )
244 match len( arguments ):
245 case 1:
246 arguments_r.append( reduce_annotation(
247 arguments[ 0 ], context, adjuncts_, cache ) )
248 case _:
249 arguments_r.extend( _reduce_annotation_arguments(
250 origin, arguments, context, adjuncts_, cache ) )
251 # TODO: Apply filters from context, replacing origin as necessary.
252 # E.g., ClassVar -> Union
253 # (Union with one argument returns the argument.)
254 try:
255 if origin in ( __.types.UnionType, __.typx.Union ):
256 # Unions cannot be reconstructed from sequences.
257 # TODO: Python 3.11: Unpack into subscript.
258 annotation = __.funct.reduce( __.operator.or_, arguments_r )
259 else:
260 match len( arguments_r ):
261 case 1: annotation = origin[ arguments_r[ 0 ] ]
262 case _: annotation = origin[ tuple( arguments_r ) ]
263 except TypeError as exc:
264 emessage = (
265 f"Cannot reconstruct {origin.__name__!r} "
266 f"with reduced annotations for arguments. Reason: {exc}" )
267 context.notifier( 'error', emessage )
268 return origin
269 return annotation
272def _introspect_class(
273 possessor: type, /,
274 context: _context.Context,
275 introspection: _context.IntrospectionControl,
276 cache: _interfaces.AnnotationsCache,
277 table: _nomina.FragmentsTable,
278) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
279 ''' Introspects a class to extract documentable information.
281 Gathers information about class annotations, potentially considering
282 inherited annotations based on introspection control settings. Tries
283 special class introspectors first, then falls back to standard
284 introspection.
285 '''
286 annotations_: dict[ str, __.typx.Any ] = { }
287 if introspection.class_control.inheritance:
288 # Descendant annotations override ancestor annotations.
289 for class_ in reversed( possessor.__mro__ ):
290 annotations_b = _access_annotations( class_, context )
291 annotations_.update( annotations_b )
292 annotations = annotations_
293 else: annotations = _access_annotations( possessor, context )
294 informations: list[ _interfaces.InformationBase ] = [ ]
295 for introspector in introspection.class_control.introspectors:
296 informations_ = introspector(
297 possessor,
298 context = context, introspection = introspection,
299 annotations = annotations, cache = cache, table = table )
300 if informations_ is not None:
301 informations.extend( informations_ )
302 break
303 else:
304 informations.extend( _introspect_class_annotations(
305 possessor, context, annotations, cache, table ) )
306 if introspection.class_control.scan_attributes:
307 informations.extend( _introspect_class_attributes(
308 possessor, context, annotations ) )
309 return tuple( informations )
312def _introspect_class_annotations(
313 possessor: type, /,
314 context: _context.Context,
315 annotations: __.cabc.Mapping[ str, __.typx.Any ],
316 cache: _interfaces.AnnotationsCache,
317 table: _nomina.FragmentsTable,
318) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
319 ''' Introspects annotations of a class.
321 Processes class annotations to extract information about class
322 attributes, including their types, descriptions from Doc objects,
323 and whether they are class or instance variables.
324 '''
325 informations: list[ _interfaces.InformationBase ] = [ ]
326 for name, annotation in annotations.items( ):
327 adjuncts = _interfaces.AdjunctsData( )
328 annotation_ = reduce_annotation(
329 annotation, context, adjuncts, cache )
330 description = _compile_description( context, adjuncts, table )
331 if not _is_attribute_visible(
332 possessor, name, annotation_, context, adjuncts, description
333 ): continue
334 association = (
335 _interfaces.AttributeAssociations.Class
336 if 'ClassVar' in adjuncts.traits
337 else _interfaces.AttributeAssociations.Instance )
338 default = _determine_default_valuator( context, adjuncts )
339 informations.append( _interfaces.AttributeInformation(
340 name = name,
341 annotation = annotation_,
342 description = description,
343 association = association,
344 default = default ) )
345 return informations
348def _introspect_class_attributes(
349 possessor: type, /,
350 context: _context.Context,
351 annotations: __.cabc.Mapping[ str, __.typx.Any ],
352) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
353 ''' Introspects attributes of a class not covered by annotations.
355 Examines class attributes that do not have corresponding annotations
356 and creates attribute information for those that should be visible.
357 '''
358 informations: list[ _interfaces.InformationBase ] = [ ]
359 adjuncts = _interfaces.AdjunctsData( ) # dummy value
360 for name, attribute in __.inspect.getmembers( possessor ):
361 if name in annotations: continue # already processed
362 if not _is_attribute_visible(
363 possessor, name, _interfaces.absent, context, adjuncts, None
364 ): continue
365 if callable( attribute ): continue # separately documented
366 informations.append( _interfaces.AttributeInformation(
367 name = name,
368 annotation = _interfaces.absent,
369 description = None,
370 association = _interfaces.AttributeAssociations.Class,
371 default = _default_default ) )
372 return informations
375def _introspect_function(
376 possessor: __.cabc.Callable[ ..., __.typx.Any ], /,
377 context: _context.Context,
378 cache: _interfaces.AnnotationsCache,
379 table: _nomina.FragmentsTable,
380) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
381 ''' Introspects a function to extract documentable information.
383 Gathers information about function arguments and return value
384 from annotations and signature analysis.
385 '''
386 annotations = _access_annotations( possessor, context )
387 if not annotations: return ( )
388 informations: list[ _interfaces.InformationBase ] = [ ]
389 try: signature = __.inspect.signature( possessor )
390 except ValueError as exc:
391 context.notifier(
392 'error',
393 f"Could not assess signature for {possessor.__qualname__!r}. "
394 f"Reason: {exc}" )
395 return ( )
396 if signature.parameters:
397 informations.extend( _introspect_function_valences(
398 annotations, signature, context, cache, table ) )
399 if 'return' in annotations:
400 informations.extend( _introspect_function_return(
401 annotations[ 'return' ], context, cache, table ) )
402 return tuple( informations )
405def _introspect_function_return(
406 annotation: __.typx.Any,
407 context: _context.Context,
408 cache: _interfaces.AnnotationsCache,
409 table: _nomina.FragmentsTable,
410) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
411 ''' Introspects function return annotation.
413 Processes function return annotation to extract return type information
414 and possible exception information from Raises annotations.
415 '''
416 informations: list[ _interfaces.InformationBase ] = [ ]
417 adjuncts = _interfaces.AdjunctsData( )
418 annotation_ = reduce_annotation( annotation, context, adjuncts, cache )
419 description = _compile_description( context, adjuncts, table )
420 informations.append(
421 _interfaces.ReturnInformation(
422 annotation = annotation_, description = description ) )
423 informations.extend(
424 _interfaces.ExceptionInformation(
425 annotation = _classes_sequence_to_union( extra.classes ),
426 description = extra.description )
427 for extra in adjuncts.extras
428 if isinstance( extra, _interfaces.Raises ) )
429 return tuple( informations )
432def _introspect_function_valences(
433 annotations: __.cabc.Mapping[ str, __.typx.Any ],
434 signature: __.inspect.Signature,
435 context: _context.Context,
436 cache: _interfaces.AnnotationsCache,
437 table: _nomina.FragmentsTable,
438) -> __.cabc.Sequence[ _interfaces.ArgumentInformation ]:
439 ''' Introspects function parameters to extract argument information.
441 Processes function signature and annotations to create information
442 about function arguments, including their types, descriptions, and
443 default value handling.
444 '''
445 informations: list[ _interfaces.ArgumentInformation ] = [ ]
446 for name, param in signature.parameters.items( ):
447 annotation = annotations.get( name, param.annotation )
448 adjuncts = _interfaces.AdjunctsData( )
449 if annotation is param.empty:
450 annotation_ = _interfaces.absent
451 description = None
452 else:
453 annotation_ = reduce_annotation(
454 annotation, context, adjuncts, cache )
455 description = _compile_description( context, adjuncts, table )
456 if param.default is param.empty: default = _default_suppress
457 else: default = _determine_default_valuator( context, adjuncts )
458 informations.append( _interfaces.ArgumentInformation(
459 name = name,
460 annotation = annotation_,
461 description = description,
462 paramspec = param,
463 default = default ) )
464 return tuple( informations )
467def _introspect_module(
468 possessor: __.types.ModuleType, /,
469 context: _context.Context,
470 introspection: _context.IntrospectionControl,
471 cache: _interfaces.AnnotationsCache,
472 table: _nomina.FragmentsTable,
473) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
474 ''' Introspects a module to extract documentable information.
476 Gathers information about module annotations and potentially about
477 module attributes based on introspection control settings.
478 '''
479 annotations = _access_annotations( possessor, context )
480 if not annotations: return ( )
481 informations: list[ _interfaces.InformationBase ] = [ ]
482 informations.extend( _introspect_module_annotations(
483 possessor, context, annotations, cache, table ) )
484 if introspection.module_control.scan_attributes:
485 informations.extend( _introspect_module_attributes(
486 possessor, context, annotations ) )
487 return tuple( informations )
490def _introspect_module_annotations(
491 possessor: __.types.ModuleType, /,
492 context: _context.Context,
493 annotations: __.cabc.Mapping[ str, __.typx.Any ],
494 cache: _interfaces.AnnotationsCache,
495 table: _nomina.FragmentsTable,
496) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
497 ''' Introspects annotations of a module.
499 Processes module annotations to extract information about module
500 attributes, including their types and descriptions from Doc objects.
501 '''
502 informations: list[ _interfaces.InformationBase ] = [ ]
503 for name, annotation in annotations.items( ):
504 adjuncts = _interfaces.AdjunctsData( )
505 annotation_ = reduce_annotation(
506 annotation, context, adjuncts, cache )
507 description = _compile_description( context, adjuncts, table )
508 if not _is_attribute_visible(
509 possessor, name, annotation_, context, adjuncts, description
510 ): continue
511 default = _determine_default_valuator( context, adjuncts )
512 informations.append( _interfaces.AttributeInformation(
513 name = name,
514 annotation = annotation_,
515 description = description,
516 association = _interfaces.AttributeAssociations.Module,
517 default = default ) )
518 return informations
521def _introspect_module_attributes(
522 possessor: __.types.ModuleType, /,
523 context: _context.Context,
524 annotations: __.cabc.Mapping[ str, __.typx.Any ],
525) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
526 ''' Introspects attributes of a module not covered by annotations.
528 Examines module attributes that do not have corresponding annotations
529 and creates attribute information for those that should be visible.
530 '''
531 informations: list[ _interfaces.InformationBase ] = [ ]
532 adjuncts = _interfaces.AdjunctsData( ) # dummy value
533 attribute: object
534 for name, attribute in __.inspect.getmembers( possessor ):
535 if name in annotations: continue # already processed
536 if not _is_attribute_visible(
537 possessor, name, _interfaces.absent, context, adjuncts, None
538 ): continue
539 if callable( attribute ): continue # separately documented
540 informations.append( _interfaces.AttributeInformation(
541 name = name,
542 annotation = _interfaces.absent,
543 description = None,
544 association = _interfaces.AttributeAssociations.Module,
545 default = _default_default ) )
546 return informations
549def _is_attribute_visible( # noqa: PLR0913
550 possessor: _nomina.Documentable,
551 name: str,
552 annotation: __.typx.Any,
553 context: _context.Context,
554 adjuncts: _interfaces.AdjunctsData,
555 description: __.typx.Optional[ str ],
556) -> bool:
557 ''' Determines if an attribute should be visible in documentation.
559 Checks for explicit visibility settings in adjuncts data and falls
560 back to the context's visibility decider if the visibility is set
561 to Default.
562 '''
563 visibility = next(
564 ( extra for extra in adjuncts.extras
565 if isinstance( extra, _interfaces.Visibilities ) ),
566 _interfaces.Visibilities.Default )
567 match visibility:
568 case _interfaces.Visibilities.Conceal: return False
569 case _interfaces.Visibilities.Reveal: return True
570 case _:
571 return context.visibility_decider(
572 possessor, name, annotation, description )
575def _reduce_annotation_arguments(
576 origin: __.typx.Any,
577 arguments: __.cabc.Sequence[ __.typx.Any ],
578 context: _context.Context,
579 adjuncts: _interfaces.AdjunctsData,
580 cache: _interfaces.AnnotationsCache,
581) -> __.cabc.Sequence[ __.typx.Any ]:
582 ''' Reduces the arguments of a generic type annotation.
584 Processes the arguments of a generic type like List[T] or Dict[K, V]
585 and returns the reduced forms of those arguments. Special handling
586 for Callable types.
587 '''
588 if __.inspect.isclass( origin ) and issubclass( origin, __.cabc.Callable ):
589 return _reduce_annotation_for_callable(
590 arguments, context, adjuncts.copy( ), cache )
591 return tuple(
592 reduce_annotation( argument, context, adjuncts.copy( ), cache )
593 for argument in arguments )
596def _reduce_annotation_core(
597 annotation: __.typx.Any,
598 context: _context.Context,
599 adjuncts: _interfaces.AdjunctsData,
600 cache: _interfaces.AnnotationsCache,
601) -> __.typx.Any:
602 ''' Core implementation of annotation reduction.
604 Handles the reduction of complex type annotations into simpler forms,
605 extracting metadata from Annotated types and processing generic types.
606 Returns the reduced annotation.
607 '''
608 origin = __.typx.get_origin( annotation )
609 # bare types, Ellipsis, typing.Any, typing.LiteralString, typing.Never,
610 # typing.TypeVar have no origin; taken as-is
611 # typing.Literal is considered fully reduced; taken as-is
612 if origin in ( None, __.typx.Literal ): return annotation
613 arguments = __.typx.get_args( annotation )
614 if not arguments: return annotation
615 if origin is __.typx.Annotated:
616 adjuncts.extras.extend( arguments[ 1 : ] )
617 return reduce_annotation(
618 annotation.__origin__, context, adjuncts, cache )
619 return _filter_reconstitute_annotation(
620 origin, arguments, context, adjuncts, cache )
623def _reduce_annotation_for_callable(
624 arguments: __.cabc.Sequence[ __.typx.Any ],
625 context: _context.Context,
626 adjuncts: _interfaces.AdjunctsData,
627 cache: _interfaces.AnnotationsCache,
628) -> tuple[ list[ __.typx.Any ] | __.types.EllipsisType, __.typx.Any ]:
629 ''' Reduces annotations for Callable types.
631 Special handling for Callable type annotations, which have a tuple
632 of (arguments, return_type). Processes the arguments list and return
633 type separately and returns the reduced forms.
634 '''
635 farguments, freturn = arguments
636 if farguments is Ellipsis:
637 farguments_r = Ellipsis
638 else:
639 farguments_r = [
640 reduce_annotation( element, context, adjuncts.copy( ), cache )
641 for element in farguments ]
642 freturn_r = (
643 reduce_annotation( freturn, context, adjuncts.copy( ), cache ) )
644 return ( farguments_r, freturn_r )