File: typing_compat.py

package info (click to toggle)
dataclass-wizard 0.39.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,112 kB
  • sloc: python: 19,560; makefile: 126; javascript: 23
file content (245 lines) | stat: -rw-r--r-- 6,565 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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
"""
Utility module for checking generic types provided by the `typing` library.
"""

__all__ = [
    'is_literal',
    'is_union',
    'get_origin',
    'get_origin_v2',
    'is_typed_dict_type_qualifier',
    'get_args',
    'get_keys_for_typed_dict',
    'is_typed_dict',
    'is_generic',
    'is_annotated',
    'eval_forward_ref',
    'eval_forward_ref_if_needed',
]

import functools
import sys
import typing
# noinspection PyUnresolvedReferences,PyProtectedMember
from typing import Literal, Union, _AnnotatedAlias

from .string_conv import repl_or_with_union
from ..constants import PY310_OR_ABOVE, PY313_OR_ABOVE
from ..type_def import (FREF,
                        PyRequired,
                        PyNotRequired,
                        PyReadOnly,
                        PyForwardRef)


_TYPED_DICT_TYPE_QUALIFIERS = frozenset(
    {PyRequired, PyNotRequired, PyReadOnly}
)


def get_keys_for_typed_dict(cls):
    """
    Given a :class:`TypedDict` sub-class, returns a pair of
    (required_keys, optional_keys)
    """
    return cls.__required_keys__, cls.__optional_keys__


def _is_annotated(cls):
    return isinstance(cls, _AnnotatedAlias)


# TODO Remove
def is_literal(cls) -> bool:
    try:
        return cls.__origin__ is Literal
    except AttributeError:
        return False

# Ref:
#   https://typing.readthedocs.io/en/latest/spec/typeddict.html#required-and-notrequired
#   https://typing.readthedocs.io/en/latest/spec/glossary.html#term-type-qualifier
def is_typed_dict_type_qualifier(cls) -> bool:
    return cls in _TYPED_DICT_TYPE_QUALIFIERS


# Ref:
#   https://github.com/python/typing/blob/master/typing_extensions/src_py3/typing_extensions.py#L2111
if PY310_OR_ABOVE:  # pragma: no cover
    from types import GenericAlias, UnionType
    _get_args = typing.get_args

    _BASE_GENERIC_TYPES = (
        typing._GenericAlias,
        typing._SpecialForm,
        GenericAlias,
        UnionType,
    )

    _UNION_TYPES = frozenset({
        UnionType,
        Union,
    })

    _TYPING_LOCALS = None

    def _process_forward_annotation(base_type):
        return PyForwardRef(base_type, is_argument=False)

    def is_union(cls) -> bool:
        return cls in _UNION_TYPES

    def get_origin_v2(cls):
        if type(cls) is UnionType:
            return UnionType

        return getattr(cls, '__origin__', cls)

    def _get_origin(cls, raise_=False):
        if isinstance(cls, UnionType):
            return Union

        try:
            return cls.__origin__
        except AttributeError:
            if raise_:
                raise
            return cls

else:  # pragma: no cover
    from typing_extensions import get_args as _get_args

    _BASE_GENERIC_TYPES = (
        typing._GenericAlias,
        typing._SpecialForm,
    )

    # PEP 585 is introduced in Python 3.9
    # PEP 604 (Allows writing union types as `X | Y`) is introduced
    #   in Python 3.10
    _TYPING_LOCALS = {'Union': Union}

    def _process_forward_annotation(base_type):
        return PyForwardRef(
            repl_or_with_union(base_type), is_argument=False)

    def is_union(cls) -> bool:
        return cls is Union

    def get_origin_v2(cls):
        return getattr(cls, '__origin__', cls)

    def _get_origin(cls, raise_=False):
        try:
            return cls.__origin__
        except AttributeError:
            if raise_:
                raise
            return cls


try:
    # noinspection PyProtectedMember,PyUnresolvedReferences
    from typing_extensions import _TYPEDDICT_TYPES

except ImportError:
    from typing import is_typeddict as is_typed_dict

else:
    def is_typed_dict(cls: type) -> bool:
        """
        Checks if `cls` is a sub-class of ``TypedDict``
        """
        return isinstance(cls, _TYPEDDICT_TYPES)


def is_generic(cls):
    """
    Detects any kind of generic, for example `List` or `List[int]`. This
    includes "special" types like Union, Any ,and Tuple - anything that's
    subscriptable, basically.

    https://stackoverflow.com/a/52664522/10237506
    """
    return isinstance(cls, _BASE_GENERIC_TYPES)


get_args = _get_args
get_args.__doc__ = """
Get type arguments with all substitutions performed.

For unions, basic simplifications used by Union constructor are performed.
Examples::
    get_args(Dict[str, int]) == (str, int)
    get_args(int) == ()
    get_args(Union[int, Union[T, int], str][int]) == (int, str)
    get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])
    get_args(Callable[[], T][int]) == ([], int)\
"""

# TODO refactor to use `typing.get_origin` when time permits.
get_origin = _get_origin
get_origin.__doc__ = """
Get the un-subscripted value of a type. If we're unable to retrieve this
value, return type `cls` if `raise_` is false.

This supports generic types, Callable, Tuple, Union, Literal, Final and
ClassVar. Return None for unsupported types.

Examples::

    get_origin(Literal[42]) is Literal
    get_origin(int) is int
    get_origin(ClassVar[int]) is ClassVar
    get_origin(Generic) is Generic
    get_origin(Generic[T]) is Generic
    get_origin(Union[T, int]) is Union
    get_origin(List[Tuple[T, T]][int]) == list

:raise AttributeError: When the `raise_` flag is enabled, and we are
  unable to retrieve the un-subscripted value.\
"""

is_annotated = _is_annotated
is_annotated.__doc__ = """Detects a :class:`typing.Annotated` class."""


if PY313_OR_ABOVE:
    # noinspection PyProtectedMember,PyUnresolvedReferences
    _eval_type = functools.partial(typing._eval_type, type_params=())
else:
    # noinspection PyProtectedMember,PyUnresolvedReferences
    _eval_type = typing._eval_type


def eval_forward_ref(base_type: FREF,
                     cls: type):
    """
    Evaluate a forward reference using the class globals, and return the
    underlying type reference.
    """

    if isinstance(base_type, str):
        base_type = _process_forward_annotation(base_type)

    # Evaluate the ForwardRef here
    base_globals = sys.modules[cls.__module__].__dict__

    return _eval_type(base_type, base_globals, _TYPING_LOCALS)


_ForwardRefTypes = frozenset(FREF.__constraints__)


def eval_forward_ref_if_needed(base_type: Union[type, FREF],
                               base_cls: type):
    """
    If needed, evaluate a forward reference using the class globals, and
    return the underlying type reference.
    """

    if type(base_type) in _ForwardRefTypes:
        # Evaluate the forward reference here.
        base_type = eval_forward_ref(base_type, base_cls)

    return base_type