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
 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 ✂
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, 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, 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        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.