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 19import typing 20from typing import TYPE_CHECKING 21from typing import Any 22from typing import Literal 23from typing import _GenericAlias # type: ignore 24from typing import get_origin 25import warnings 26 27from . import extract 28from ._compat import UnionType 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 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 assert module.__dict__ is globalns 129 try: 130 _eval_type_checking_sections(module, set()) 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 else: 153 globalns[mod] = val 154 return safe_eval_type(t, globalns, localns, module, fullname) 155 156 157def _eval_type_checking_sections(module: types.ModuleType, seen: set) -> None: 158 """ 159 Evaluate all TYPE_CHECKING sections within a module. 160 161 The added complication here is that TYPE_CHECKING sections may import members from other modules' TYPE_CHECKING 162 sections. So we try to recursively execute those other modules' TYPE_CHECKING sections as well. 163 See https://github.com/mitmproxy/pdoc/issues/648 for a real world example. 164 """ 165 if module.__name__ in seen: 166 raise RecursionError(f"Recursion error when importing {module.__name__}.") 167 seen.add(module.__name__) 168 169 code = compile(type_checking_sections(module), "<string>", "exec") 170 while True: 171 try: 172 eval(code, module.__dict__, module.__dict__) 173 except ImportError as e: 174 if e.name is not None and (mod := sys.modules.get(e.name, None)): 175 _eval_type_checking_sections(mod, seen) 176 else: 177 raise 178 else: 179 break 180 181 182def _eval_type(t, globalns, localns, recursive_guard=frozenset()): 183 # Adapted from typing._eval_type. 184 # Added type coercion originally found in get_type_hints, but removed NoneType check because that was distracting. 185 # Added a special check for typing.Literal, whose literal strings would otherwise be evaluated. 186 187 if isinstance(t, str): 188 if sys.version_info < (3, 9): # pragma: no cover 189 t = t.strip("\"'") 190 t = typing.ForwardRef(t) 191 192 if get_origin(t) is Literal: 193 return t 194 195 if isinstance(t, typing.ForwardRef): 196 # inlined from 197 # https://github.com/python/cpython/blob/4f51fa9e2d3ea9316e674fb9a9f3e3112e83661c/Lib/typing.py#L684-L707 198 if t.__forward_arg__ in recursive_guard: # pragma: no cover 199 return t 200 if not t.__forward_evaluated__ or localns is not globalns: 201 if globalns is None and localns is None: # pragma: no cover 202 globalns = localns = {} 203 elif globalns is None: # pragma: no cover 204 globalns = localns 205 elif localns is None: # pragma: no cover 206 localns = globalns 207 __forward_module__ = getattr(t, "__forward_module__", None) 208 if __forward_module__ is not None: 209 globalns = getattr( 210 sys.modules.get(__forward_module__, None), "__dict__", globalns 211 ) 212 (type_,) = (eval(t.__forward_code__, globalns, localns),) 213 t.__forward_value__ = _eval_type( 214 type_, globalns, localns, recursive_guard | {t.__forward_arg__} 215 ) 216 t.__forward_evaluated__ = True 217 return t.__forward_value__ 218 219 # https://github.com/python/cpython/blob/main/Lib/typing.py#L333-L343 220 # fmt: off 221 # ✂ start ✂ 222 if isinstance(t, (_GenericAlias, GenericAlias, UnionType)): 223 ev_args = tuple(_eval_type(a, globalns, localns, recursive_guard) for a in t.__args__) 224 if ev_args == t.__args__: 225 return t 226 if isinstance(t, GenericAlias): 227 return GenericAlias(t.__origin__, ev_args) 228 if isinstance(t, UnionType): 229 return functools.reduce(operator.or_, ev_args) 230 else: 231 return t.copy_with(ev_args) 232 return t 233 # ✂ 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 if "unsupported operand type(s) for |" in str(e) and sys.version_info < (3, 10): 113 py_ver = ".".join(str(x) for x in sys.version_info[:3]) 114 warnings.warn( 115 f"Error parsing type annotation {t} for {fullname}: {e}. " 116 f"You are likely attempting to use Python 3.10 syntax (PEP 604 union types) with an older Python " 117 f"release. `X | Y`-style type annotations are invalid syntax on Python {py_ver}, which is what your " 118 f"pdoc instance is using. `from future import __annotations__` (PEP 563) postpones evaluation of " 119 f"annotations, which is why your program won't crash right away. However, pdoc needs to evaluate your " 120 f"type annotations and is unable to do so on Python {py_ver}. To fix this issue, either invoke pdoc " 121 f"from Python 3.10+, or switch to `typing.Union[]` syntax." 122 ) 123 else: 124 warnings.warn(f"Error parsing type annotation {t} for {fullname}: {e}") 125 return t 126 127 # Simple _eval_type has failed. We now execute all TYPE_CHECKING sections in the module and try again. 128 if module: 129 assert module.__dict__ is globalns 130 try: 131 _eval_type_checking_sections(module, set()) 132 except Exception as e: 133 warnings.warn( 134 f"Failed to run TYPE_CHECKING code while parsing {t} type annotation for {fullname}: {e}" 135 ) 136 try: 137 return _eval_type(t, globalns, None) 138 except (AttributeError, NameError): 139 pass # still not found 140 except Exception as e: 141 warnings.warn( 142 f"Error parsing type annotation {t} for {fullname} after evaluating TYPE_CHECKING blocks: {e}" 143 ) 144 return t 145 146 try: 147 val = extract.load_module(mod) 148 except Exception: 149 warnings.warn( 150 f"Error parsing type annotation {t} for {fullname}. Import of {mod} failed: {err}" 151 ) 152 return t 153 else: 154 globalns[mod] = val 155 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.