Coverage for sources/dynadoc/introspection.py: 100%
210 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-05-28 04:38 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-05-28 04:38 +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 cache.enter( annotation ) # mark as incomplete
136 return cache.enter(
137 annotation,
138 _reduce_annotation_core( annotation, context, adjuncts, cache ) )
141def _access_annotations(
142 possessor: _nomina.Documentable, /, context: _context.Context
143) -> __.cabc.Mapping[ str, __.typx.Any ]:
144 ''' Accesses annotations from a documentable object.
146 Retrieves annotations with appropriate resolver settings from the
147 context. Handles errors gracefully.
148 '''
149 nomargs: _nomina.Variables = dict( eval_str = True )
150 nomargs[ 'globals' ] = context.resolver_globals
151 nomargs[ 'locals' ] = context.resolver_locals
152 try:
153 return __.types.MappingProxyType(
154 __.inspect.get_annotations( possessor, **nomargs ) )
155 except TypeError as exc:
156 emessage = f"Cannot access annotations for {possessor!r}: {exc}"
157 context.notifier( 'error', emessage )
158 return __.dictproxy_empty
161def _classes_sequence_to_union(
162 annotation: type | __.cabc.Sequence[ type ]
163) -> __.typx.Any:
164 ''' Converts a sequence of exception classes to a Union type.
166 Used for Raises annotations to convert a sequence of exception
167 classes into a Union type for documentation.
168 '''
169 if not isinstance( annotation, __.cabc.Sequence ):
170 return annotation
171 return __.funct.reduce( __.operator.or_, annotation )
174def _compile_description(
175 context: _context.Context,
176 adjuncts: _interfaces.AdjunctsData,
177 table: _nomina.FragmentsTable,
178) -> str:
179 ''' Compiles a description from adjuncts data.
181 Processes Doc objects and Findex references in adjuncts data
182 to create a combined description string with proper formatting.
183 '''
184 fragments: list[ str ] = [ ]
185 for extra in adjuncts.extras:
186 if isinstance( extra, _interfaces.Doc ):
187 fragments.append( extra.documentation )
188 elif isinstance( extra, _interfaces.Fname ):
189 name = extra.name
190 if name not in table:
191 emessage = f"Fragment '{name}' not in provided table."
192 context.notifier( 'error', emessage )
193 else: fragments.append( table[ name ] )
194 return '\n\n'.join(
195 context.fragment_rectifier(
196 fragment, source = _interfaces.FragmentSources.Annotation )
197 for fragment in fragments )
200def _determine_default_valuator(
201 context: _context.Context,
202 adjuncts: _interfaces.AdjunctsData,
203) -> _interfaces.Default:
204 ''' Determines how default values should be handled.
206 Extracts the Default object from adjuncts data or falls back
207 to the default Default settings.
208 '''
209 return next(
210 ( extra for extra in adjuncts.extras
211 if isinstance( extra, _interfaces.Default ) ),
212 _default_default )
215def _filter_reconstitute_annotation(
216 origin: __.typx.Any,
217 arguments: __.cabc.Sequence[ __.typx.Any ],
218 context: _context.Context,
219 adjuncts: _interfaces.AdjunctsData,
220 cache: _interfaces.AnnotationsCache,
221) -> __.typx.Any:
222 ''' Filters and reconstitutes a generic type annotation.
224 After reducing the arguments of a generic type, this function
225 reconstitutes the type with the reduced arguments, potentially
226 applying transformations based on context.
227 '''
228 adjuncts.traits.add( origin.__name__ )
229 arguments_r: list[ __.typx.Any ] = [ ]
230 match len( arguments ):
231 case 1:
232 arguments_r.append( reduce_annotation(
233 arguments[ 0 ], context, adjuncts, cache ) )
234 case _:
235 # upward propagation is ambiguous, so sever adjuncts data
236 adjuncts_ = _interfaces.AdjunctsData( )
237 adjuncts_.traits.add( origin.__name__ )
238 arguments_r.extend( _reduce_annotation_arguments(
239 origin, arguments, context, adjuncts_.copy( ), cache ) )
240 # TODO: Apply filters from context, replacing origin as necessary.
241 # E.g., ClassVar -> Union
242 # (Union with one argument returns the argument.)
243 try:
244 if origin in ( __.types.UnionType, __.typx.Union ):
245 # Unions cannot be reconstructed from sequences.
246 # TODO: Python 3.11: Unpack into subscript.
247 annotation = __.funct.reduce( __.operator.or_, arguments_r )
248 else:
249 match len( arguments_r ):
250 case 1: annotation = origin[ arguments_r[ 0 ] ]
251 case _: annotation = origin[ tuple( arguments_r ) ]
252 except TypeError as exc:
253 emessage = (
254 f"Cannot reconstruct {origin.__name__!r} "
255 f"with reduced annotations for arguments. Reason: {exc}" )
256 context.notifier( 'error', emessage )
257 return origin
258 return annotation
261def _introspect_class(
262 possessor: type, /,
263 context: _context.Context,
264 introspection: _context.IntrospectionControl,
265 cache: _interfaces.AnnotationsCache,
266 table: _nomina.FragmentsTable,
267) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
268 ''' Introspects a class to extract documentable information.
270 Gathers information about class annotations, potentially considering
271 inherited annotations based on introspection control settings. Tries
272 special class introspectors first, then falls back to standard
273 introspection.
274 '''
275 annotations_: dict[ str, __.typx.Any ] = { }
276 if introspection.class_control.inheritance:
277 # Descendant annotations override ancestor annotations.
278 for class_ in reversed( possessor.__mro__ ):
279 annotations_b = _access_annotations( class_, context )
280 annotations_.update( annotations_b )
281 annotations = annotations_
282 else: annotations = _access_annotations( possessor, context )
283 informations: list[ _interfaces.InformationBase ] = [ ]
284 for introspector in introspection.class_control.introspectors:
285 informations_ = introspector(
286 possessor,
287 context = context, introspection = introspection,
288 annotations = annotations, cache = cache, table = table )
289 if informations_ is not None:
290 informations.extend( informations_ )
291 break
292 else:
293 informations.extend( _introspect_class_annotations(
294 possessor, context, annotations, cache, table ) )
295 if introspection.class_control.scan_attributes:
296 informations.extend( _introspect_class_attributes(
297 possessor, context, annotations ) )
298 return tuple( informations )
301def _introspect_class_annotations(
302 possessor: type, /,
303 context: _context.Context,
304 annotations: __.cabc.Mapping[ str, __.typx.Any ],
305 cache: _interfaces.AnnotationsCache,
306 table: _nomina.FragmentsTable,
307) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
308 ''' Introspects annotations of a class.
310 Processes class annotations to extract information about class
311 attributes, including their types, descriptions from Doc objects,
312 and whether they are class or instance variables.
313 '''
314 informations: list[ _interfaces.InformationBase ] = [ ]
315 for name, annotation in annotations.items( ):
316 adjuncts = _interfaces.AdjunctsData( )
317 annotation_ = reduce_annotation(
318 annotation, context, adjuncts, cache )
319 description = _compile_description( context, adjuncts, table )
320 if not _is_attribute_visible(
321 possessor, name, annotation_, context, adjuncts, description
322 ): continue
323 association = (
324 _interfaces.AttributeAssociations.Class
325 if 'ClassVar' in adjuncts.traits
326 else _interfaces.AttributeAssociations.Instance )
327 default = _determine_default_valuator( context, adjuncts )
328 informations.append( _interfaces.AttributeInformation(
329 name = name,
330 annotation = annotation_,
331 description = description,
332 association = association,
333 default = default ) )
334 return informations
337def _introspect_class_attributes(
338 possessor: type, /,
339 context: _context.Context,
340 annotations: __.cabc.Mapping[ str, __.typx.Any ],
341) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
342 ''' Introspects attributes of a class not covered by annotations.
344 Examines class attributes that do not have corresponding annotations
345 and creates attribute information for those that should be visible.
346 '''
347 informations: list[ _interfaces.InformationBase ] = [ ]
348 adjuncts = _interfaces.AdjunctsData( ) # dummy value
349 for name, attribute in __.inspect.getmembers( possessor ):
350 if name in annotations: continue # already processed
351 if not _is_attribute_visible(
352 possessor, name, _interfaces.absent, context, adjuncts, None
353 ): continue
354 if callable( attribute ): continue # separately documented
355 informations.append( _interfaces.AttributeInformation(
356 name = name,
357 annotation = _interfaces.absent,
358 description = None,
359 association = _interfaces.AttributeAssociations.Class,
360 default = _default_default ) )
361 return informations
364def _introspect_function(
365 possessor: __.cabc.Callable[ ..., __.typx.Any ], /,
366 context: _context.Context,
367 cache: _interfaces.AnnotationsCache,
368 table: _nomina.FragmentsTable,
369) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
370 ''' Introspects a function to extract documentable information.
372 Gathers information about function arguments and return value
373 from annotations and signature analysis.
374 '''
375 annotations = _access_annotations( possessor, context )
376 if not annotations: return ( )
377 informations: list[ _interfaces.InformationBase ] = [ ]
378 try: signature = __.inspect.signature( possessor )
379 except ValueError as exc:
380 context.notifier(
381 'error',
382 f"Could not assess signature for {possessor.__qualname__!r}. "
383 f"Reason: {exc}" )
384 return ( )
385 if signature.parameters:
386 informations.extend( _introspect_function_valences(
387 annotations, signature, context, cache, table ) )
388 if 'return' in annotations:
389 informations.extend( _introspect_function_return(
390 annotations[ 'return' ], context, cache, table ) )
391 return tuple( informations )
394def _introspect_function_return(
395 annotation: __.typx.Any,
396 context: _context.Context,
397 cache: _interfaces.AnnotationsCache,
398 table: _nomina.FragmentsTable,
399) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
400 ''' Introspects function return annotation.
402 Processes function return annotation to extract return type information
403 and possible exception information from Raises annotations.
404 '''
405 informations: list[ _interfaces.InformationBase ] = [ ]
406 adjuncts = _interfaces.AdjunctsData( )
407 annotation_ = reduce_annotation( annotation, context, adjuncts, cache )
408 description = _compile_description( context, adjuncts, table )
409 informations.append(
410 _interfaces.ReturnInformation(
411 annotation = annotation_, description = description ) )
412 informations.extend(
413 _interfaces.ExceptionInformation(
414 annotation = _classes_sequence_to_union( extra.classes ),
415 description = extra.description )
416 for extra in adjuncts.extras
417 if isinstance( extra, _interfaces.Raises ) )
418 return tuple( informations )
421def _introspect_function_valences(
422 annotations: __.cabc.Mapping[ str, __.typx.Any ],
423 signature: __.inspect.Signature,
424 context: _context.Context,
425 cache: _interfaces.AnnotationsCache,
426 table: _nomina.FragmentsTable,
427) -> __.cabc.Sequence[ _interfaces.ArgumentInformation ]:
428 ''' Introspects function parameters to extract argument information.
430 Processes function signature and annotations to create information
431 about function arguments, including their types, descriptions, and
432 default value handling.
433 '''
434 informations: list[ _interfaces.ArgumentInformation ] = [ ]
435 for name, param in signature.parameters.items( ):
436 annotation = annotations.get( name, param.annotation )
437 adjuncts = _interfaces.AdjunctsData( )
438 if annotation is param.empty:
439 annotation_ = _interfaces.absent
440 description = None
441 else:
442 annotation_ = reduce_annotation(
443 annotation, context, adjuncts, cache )
444 description = _compile_description( context, adjuncts, table )
445 if param.default is param.empty: default = _default_suppress
446 else: default = _determine_default_valuator( context, adjuncts )
447 informations.append( _interfaces.ArgumentInformation(
448 name = name,
449 annotation = annotation_,
450 description = description,
451 paramspec = param,
452 default = default ) )
453 return tuple( informations )
456def _introspect_module(
457 possessor: __.types.ModuleType, /,
458 context: _context.Context,
459 introspection: _context.IntrospectionControl,
460 cache: _interfaces.AnnotationsCache,
461 table: _nomina.FragmentsTable,
462) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
463 ''' Introspects a module to extract documentable information.
465 Gathers information about module annotations and potentially about
466 module attributes based on introspection control settings.
467 '''
468 annotations = _access_annotations( possessor, context )
469 if not annotations: return ( )
470 informations: list[ _interfaces.InformationBase ] = [ ]
471 informations.extend( _introspect_module_annotations(
472 possessor, context, annotations, cache, table ) )
473 if introspection.module_control.scan_attributes:
474 informations.extend( _introspect_module_attributes(
475 possessor, context, annotations ) )
476 return tuple( informations )
479def _introspect_module_annotations(
480 possessor: __.types.ModuleType, /,
481 context: _context.Context,
482 annotations: __.cabc.Mapping[ str, __.typx.Any ],
483 cache: _interfaces.AnnotationsCache,
484 table: _nomina.FragmentsTable,
485) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
486 ''' Introspects annotations of a module.
488 Processes module annotations to extract information about module
489 attributes, including their types and descriptions from Doc objects.
490 '''
491 informations: list[ _interfaces.InformationBase ] = [ ]
492 for name, annotation in annotations.items( ):
493 adjuncts = _interfaces.AdjunctsData( )
494 annotation_ = reduce_annotation(
495 annotation, context, adjuncts, cache )
496 description = _compile_description( context, adjuncts, table )
497 if not _is_attribute_visible(
498 possessor, name, annotation_, context, adjuncts, description
499 ): continue
500 default = _determine_default_valuator( context, adjuncts )
501 informations.append( _interfaces.AttributeInformation(
502 name = name,
503 annotation = annotation_,
504 description = description,
505 association = _interfaces.AttributeAssociations.Module,
506 default = default ) )
507 return informations
510def _introspect_module_attributes(
511 possessor: __.types.ModuleType, /,
512 context: _context.Context,
513 annotations: __.cabc.Mapping[ str, __.typx.Any ],
514) -> __.cabc.Sequence[ _interfaces.InformationBase ]:
515 ''' Introspects attributes of a module not covered by annotations.
517 Examines module attributes that do not have corresponding annotations
518 and creates attribute information for those that should be visible.
519 '''
520 informations: list[ _interfaces.InformationBase ] = [ ]
521 adjuncts = _interfaces.AdjunctsData( ) # dummy value
522 attribute: object
523 for name, attribute in __.inspect.getmembers( possessor ):
524 if name in annotations: continue # already processed
525 if not _is_attribute_visible(
526 possessor, name, _interfaces.absent, context, adjuncts, None
527 ): continue
528 if callable( attribute ): continue # separately documented
529 informations.append( _interfaces.AttributeInformation(
530 name = name,
531 annotation = _interfaces.absent,
532 description = None,
533 association = _interfaces.AttributeAssociations.Module,
534 default = _default_default ) )
535 return informations
538def _is_attribute_visible( # noqa: PLR0913
539 possessor: _nomina.Documentable,
540 name: str,
541 annotation: __.typx.Any,
542 context: _context.Context,
543 adjuncts: _interfaces.AdjunctsData,
544 description: __.typx.Optional[ str ],
545) -> bool:
546 ''' Determines if an attribute should be visible in documentation.
548 Checks for explicit visibility settings in adjuncts data and falls
549 back to the context's visibility decider if the visibility is set
550 to Default.
551 '''
552 visibility = next(
553 ( extra for extra in adjuncts.extras
554 if isinstance( extra, _interfaces.Visibilities ) ),
555 _interfaces.Visibilities.Default )
556 match visibility:
557 case _interfaces.Visibilities.Conceal: return False
558 case _interfaces.Visibilities.Reveal: return True
559 case _:
560 return context.visibility_decider(
561 possessor, name, annotation, description )
564def _reduce_annotation_arguments(
565 origin: __.typx.Any,
566 arguments: __.cabc.Sequence[ __.typx.Any ],
567 context: _context.Context,
568 adjuncts: _interfaces.AdjunctsData,
569 cache: _interfaces.AnnotationsCache,
570) -> __.cabc.Sequence[ __.typx.Any ]:
571 ''' Reduces the arguments of a generic type annotation.
573 Processes the arguments of a generic type like List[T] or Dict[K, V]
574 and returns the reduced forms of those arguments. Special handling
575 for Callable types.
576 '''
577 if __.inspect.isclass( origin ) and issubclass( origin, __.cabc.Callable ):
578 return _reduce_annotation_for_callable(
579 arguments, context, adjuncts.copy( ), cache )
580 return tuple(
581 reduce_annotation( argument, context, adjuncts.copy( ), cache )
582 for argument in arguments )
585def _reduce_annotation_core(
586 annotation: __.typx.Any,
587 context: _context.Context,
588 adjuncts: _interfaces.AdjunctsData,
589 cache: _interfaces.AnnotationsCache,
590) -> __.typx.Any:
591 ''' Core implementation of annotation reduction.
593 Handles the reduction of complex type annotations into simpler forms,
594 extracting metadata from Annotated types and processing generic types.
595 Returns the reduced annotation.
596 '''
597 origin = __.typx.get_origin( annotation )
598 # bare types, Ellipsis, typing.Any, typing.LiteralString, typing.Never,
599 # typing.TypeVar have no origin; taken as-is
600 # typing.Literal is considered fully reduced; taken as-is
601 if origin in ( None, __.typx.Literal ): return annotation
602 arguments = __.typx.get_args( annotation )
603 if not arguments: return annotation
604 if origin is __.typx.Annotated:
605 adjuncts.extras.extend( arguments[ 1 : ] )
606 return reduce_annotation(
607 annotation.__origin__, context, adjuncts, cache )
608 return _filter_reconstitute_annotation(
609 origin, arguments, context, adjuncts, cache )
612def _reduce_annotation_for_callable(
613 arguments: __.cabc.Sequence[ __.typx.Any ],
614 context: _context.Context,
615 adjuncts: _interfaces.AdjunctsData,
616 cache: _interfaces.AnnotationsCache,
617) -> tuple[ list[ __.typx.Any ] | __.types.EllipsisType, __.typx.Any ]:
618 ''' Reduces annotations for Callable types.
620 Special handling for Callable type annotations, which have a tuple
621 of (arguments, return_type). Processes the arguments list and return
622 type separately and returns the reduced forms.
623 '''
624 farguments, freturn = arguments
625 if farguments is Ellipsis:
626 farguments_r = Ellipsis
627 else:
628 farguments_r = [
629 reduce_annotation( element, context, adjuncts.copy( ), cache )
630 for element in farguments ]
631 freturn_r = (
632 reduce_annotation( freturn, context, adjuncts.copy( ), cache ) )
633 return ( farguments_r, freturn_r )