File: doc_types.py

package info (click to toggle)
python-pdoc 16.0.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,080 kB
  • sloc: python: 5,260; javascript: 1,156; makefile: 18; sh: 3
file content (217 lines) | stat: -rw-r--r-- 7,201 bytes parent folder | download
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 ✂