pdoc.doc_types
This module handles all interpretation of type annotations in pdoc.
In particular, it provides functionality to resolve typing.ForwardRef objects without raising an exception.
1""" 2This module handles all interpretation of type annotations in pdoc. 3 4In particular, it provides functionality to resolve 5[typing.ForwardRef](https://docs.python.org/3/library/typing.html#typing.ForwardRef) objects without raising an 6exception. 7""" 8from __future__ import annotations 9 10import functools 11import inspect 12import operator 13import sys 14import types 15from types import BuiltinFunctionType 16from types import ModuleType 17import typing 18from typing import TYPE_CHECKING 19from typing import Any 20from typing import Literal 21from typing import _GenericAlias # type: ignore 22from typing import get_origin 23import warnings 24 25from . import extract 26from ._compat import GenericAlias 27from ._compat import UnionType 28from .doc_ast import type_checking_sections 29 30if TYPE_CHECKING: 31 32 class empty: 33 pass 34 35 36empty: type = inspect.Signature.empty # type: ignore # noqa 37""" 38A "special" object signaling the absence of a type annotation. 39This is useful to distinguish it from an actual annotation with `None`. 40This value is an alias of `inspect.Signature.empty`. 41""" 42 43# adapted from 44# https://github.com/python/cpython/blob/9feae41c4f04ca27fd2c865807a5caeb50bf4fc4/Lib/inspect.py#L1740-L1747 45# ✂ start ✂ 46_WrapperDescriptor = type(type.__call__) 47_MethodWrapper = type(all.__call__) # type: ignore 48_ClassMethodWrapper = type(int.__dict__["from_bytes"]) 49 50NonUserDefinedCallables = ( 51 _WrapperDescriptor, 52 _MethodWrapper, 53 _ClassMethodWrapper, 54 BuiltinFunctionType, 55) 56 57 58# ✂ end ✂ 59 60 61def resolve_annotations( 62 annotations: dict[str, Any], 63 module: ModuleType | None, 64 localns: dict[str, Any] | None, 65 fullname: str, 66) -> dict[str, Any]: 67 """ 68 Given an `annotations` dictionary with type annotations (for example, `cls.__annotations__`), 69 this function tries to resolve all types using `pdoc.doc_types.safe_eval_type`. 70 71 Returns: A dictionary with the evaluated types. 72 """ 73 globalns = getattr(module, "__dict__", {}) 74 75 resolved = {} 76 for name, value in annotations.items(): 77 resolved[name] = safe_eval_type( 78 value, globalns, localns, module, f"{fullname}.{name}" 79 ) 80 81 return resolved 82 83 84def safe_eval_type( 85 t: Any, 86 globalns: dict[str, Any], 87 localns: dict[str, Any] | None, 88 module: types.ModuleType | None, 89 fullname: str, 90) -> Any: 91 """ 92 This method wraps `typing._eval_type`, but doesn't raise on errors. 93 It is used to evaluate a type annotation, which might already be 94 a proper type (in which case no action is required), or a forward reference string, 95 which needs to be resolved. 96 97 If _eval_type fails, we try some heuristics to import a missing module. 98 If that still fails, a warning is emitted and `t` is returned as-is. 99 """ 100 try: 101 return _eval_type(t, globalns, localns) 102 except AttributeError as e: 103 err = str(e) 104 _, obj, _, attr, _ = err.split("'") 105 mod = f"{obj}.{attr}" 106 except NameError as e: 107 err = str(e) 108 _, mod, _ = err.split("'") 109 except Exception as e: 110 if "unsupported operand type(s) for |" in str(e) and sys.version_info < (3, 10): 111 py_ver = ".".join(str(x) for x in sys.version_info[:3]) 112 warnings.warn( 113 f"Error parsing type annotation {t} for {fullname}: {e}. " 114 f"You are likely attempting to use Python 3.10 syntax (PEP 604 union types) with an older Python " 115 f"release. `X | Y`-style type annotations are invalid syntax on Python {py_ver}, which is what your " 116 f"pdoc instance is using. `from future import __annotations__` (PEP 563) postpones evaluation of " 117 f"annotations, which is why your program won't crash right away. However, pdoc needs to evaluate your " 118 f"type annotations and is unable to do so on Python {py_ver}. To fix this issue, either invoke pdoc " 119 f"from Python 3.10+, or switch to `typing.Union[]` syntax." 120 ) 121 else: 122 warnings.warn(f"Error parsing type annotation {t} for {fullname}: {e}") 123 return t 124 125 # Simple _eval_type has failed. We now execute all TYPE_CHECKING sections in the module and try again. 126 if module: 127 try: 128 code = compile(type_checking_sections(module), "<string>", "exec") 129 eval(code, globalns, globalns) 130 except Exception as e: 131 warnings.warn( 132 f"Failed to run TYPE_CHECKING code while parsing {t} type annotation for {fullname}: {e}" 133 ) 134 try: 135 return _eval_type(t, globalns, None) 136 except (AttributeError, NameError): 137 pass # still not found 138 except Exception as e: 139 warnings.warn( 140 f"Error parsing type annotation {t} for {fullname} after evaluating TYPE_CHECKING blocks: {e}" 141 ) 142 return t 143 144 try: 145 val = extract.load_module(mod) 146 except Exception: 147 warnings.warn( 148 f"Error parsing type annotation {t} for {fullname}. Import of {mod} failed: {err}" 149 ) 150 return t 151 return safe_eval_type(t, {mod: val, **globalns}, localns, module, fullname) 152 153 154def _eval_type(t, globalns, localns, recursive_guard=frozenset()): 155 # Adapted from typing._eval_type. 156 # Added type coercion originally found in get_type_hints, but removed NoneType check because that was distracting. 157 # Added a special check for typing.Literal, whose literal strings would otherwise be evaluated. 158 159 if isinstance(t, str): 160 if sys.version_info < (3, 9): # pragma: no cover 161 t = t.strip("\"'") 162 t = typing.ForwardRef(t) 163 164 if get_origin(t) is Literal: 165 return t 166 167 if isinstance(t, typing.ForwardRef): 168 # inlined from 169 # https://github.com/python/cpython/blob/4f51fa9e2d3ea9316e674fb9a9f3e3112e83661c/Lib/typing.py#L684-L707 170 if t.__forward_arg__ in recursive_guard: # pragma: no cover 171 return t 172 if not t.__forward_evaluated__ or localns is not globalns: 173 if globalns is None and localns is None: # pragma: no cover 174 globalns = localns = {} 175 elif globalns is None: # pragma: no cover 176 globalns = localns 177 elif localns is None: # pragma: no cover 178 localns = globalns 179 __forward_module__ = getattr(t, "__forward_module__", None) 180 if __forward_module__ is not None: 181 globalns = getattr( 182 sys.modules.get(__forward_module__, None), "__dict__", globalns 183 ) 184 (type_,) = (eval(t.__forward_code__, globalns, localns),) 185 t.__forward_value__ = _eval_type( 186 type_, globalns, localns, recursive_guard | {t.__forward_arg__} 187 ) 188 t.__forward_evaluated__ = True 189 return t.__forward_value__ 190 191 # https://github.com/python/cpython/blob/main/Lib/typing.py#L333-L343 192 # fmt: off 193 # ✂ start ✂ 194 if isinstance(t, (_GenericAlias, GenericAlias, UnionType)): 195 ev_args = tuple(_eval_type(a, globalns, localns, recursive_guard) for a in t.__args__) 196 if ev_args == t.__args__: 197 return t 198 if isinstance(t, GenericAlias): 199 return GenericAlias(t.__origin__, ev_args) 200 if isinstance(t, UnionType): 201 return functools.reduce(operator.or_, ev_args) 202 else: 203 return t.copy_with(ev_args) 204 return t 205 # ✂ end ✂
A "special" object signaling the absence of a type annotation.
This is useful to distinguish it from an actual annotation with None
.
This value is an alias of inspect.Signature.empty
.
62def resolve_annotations( 63 annotations: dict[str, Any], 64 module: ModuleType | None, 65 localns: dict[str, Any] | None, 66 fullname: str, 67) -> dict[str, Any]: 68 """ 69 Given an `annotations` dictionary with type annotations (for example, `cls.__annotations__`), 70 this function tries to resolve all types using `pdoc.doc_types.safe_eval_type`. 71 72 Returns: A dictionary with the evaluated types. 73 """ 74 globalns = getattr(module, "__dict__", {}) 75 76 resolved = {} 77 for name, value in annotations.items(): 78 resolved[name] = safe_eval_type( 79 value, globalns, localns, module, f"{fullname}.{name}" 80 ) 81 82 return resolved
Given an annotations
dictionary with type annotations (for example, cls.__annotations__
),
this function tries to resolve all types using safe_eval_type
.
Returns: A dictionary with the evaluated types.
85def safe_eval_type( 86 t: Any, 87 globalns: dict[str, Any], 88 localns: dict[str, Any] | None, 89 module: types.ModuleType | None, 90 fullname: str, 91) -> Any: 92 """ 93 This method wraps `typing._eval_type`, but doesn't raise on errors. 94 It is used to evaluate a type annotation, which might already be 95 a proper type (in which case no action is required), or a forward reference string, 96 which needs to be resolved. 97 98 If _eval_type fails, we try some heuristics to import a missing module. 99 If that still fails, a warning is emitted and `t` is returned as-is. 100 """ 101 try: 102 return _eval_type(t, globalns, localns) 103 except AttributeError as e: 104 err = str(e) 105 _, obj, _, attr, _ = err.split("'") 106 mod = f"{obj}.{attr}" 107 except NameError as e: 108 err = str(e) 109 _, mod, _ = err.split("'") 110 except Exception as e: 111 if "unsupported operand type(s) for |" in str(e) and sys.version_info < (3, 10): 112 py_ver = ".".join(str(x) for x in sys.version_info[:3]) 113 warnings.warn( 114 f"Error parsing type annotation {t} for {fullname}: {e}. " 115 f"You are likely attempting to use Python 3.10 syntax (PEP 604 union types) with an older Python " 116 f"release. `X | Y`-style type annotations are invalid syntax on Python {py_ver}, which is what your " 117 f"pdoc instance is using. `from future import __annotations__` (PEP 563) postpones evaluation of " 118 f"annotations, which is why your program won't crash right away. However, pdoc needs to evaluate your " 119 f"type annotations and is unable to do so on Python {py_ver}. To fix this issue, either invoke pdoc " 120 f"from Python 3.10+, or switch to `typing.Union[]` syntax." 121 ) 122 else: 123 warnings.warn(f"Error parsing type annotation {t} for {fullname}: {e}") 124 return t 125 126 # Simple _eval_type has failed. We now execute all TYPE_CHECKING sections in the module and try again. 127 if module: 128 try: 129 code = compile(type_checking_sections(module), "<string>", "exec") 130 eval(code, globalns, globalns) 131 except Exception as e: 132 warnings.warn( 133 f"Failed to run TYPE_CHECKING code while parsing {t} type annotation for {fullname}: {e}" 134 ) 135 try: 136 return _eval_type(t, globalns, None) 137 except (AttributeError, NameError): 138 pass # still not found 139 except Exception as e: 140 warnings.warn( 141 f"Error parsing type annotation {t} for {fullname} after evaluating TYPE_CHECKING blocks: {e}" 142 ) 143 return t 144 145 try: 146 val = extract.load_module(mod) 147 except Exception: 148 warnings.warn( 149 f"Error parsing type annotation {t} for {fullname}. Import of {mod} failed: {err}" 150 ) 151 return t 152 return safe_eval_type(t, {mod: val, **globalns}, localns, module, fullname)
This method wraps typing._eval_type
, but doesn't raise on errors.
It is used to evaluate a type annotation, which might already be
a proper type (in which case no action is required), or a forward reference string,
which needs to be resolved.
If _eval_type fails, we try some heuristics to import a missing module.
If that still fails, a warning is emitted and t
is returned as-is.