Edit on GitHub

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 ✂
empty: type

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.

NonUserDefinedCallables = (<class 'wrapper_descriptor'>, <class 'method-wrapper'>, <class 'classmethod_descriptor'>, <class 'builtin_function_or_method'>)
def resolve_annotations( annotations: dict[str, typing.Any], module: module | None, localns: dict[str, typing.Any] | None, fullname: str) -> dict[str, typing.Any]:
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.

def safe_eval_type( t: Any, globalns: dict[str, typing.Any], localns: dict[str, typing.Any] | None, module: module | None, fullname: str) -> Any:
 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.