File: _type_enforce.py

package info (click to toggle)
python-libcst 1.4.0-1.2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 5,928 kB
  • sloc: python: 76,235; makefile: 10; sh: 2
file content (169 lines) | stat: -rw-r--r-- 5,891 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
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

from typing import (
    Any,
    ClassVar,
    ForwardRef,
    get_args,
    get_origin,
    Iterable,
    Literal,
    Mapping,
    MutableMapping,
    MutableSequence,
    Tuple,
    TypeVar,
    Union,
)


def is_value_of_type(  # noqa: C901 "too complex"
    # pyre-fixme[2]: Parameter annotation cannot be `Any`.
    value: Any,
    # pyre-fixme[2]: Parameter annotation cannot be `Any`.
    expected_type: Any,
    invariant_check: bool = False,
) -> bool:
    """
    This method attempts to verify a given value is of a given type. If the type is
    not supported, it returns True but throws an exception in tests.

    It is similar to typeguard / enforce pypi modules, but neither of those have
    permissive options for types they do not support.

    Supported types for now:
    - List/Set/Iterable
    - Dict/Mapping
    - base types (str, int, etc)
    - Literal
    - Unions
    - Tuples
    - Concrete Classes
    - ClassVar

    Not supported:
    - Callables, which will likely not be used in XHP anyways
    - Generics, Type Vars (treated as Any)
    - Generators
    - Forward Refs -- use `typing.get_type_hints` to resolve these
    - Type[...]
    """
    if expected_type is ClassVar or get_origin(expected_type) is ClassVar:
        classvar_args = get_args(expected_type)
        expected_type = (classvar_args[0] or Any) if classvar_args else Any

    if type(expected_type) is TypeVar:
        # treat this the same as Any
        # TODO: evaluate bounds
        return True

    expected_origin_type = get_origin(expected_type) or expected_type

    if expected_origin_type == Any:
        return True

    elif expected_type is Union or get_origin(expected_type) is Union:
        return any(
            is_value_of_type(value, subtype) for subtype in expected_type.__args__
        )

    elif isinstance(expected_origin_type, type(Literal)):
        literal_values = get_args(expected_type)
        return any(value == literal for literal in literal_values)

    elif isinstance(expected_origin_type, ForwardRef):
        # not much we can do here for now, lets just return :(
        return True

    # Handle `Tuple[A, B, C]`.
    # We don't want to include Tuple subclasses, like NamedTuple, because they're
    # unlikely to behave similarly.
    elif expected_origin_type in [Tuple, tuple]:  # py36 uses Tuple, py37+ uses tuple
        if not isinstance(value, tuple):
            return False

        type_args = get_args(expected_type)
        if len(type_args) == 0:
            # `Tuple` (no subscript) is implicitly `Tuple[Any, ...]`
            return True

        if len(value) != len(type_args):
            return False
        # TODO: Handle `Tuple[T, ...]` like `Iterable[T]`
        for subvalue, subtype in zip(value, type_args):
            if not is_value_of_type(subvalue, subtype):
                return False
            return True

    elif issubclass(expected_origin_type, Mapping):
        # We're expecting *some* kind of Mapping, but we also want to make sure it's
        # the correct Mapping subtype. That means we want {a: b, c: d} to match Mapping,
        # MutableMapping, and Dict, but we don't want MappingProxyType({a: b, c: d}) to
        # match MutableMapping or Dict.
        if not issubclass(type(value), expected_origin_type):
            return False

        type_args = get_args(expected_type)
        if len(type_args) == 0:
            # `Mapping` (no subscript) is implicitly `Mapping[Any, Any]`.
            return True

        invariant_check = issubclass(expected_origin_type, MutableMapping)

        for subkey, subvalue in value.items():
            if not is_value_of_type(
                subkey,
                type_args[0],
                # key type is always invariant
                invariant_check=True,
            ):
                return False
            if not is_value_of_type(
                subvalue, type_args[1], invariant_check=invariant_check
            ):
                return False
        return True

    # While this does technically work fine for str and bytes (they are iterables), it's
    # better to use the default isinstance behavior for them.
    #
    # Similarly, tuple subclasses tend to have pretty different behavior, and we should
    # fall back to the default check.
    elif issubclass(expected_origin_type, Iterable) and not issubclass(
        expected_origin_type,
        (str, bytes, tuple),
    ):
        # We know this thing is *some* kind of Iterable, but we want to
        # allow subclasses. That means we want [1,2,3] to match both
        # List[int] and Iterable[int], but we do NOT want that
        # to match Set[int].
        if not issubclass(type(value), expected_origin_type):
            return False

        type_args = get_args(expected_type)
        if len(type_args) == 0:
            # `Iterable` (no subscript) is implicitly `Iterable[Any]`.
            return True

        # We invariant check if its a mutable sequence
        invariant_check = issubclass(expected_origin_type, MutableSequence)
        return all(
            is_value_of_type(subvalue, type_args[0], invariant_check=invariant_check)
            for subvalue in value
        )

    try:
        if not invariant_check:
            if expected_type is float:
                return isinstance(value, (int, float))
            else:
                return isinstance(value, expected_type)
        return type(value) is expected_type
    except Exception as e:
        raise NotImplementedError(
            f"the value {value!r} was compared to type {expected_type!r} "
            + f"but support for that has not been implemented yet! Exception: {e!r}"
        )