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