|
12 | 12 | from importlib.machinery import EXTENSION_SUFFIXES |
13 | 13 | from importlib.util import decode_source, find_spec, module_from_spec, spec_from_loader |
14 | 14 | from pathlib import Path |
15 | | -from types import SimpleNamespace |
| 15 | +from types import ModuleType, SimpleNamespace |
16 | 16 | from typing import TYPE_CHECKING, NewType, TypeVar |
| 17 | +from weakref import WeakSet |
17 | 18 |
|
18 | 19 | from sphinx.errors import PycodeError |
19 | 20 | from sphinx.ext.autodoc._property_types import ( |
|
41 | 42 | if TYPE_CHECKING: |
42 | 43 | from collections.abc import Sequence |
43 | 44 | from importlib.machinery import ModuleSpec |
44 | | - from types import ModuleType |
45 | 45 | from typing import Any, Protocol |
46 | 46 |
|
47 | 47 | from sphinx.config import Config |
@@ -660,20 +660,7 @@ def _load_object_by_name( |
660 | 660 | ) |
661 | 661 | elif objtype == 'data': |
662 | 662 | # Update __annotations__ to support type_comment and so on |
663 | | - annotations = dict(inspect.getannotations(parent)) |
664 | | - parent.__annotations__ = annotations |
665 | | - |
666 | | - try: |
667 | | - analyzer = ModuleAnalyzer.for_module(module_name) |
668 | | - analyzer.analyze() |
669 | | - for ( |
670 | | - classname, |
671 | | - attrname, |
672 | | - ), annotation in analyzer.annotations.items(): |
673 | | - if not classname and attrname not in annotations: |
674 | | - annotations[attrname] = annotation |
675 | | - except PycodeError: |
676 | | - pass |
| 663 | + _ensure_annotations_from_type_comments(parent) |
677 | 664 |
|
678 | 665 | # obtain annotation |
679 | 666 | annotations = get_type_hints( |
@@ -729,27 +716,8 @@ def _load_object_by_name( |
729 | 716 | elif inspect.isenumattribute(obj): |
730 | 717 | obj = obj.value |
731 | 718 | if parent: |
732 | | - # Update __annotations__ to support type_comment and so on. |
733 | | - try: |
734 | | - annotations = dict(inspect.getannotations(parent)) |
735 | | - parent.__annotations__ = annotations |
736 | | - |
737 | | - for cls in inspect.getmro(parent): |
738 | | - try: |
739 | | - module = safe_getattr(cls, '__module__') |
740 | | - qualname = safe_getattr(cls, '__qualname__') |
741 | | - |
742 | | - analyzer = ModuleAnalyzer.for_module(module) |
743 | | - analyzer.analyze() |
744 | | - anns = analyzer.annotations |
745 | | - for (classname, attrname), annotation in anns.items(): |
746 | | - if classname == qualname and attrname not in annotations: |
747 | | - annotations[attrname] = annotation |
748 | | - except (AttributeError, PycodeError): |
749 | | - pass |
750 | | - except (AttributeError, TypeError): |
751 | | - # Failed to set __annotations__ (built-in, extensions, etc.) |
752 | | - pass |
| 719 | + # Update __annotations__ to support type_comment and so on |
| 720 | + _ensure_annotations_from_type_comments(parent) |
753 | 721 |
|
754 | 722 | # obtain annotation |
755 | 723 | annotations = get_type_hints( |
@@ -964,3 +932,81 @@ def _resolve_name( |
964 | 932 | return module_name, (*parents, base) |
965 | 933 |
|
966 | 934 | return None |
| 935 | + |
| 936 | + |
| 937 | +_objects_with_type_comment_annotations: WeakSet[Any] = WeakSet() |
| 938 | +"""Cache of objects with annotations updated from type comments.""" |
| 939 | + |
| 940 | + |
| 941 | +def _ensure_annotations_from_type_comments(obj: Any) -> None: |
| 942 | + """Ensures `obj.__annotations__` includes type comment information. |
| 943 | +
|
| 944 | + Failures to assign to `__annotations__` are silently ignored. |
| 945 | +
|
| 946 | + If `obj` is a class type, this also ensures that type comment |
| 947 | + information is incorporated into the `__annotations__` member of |
| 948 | + all parent classes, if possible. |
| 949 | +
|
| 950 | + This mutates the `__annotations__` of existing imported objects, |
| 951 | + in order to allow the existing `typing.get_type_hints` method to |
| 952 | + take the modified annotations into account. |
| 953 | +
|
| 954 | + Modifying existing imported objects is unfortunate but avoids the |
| 955 | + need to reimplement `typing.get_type_hints` in order to take into |
| 956 | + account type comment information. |
| 957 | +
|
| 958 | + Note that this does not directly include type comment information |
| 959 | + from parent classes, but `typing.get_type_hints` takes that into |
| 960 | + account. |
| 961 | + """ |
| 962 | + if obj in _objects_with_type_comment_annotations: |
| 963 | + return |
| 964 | + _objects_with_type_comment_annotations.add(obj) |
| 965 | + |
| 966 | + if isinstance(obj, type): |
| 967 | + for cls in inspect.getmro(obj): |
| 968 | + modname = safe_getattr(cls, '__module__') |
| 969 | + mod = sys.modules.get(modname) |
| 970 | + if mod is not None: |
| 971 | + _ensure_annotations_from_type_comments(mod) |
| 972 | + |
| 973 | + elif isinstance(obj, ModuleType): |
| 974 | + _update_module_annotations_from_type_comments(obj) |
| 975 | + |
| 976 | + |
| 977 | +def _update_module_annotations_from_type_comments(mod: ModuleType) -> None: |
| 978 | + """Adds type comment annotations for a single module. |
| 979 | +
|
| 980 | + Both module-level and class-level annotations are added. |
| 981 | + """ |
| 982 | + mod_annotations = dict(inspect.getannotations(mod)) |
| 983 | + mod.__annotations__ = mod_annotations |
| 984 | + |
| 985 | + class_annotations: dict[str, dict[str, Any]] = {} |
| 986 | + |
| 987 | + try: |
| 988 | + analyzer = ModuleAnalyzer.for_module(mod.__name__) |
| 989 | + analyzer.analyze() |
| 990 | + anns = analyzer.annotations |
| 991 | + for (classname, attrname), annotation in anns.items(): |
| 992 | + if not classname: |
| 993 | + annotations = mod_annotations |
| 994 | + else: |
| 995 | + cls_annotations = class_annotations.get(classname) |
| 996 | + if cls_annotations is None: |
| 997 | + try: |
| 998 | + cls = mod |
| 999 | + for part in classname.split('.'): |
| 1000 | + cls = safe_getattr(cls, part) |
| 1001 | + annotations = dict(inspect.getannotations(cls)) |
| 1002 | + # Ignore errors setting __annotations__ |
| 1003 | + with contextlib.suppress(TypeError, AttributeError): |
| 1004 | + cls.__annotations__ = annotations |
| 1005 | + except AttributeError: |
| 1006 | + annotations = {} |
| 1007 | + class_annotations[classname] = annotations |
| 1008 | + else: |
| 1009 | + annotations = cls_annotations |
| 1010 | + annotations.setdefault(attrname, annotation) |
| 1011 | + except PycodeError: |
| 1012 | + pass |
0 commit comments