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