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}"
)
|