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