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, 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 ✂
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, fullname: str) -> dict[str, typing.Any]:
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.

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