Beispiel #1
0
def is_valid_type(arg, message: str, is_argument: bool = True):
    """
    exposes the _type_check function from typing module that does basic validations of the type
    """
    if NEW_TYPING:
        return _type_check(arg, message, is_argument)
    if is_classvar(arg) and not is_argument:
        return arg
    return _type_check(arg, message)
Beispiel #2
0
    def _init_parametric_base(cls) -> None:
        """Initialize a direct subclass of ParametricType"""

        # Direct subclasses of ParametricType must declare
        # ClassVar attributes corresponding to the Generic type vars.
        # For example:
        #     class P(ParametricType, Generic[T, V]):
        #         t: ClassVar[Type[T]]
        #         v: ClassVar[Type[V]]

        params = getattr(cls, '__parameters__', None)

        if not params:
            raise TypeError(f'{cls} must be declared as Generic')

        mod = sys.modules[cls.__module__]
        annos = get_type_hints(cls, mod.__dict__)
        param_map = {}

        for attr, t in annos.items():
            if not typing_inspect.is_classvar(t):
                continue

            args = typing_inspect.get_args(t)
            # ClassVar constructor should have the check, but be extra safe.
            assert len(args) == 1

            arg = args[0]
            if typing_inspect.get_origin(arg) != type:
                continue

            arg_args = typing_inspect.get_args(arg)
            # Likewise, rely on Type checking its stuff in the constructor
            assert len(arg_args) == 1

            if not typing_inspect.is_typevar(arg_args[0]):
                continue

            if arg_args[0] in params:
                param_map[arg_args[0]] = attr

        for param in params:
            if param not in param_map:
                raise TypeError(f'{cls.__name__}: missing ClassVar for'
                                f' generic parameter {param}')

        cls._type_param_map = param_map
    def get(cls, type_or_hint, *, is_argument: bool = False) -> "TypeChecker":
        # This ensures the validity of the type passed (see typing documentation for info)
        type_or_hint = is_valid_type(type_or_hint, "Invalid type.",
                                     is_argument)

        if type_or_hint is Any:
            return AnyTypeChecker()

        if is_type(type_or_hint):
            return TypeTypeChecker.make(type_or_hint, is_argument)

        if is_literal_type(type_or_hint):
            return LiteralTypeChecker.make(type_or_hint, is_argument)

        if is_generic_type(type_or_hint):
            origin = get_origin(type_or_hint)
            if issubclass(origin, MappingCol):
                return MappingTypeChecker.make(type_or_hint, is_argument)

            if issubclass(origin, Collection):
                return CollectionTypeChecker.make(type_or_hint, is_argument)

            # CONSIDER: how to cater for exhaustible generators?
            if issubclass(origin, Iterable):
                raise NotImplementedError(
                    "No type-checker is setup for iterables that exhaust.")

            return GenericTypeChecker.make(type_or_hint, is_argument)

        if is_tuple_type(type_or_hint):
            return TupleTypeChecker.make(type_or_hint, is_argument)

        if is_callable_type(type_or_hint):
            return CallableTypeChecker.make(type_or_hint, is_argument)

        if isclass(type_or_hint):
            if is_typed_dict(type_or_hint):
                return TypedDictChecker.make(type_or_hint, is_argument)
            return ConcreteTypeChecker.make(type_or_hint, is_argument)

        if is_union_type(type_or_hint):
            return UnionTypeChecker.make(type_or_hint, is_argument)

        if is_typevar(type_or_hint):
            bound_type = get_bound(type_or_hint)
            if bound_type:
                return cls.get(bound_type)
            constraints = get_constraints(type_or_hint)
            if constraints:
                union_type_checkers = tuple(
                    cls.get(type_) for type_ in constraints)
                return UnionTypeChecker(Union.__getitem__(constraints),
                                        union_type_checkers)
            else:
                return AnyTypeChecker()

        if is_new_type(type_or_hint):
            super_type = getattr(type_or_hint, "__supertype__", None)
            if super_type is None:
                raise TypeError(
                    f"No supertype for NewType: {type_or_hint}. This is not allowed."
                )
            return cls.get(super_type)

        if is_forward_ref(type_or_hint):
            return ForwardTypeChecker.make(type_or_hint,
                                           is_argument=is_argument)

        if is_classvar(type_or_hint):
            var_type = get_args(type_or_hint, evaluate=True)[0]
            return cls.get(var_type)

        raise NotImplementedError(
            f"No {TypeChecker.__qualname__} is available for type or hint: '{type_or_hint}'"
        )
Beispiel #4
0
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 is_classvar(expected_type):
        # `ClassVar` (no subscript) is implicitly `ClassVar[Any]`
        if hasattr(expected_type, "__type__"):  # py36
            expected_type = expected_type.__type__ or Any
        else:  # py37+
            classvar_args = get_args(expected_type)
            expected_type = (classvar_args[0] or Any) if classvar_args else Any

    if is_typevar(expected_type):
        # 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 is_union_type(expected_type):
        return any(
            is_value_of_type(value, subtype) for subtype in expected_type.__args__
        )

    elif isinstance(expected_origin_type, type(Literal)):
        if hasattr(expected_type, "__values__"):  # py36
            literal_values = expected_type.__values__
        else:  # py37+
            literal_values = get_args(expected_type, evaluate=True)
        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, evaluate=True)
        if len(type_args) == 0:
            # `Tuple` (no subscript) is implicitly `Tuple[Any, ...]`
            return True

        if type_args is None:
            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, evaluate=True)
        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, evaluate=True)
        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}"
        )
Beispiel #5
0
def _type_from_runtime(val, ctx):
    if isinstance(val, str):
        return _eval_forward_ref(val, ctx)
    elif isinstance(val, tuple):
        # This happens under some Python versions for types
        # nested in tuples, e.g. on 3.6:
        # > typing_inspect.get_args(Union[Set[int], List[str]])
        # ((typing.Set, int), (typing.List, str))
        origin = val[0]
        if len(val) == 2:
            args = (val[1], )
        else:
            args = val[1:]
        return _value_of_origin_args(origin, args, val, ctx)
    elif typing_inspect.is_literal_type(val):
        args = typing_inspect.get_args(val)
        if len(args) == 0:
            return KnownValue(args[0])
        else:
            return unite_values(*[KnownValue(arg) for arg in args])
    elif typing_inspect.is_union_type(val):
        args = typing_inspect.get_args(val)
        return unite_values(*[_type_from_runtime(arg, ctx) for arg in args])
    elif typing_inspect.is_tuple_type(val):
        args = typing_inspect.get_args(val)
        if not args:
            return TypedValue(tuple)
        elif len(args) == 2 and args[1] is Ellipsis:
            return GenericValue(tuple, [_type_from_runtime(args[0], ctx)])
        else:
            args_vals = [_type_from_runtime(arg, ctx) for arg in args]
            return SequenceIncompleteValue(tuple, args_vals)
    elif is_instance_of_typing_name(val, "_TypedDictMeta"):
        return TypedDictValue({
            key: _type_from_runtime(value, ctx)
            for key, value in val.__annotations__.items()
        })
    elif typing_inspect.is_callable_type(val):
        return TypedValue(Callable)
    elif typing_inspect.is_generic_type(val):
        origin = typing_inspect.get_origin(val)
        args = typing_inspect.get_args(val)
        return _value_of_origin_args(origin, args, val, ctx)
    elif GenericAlias is not None and isinstance(val, GenericAlias):
        origin = get_origin(val)
        args = get_args(val)
        return GenericValue(origin,
                            [_type_from_runtime(arg, ctx) for arg in args])
    elif isinstance(val, type):
        if val is type(None):
            return KnownValue(None)
        return TypedValue(val)
    elif val is None:
        return KnownValue(None)
    elif is_typing_name(val, "NoReturn"):
        return NO_RETURN_VALUE
    elif val is typing.Any:
        return UNRESOLVED_VALUE
    elif hasattr(val, "__supertype__"):
        if isinstance(val.__supertype__, type):
            # NewType
            return NewTypeValue(val)
        elif typing_inspect.is_tuple_type(val.__supertype__):
            # TODO figure out how to make NewTypes over tuples work
            return UNRESOLVED_VALUE
        else:
            ctx.show_error("Invalid NewType %s" % (val, ))
            return UNRESOLVED_VALUE
    elif typing_inspect.is_typevar(val):
        # TypeVar; not supported yet
        return UNRESOLVED_VALUE
    elif typing_inspect.is_classvar(val):
        return UNRESOLVED_VALUE
    elif is_instance_of_typing_name(
            val, "_ForwardRef") or is_instance_of_typing_name(
                val, "ForwardRef"):
        # This has issues because the forward ref may be defined in a different file, in
        # which case we don't know which names are valid in it.
        with qcore.override(ctx, "suppress_undefined_name", True):
            return UNRESOLVED_VALUE
    elif val is Ellipsis:
        # valid in Callable[..., ]
        return UNRESOLVED_VALUE
    elif is_instance_of_typing_name(val, "_TypeAlias"):
        # typing.Pattern and Match, which are not normal generic types for some reason
        return GenericValue(val.impl_type,
                            [_type_from_runtime(val.type_var, ctx)])
    else:
        origin = get_origin(val)
        if origin is not None:
            return TypedValue(origin)
        ctx.show_error("Invalid type annotation %s" % (val, ))
        return UNRESOLVED_VALUE