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

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.

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

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:
 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        try:
129            code = compile(type_checking_sections(module), "<string>", "exec")
130            eval(code, globalns, globalns)
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    return safe_eval_type(t, {mod: val, **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.