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""" 8 9from __future__ import annotations 10 11import functools 12import inspect 13import operator 14import sys 15import types 16from types import BuiltinFunctionType 17from types import GenericAlias 18from types import ModuleType 19from types import UnionType 20import typing 21from typing import TYPE_CHECKING 22from typing import Any 23from typing import Literal 24from typing import _GenericAlias # type: ignore 25from typing import get_origin 26import warnings 27 28from . import extract 29from .doc_ast import type_checking_sections 30 31if TYPE_CHECKING: 32 33 class empty: 34 pass 35 36 37empty: type = inspect.Signature.empty # type: ignore # noqa 38""" 39A "special" object signaling the absence of a type annotation. 40This is useful to distinguish it from an actual annotation with `None`. 41This value is an alias of `inspect.Signature.empty`. 42""" 43 44# adapted from 45# https://github.com/python/cpython/blob/9feae41c4f04ca27fd2c865807a5caeb50bf4fc4/Lib/inspect.py#L1740-L1747 46# ✂ start ✂ 47_WrapperDescriptor = type(type.__call__) 48_MethodWrapper = type(all.__call__) # type: ignore 49_ClassMethodWrapper = type(int.__dict__["from_bytes"]) 50 51NonUserDefinedCallables = ( 52 _WrapperDescriptor, 53 _MethodWrapper, 54 _ClassMethodWrapper, 55 BuiltinFunctionType, 56) 57 58 59# ✂ end ✂ 60 61 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 83 84 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 warnings.warn(f"Error parsing type annotation {t} for {fullname}: {e}") 112 return t 113 114 # Simple _eval_type has failed. We now execute all TYPE_CHECKING sections in the module and try again. 115 if module: 116 assert module.__dict__ is globalns 117 try: 118 _eval_type_checking_sections(module, set()) 119 except Exception as e: 120 warnings.warn( 121 f"Failed to run TYPE_CHECKING code while parsing {t} type annotation for {fullname}: {e}" 122 ) 123 try: 124 return _eval_type(t, globalns, None) 125 except (AttributeError, NameError): 126 pass # still not found 127 except Exception as e: 128 warnings.warn( 129 f"Error parsing type annotation {t} for {fullname} after evaluating TYPE_CHECKING blocks: {e}" 130 ) 131 return t 132 133 try: 134 val = extract.load_module(mod) 135 except Exception: 136 warnings.warn( 137 f"Error parsing type annotation {t} for {fullname}. Import of {mod} failed: {err}" 138 ) 139 return t 140 else: 141 globalns[mod] = val 142 return safe_eval_type(t, globalns, localns, module, fullname) 143 144 145def _eval_type_checking_sections(module: types.ModuleType, seen: set) -> None: 146 """ 147 Evaluate all TYPE_CHECKING sections within a module. 148 149 The added complication here is that TYPE_CHECKING sections may import members from other modules' TYPE_CHECKING 150 sections. So we try to recursively execute those other modules' TYPE_CHECKING sections as well. 151 See https://github.com/mitmproxy/pdoc/issues/648 for a real world example. 152 """ 153 if module.__name__ in seen: 154 raise RecursionError(f"Recursion error when importing {module.__name__}.") 155 seen.add(module.__name__) 156 157 code = compile(type_checking_sections(module), "<string>", "exec") 158 while True: 159 try: 160 eval(code, module.__dict__, module.__dict__) 161 except ImportError as e: 162 if e.name is not None and (mod := sys.modules.get(e.name, None)): 163 _eval_type_checking_sections(mod, seen) 164 else: 165 raise 166 else: 167 break 168 169 170def _eval_type(t, globalns, localns, recursive_guard=frozenset()): 171 # Adapted from typing._eval_type. 172 # Added type coercion originally found in get_type_hints, but removed NoneType check because that was distracting. 173 # Added a special check for typing.Literal, whose literal strings would otherwise be evaluated. 174 175 if isinstance(t, str): 176 t = typing.ForwardRef(t) 177 178 if get_origin(t) is Literal: 179 return t 180 181 if isinstance(t, typing.ForwardRef): 182 # inlined from 183 # https://github.com/python/cpython/blob/4f51fa9e2d3ea9316e674fb9a9f3e3112e83661c/Lib/typing.py#L684-L707 184 if t.__forward_arg__ in recursive_guard: # pragma: no cover 185 return t 186 187 if globalns is None and localns is None: # pragma: no cover 188 globalns = localns = {} 189 elif globalns is None: # pragma: no cover 190 globalns = localns 191 elif localns is None: # pragma: no cover 192 localns = globalns 193 __forward_module__ = getattr(t, "__forward_module__", None) 194 if __forward_module__ is not None: 195 globalns = getattr( 196 sys.modules.get(__forward_module__, None), "__dict__", globalns 197 ) 198 (type_,) = (eval(t.__forward_code__, globalns, localns),) 199 return _eval_type( 200 type_, globalns, localns, recursive_guard | {t.__forward_arg__} 201 ) 202 203 # https://github.com/python/cpython/blob/main/Lib/typing.py#L333-L343 204 # fmt: off 205 # ✂ start ✂ 206 if isinstance(t, (_GenericAlias, GenericAlias, UnionType)): 207 ev_args = tuple(_eval_type(a, globalns, localns, recursive_guard) for a in t.__args__) 208 if ev_args == t.__args__: 209 return t 210 if isinstance(t, GenericAlias): 211 return GenericAlias(t.__origin__, ev_args) 212 if isinstance(t, UnionType): 213 return functools.reduce(operator.or_, ev_args) 214 else: 215 return t.copy_with(ev_args) 216 return t 217 # ✂ 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
.
63def resolve_annotations( 64 annotations: dict[str, Any], 65 module: ModuleType | None, 66 localns: dict[str, Any] | None, 67 fullname: str, 68) -> dict[str, Any]: 69 """ 70 Given an `annotations` dictionary with type annotations (for example, `cls.__annotations__`), 71 this function tries to resolve all types using `pdoc.doc_types.safe_eval_type`. 72 73 Returns: A dictionary with the evaluated types. 74 """ 75 globalns = getattr(module, "__dict__", {}) 76 77 resolved = {} 78 for name, value in annotations.items(): 79 resolved[name] = safe_eval_type( 80 value, globalns, localns, module, f"{fullname}.{name}" 81 ) 82 83 return resolved
Given an annotations
dictionary with type annotations (for example, cls.__annotations__
),
this function tries to resolve all types using pdoc.doc_types.safe_eval_type
.
Returns: A dictionary with the evaluated types.
86def safe_eval_type( 87 t: Any, 88 globalns: dict[str, Any], 89 localns: dict[str, Any] | None, 90 module: types.ModuleType | None, 91 fullname: str, 92) -> Any: 93 """ 94 This method wraps `typing._eval_type`, but doesn't raise on errors. 95 It is used to evaluate a type annotation, which might already be 96 a proper type (in which case no action is required), or a forward reference string, 97 which needs to be resolved. 98 99 If _eval_type fails, we try some heuristics to import a missing module. 100 If that still fails, a warning is emitted and `t` is returned as-is. 101 """ 102 try: 103 return _eval_type(t, globalns, localns) 104 except AttributeError as e: 105 err = str(e) 106 _, obj, _, attr, _ = err.split("'") 107 mod = f"{obj}.{attr}" 108 except NameError as e: 109 err = str(e) 110 _, mod, _ = err.split("'") 111 except Exception as e: 112 warnings.warn(f"Error parsing type annotation {t} for {fullname}: {e}") 113 return t 114 115 # Simple _eval_type has failed. We now execute all TYPE_CHECKING sections in the module and try again. 116 if module: 117 assert module.__dict__ is globalns 118 try: 119 _eval_type_checking_sections(module, set()) 120 except Exception as e: 121 warnings.warn( 122 f"Failed to run TYPE_CHECKING code while parsing {t} type annotation for {fullname}: {e}" 123 ) 124 try: 125 return _eval_type(t, globalns, None) 126 except (AttributeError, NameError): 127 pass # still not found 128 except Exception as e: 129 warnings.warn( 130 f"Error parsing type annotation {t} for {fullname} after evaluating TYPE_CHECKING blocks: {e}" 131 ) 132 return t 133 134 try: 135 val = extract.load_module(mod) 136 except Exception: 137 warnings.warn( 138 f"Error parsing type annotation {t} for {fullname}. Import of {mod} failed: {err}" 139 ) 140 return t 141 else: 142 globalns[mod] = val 143 return safe_eval_type(t, 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.