def _check_type_param(self, params: Dict[str, inspect.Parameter]) -> None: arg_index = 1 if self.func.is_instance_method else 0 for key, param in params.items(): self._already_checked_kwargs.append(key) self._assert_param_has_type_annotation(param=param) if param.default is inspect.Signature.empty: if self.func.should_have_kwargs: if key not in self.kwargs: raise PedanticTypeCheckException( f'{self.func.err}Parameter "{key}" is unfilled.') actual_value = self.kwargs[key] else: actual_value = self.args[arg_index] arg_index += 1 else: if key in self.kwargs: actual_value = self.kwargs[key] else: actual_value = param.default _assert_value_matches_type(value=actual_value, type_=param.annotation, err=self.func.err, type_vars=self.type_vars, key=key)
def __init__(self, func: Callable[..., Any]) -> None: self._func = func if not isinstance(func, (types.FunctionType, types.MethodType)): raise PedanticTypeCheckException(f'{self.full_name} should be a method or function.') self._full_arg_spec = inspect.getfullargspec(func) self._signature = inspect.signature(func) self._err = f'In function {func.__qualname__}:' + '\n' self._source: str = inspect.getsource(object=func) self._docstring = parse(func.__doc__)
def _set_and_check_return_types(self, expected_return_type: Any) -> Any: base_generic = _get_base_generic(cls=expected_return_type) if base_generic not in [Generator, Iterable, Iterator]: raise PedanticTypeCheckException( f'{self._err}Generator should have type annotation "typing.Generator[]", "typing.Iterator[]" or ' f'"typing.Iterable[]". Got "{expected_return_type}" instead.') result = _get_type_arguments(expected_return_type) if len(result) == 1: self._yield_type = result[0] elif len(result) == 3: self._yield_type = result[0] self._send_type = result[1] self._return_type = result[2] else: raise PedanticTypeCheckException( f'{self._err}Generator should have a type argument. Got: {result}' ) return result[0]
def decorate(cls: C) -> C: if not is_enabled(): return cls if issubclass(cls, enum.Enum): raise PedanticTypeCheckException( f'Enum "{cls}" cannot be decorated with "@pedantic_class". ' f'Enums are not supported yet.') if sys.version_info >= (3, 7): from dataclasses import is_dataclass if is_dataclass(obj=cls): raise PedanticTypeCheckException( f'Dataclass "{cls}" cannot be decorated with "@pedantic_class". ' f'Try to write "@dataclass" over "@pedantic_class".') for attr in cls.__dict__: attr_value = getattr(cls, attr) if isinstance(attr_value, (types.FunctionType, types.MethodType)): setattr(cls, attr, decorator(attr_value)) elif isinstance(attr_value, property): prop = attr_value wrapped_getter = _get_wrapped(prop=prop.fget, decorator=decorator) wrapped_setter = _get_wrapped(prop=prop.fset, decorator=decorator) wrapped_deleter = _get_wrapped(prop=prop.fdel, decorator=decorator) new_prop = property(fget=wrapped_getter, fset=wrapped_setter, fdel=wrapped_deleter) setattr(cls, attr, new_prop) _add_type_var_attr_and_method_to_class(cls=cls) return cls
def _assert_value_matches_type(value: Any, type_: Any, err: str, type_vars: Dict[TypeVar_, Any], key: Optional[str] = None, msg: Optional[str] = None ) -> None: if not _check_type(value=value, type_=type_, err=err, type_vars=type_vars): t = type(value) value = f'{key}={value}' if key is not None else str(value) if not msg: msg = f'{err}Type hint is incorrect: Argument {value} of type {t} does not match expected type {type_}.' raise PedanticTypeCheckException(msg)
def _check_types_return(self, result: Any) -> Union[None, GeneratorWrapper]: if self.func.signature.return_annotation is inspect.Signature.empty: raise PedanticTypeCheckException( f'{self.func.err}There should be a type hint for the return type (e.g. None if nothing is returned).' ) expected_result_type = self.func.annotations['return'] if self.func.is_generator: return GeneratorWrapper(wrapped=result, expected_type=expected_result_type, err_msg=self.func.err, type_vars=self.type_vars) msg = f'{self.func.err}Type hint of return value is incorrect: Expected type {expected_result_type} ' \ f'but {result} of type {type(result)} was the return value which does not match.' _assert_value_matches_type(value=result, type_=expected_result_type, err=self.func.err, type_vars=self.type_vars, msg=msg) return result
def _assert_param_has_type_annotation(self, param: inspect.Parameter): if param.annotation == inspect.Parameter.empty: raise PedanticTypeCheckException( f'{self.func.err}Parameter "{param.name}" should have a type hint.' )
def _check_type(value: Any, type_: Any, err: str, type_vars: Dict[TypeVar_, Any]) -> bool: """ >>> from typing import List, Union, Optional, Callable, Any >>> _check_type(5, int, '', {}) True >>> _check_type(5, float, '', {}) False >>> _check_type('hi', str, '', {}) True >>> _check_type(None, str, '', {}) False >>> _check_type(None, Any, '', {}) True >>> _check_type(None, None, '', {}) True >>> _check_type(5, Any, '', {}) True >>> _check_type(3.1415, float, '', {}) True >>> _check_type([1, 2, 3, 4], List[int], '', {}) True >>> _check_type([1, 2, 3.0, 4], List[int], '', {}) False >>> _check_type([1, 2, 3.0, 4], List[float], '', {}) False >>> _check_type([1, 2, 3.0, 4], List[Union[float, int]], '', {}) True >>> _check_type([[True, False], [False], [True], []], List[List[bool]], '', {}) True >>> _check_type([[True, False, 1], [False], [True], []], List[List[bool]], '', {}) False >>> _check_type(5, Union[int, float, bool], '', {}) True >>> _check_type(5.0, Union[int, float, bool], '', {}) True >>> _check_type(False, Union[int, float, bool], '', {}) True >>> _check_type('5', Union[int, float, bool], '', {}) False >>> def f(a: int, b: bool, c: str) -> float: pass >>> _check_type(f, Callable[[int, bool, str], float], '', {}) True >>> _check_type(None, Optional[List[Dict[str, float]]], '', {}) True >>> _check_type([{'a': 1.2, 'b': 3.4}], Optional[List[Dict[str, float]]], '', {}) True >>> _check_type([{'a': 1.2, 'b': 3}], Optional[List[Dict[str, float]]], '', {}) False >>> _check_type({'a': 1.2, 'b': 3.4}, Optional[List[Dict[str, float]]], '', {}) False >>> _check_type([{'a': 1.2, 7: 3.4}], Optional[List[Dict[str, float]]], '', {}) False >>> class MyClass: pass >>> _check_type(MyClass(), 'MyClass', '', {}) True >>> _check_type(MyClass(), 'MyClas', '', {}) False >>> _check_type([1, 2, 3], list, '', {}) Traceback (most recent call last): ... pedantic.exceptions.PedanticTypeCheckException: Use "List[]" instead of "list" as type hint. >>> _check_type((1, 2, 3), tuple, '', {}) Traceback (most recent call last): ... pedantic.exceptions.PedanticTypeCheckException: Use "Tuple[]" instead of "tuple" as type hint. >>> _check_type({1: 1.0, 2: 2.0, 3: 3.0}, dict, '', {}) Traceback (most recent call last): ... pedantic.exceptions.PedanticTypeCheckException: Use "Dict[]" instead of "dict" as type hint. """ if type_ is None: return value == type_ elif isinstance(type_, str): class_name = value.__class__.__name__ base_class_name = value.__class__.__base__.__name__ return class_name == type_ or base_class_name == type_ if isinstance(type_, tuple): raise PedanticTypeCheckException(f'{err}Use "Tuple[]" instead of "{type_}" as type hint.') if isinstance(type_, list): raise PedanticTypeCheckException(f'{err}Use "List[]" instead of "{type_}" as type hint.') if type_ is tuple: raise PedanticTypeCheckException(f'{err}Use "Tuple[]" instead of "tuple" as type hint.') if type_ is list: raise PedanticTypeCheckException(f'{err}Use "List[]" instead of "list" as type hint.') if type_ is dict: raise PedanticTypeCheckException(f'{err}Use "Dict[]" instead of "dict" as type hint.') if type_ is set: raise PedanticTypeCheckException(f'{err}Use "Set[]" instead of "set" as type hint.') if type_ is frozenset: raise PedanticTypeCheckException(f'{err}Use "FrozenSet[]" instead of "frozenset" as type hint.') if type_ is type: raise PedanticTypeCheckException(f'{err}Use "Type[]" instead of "type" as type hint.') try: return _is_instance(obj=value, type_=type_, type_vars=type_vars) except PedanticTypeCheckException as ex: raise PedanticTypeCheckException(f'{err} {ex}') except PedanticTypeVarMismatchException as ex: raise PedanticTypeVarMismatchException(f'{err} {ex}') except (AttributeError, Exception) as ex: raise PedanticTypeCheckException( f'{err}An error occurred during type hint checking. Value: {value} Annotation: ' f'{type_} Mostly this is caused by an incorrect type annotation. Details: {ex} ')
def _is_instance(obj: Any, type_: Any, type_vars: Dict[TypeVar_, Any]) -> bool: if not _has_required_type_arguments(type_): raise PedanticTypeCheckException( f'The type annotation "{type_}" misses some type arguments e.g. ' f'"typing.Tuple[Any, ...]" or "typing.Callable[..., str]".') if type_.__module__ == 'typing': if _is_generic(type_): origin = _get_base_generic(type_) else: origin = type_ name = _get_name(origin) if name in _SPECIAL_INSTANCE_CHECKERS: validator = _SPECIAL_INSTANCE_CHECKERS[name] return validator(obj, type_, type_vars) if type_ == typing.BinaryIO: return isinstance(obj, (BytesIO, BufferedWriter)) elif type_ == typing.TextIO: return isinstance(obj, (StringIO, TextIOWrapper)) if _is_generic(type_): python_type = type_.__origin__ if not isinstance(obj, python_type): return False base = _get_base_generic(type_) type_args = _get_type_arguments(cls=type_) if base in _ORIGIN_TYPE_CHECKERS: validator = _ORIGIN_TYPE_CHECKERS[base] return validator(obj, type_args, type_vars) assert base.__base__ == typing.Generic, f'Unknown base: {base}' return isinstance(obj, base) if isinstance(type_, TypeVar): constraints = type_.__constraints__ if len(constraints) > 0 and type(obj) not in constraints: return False if _is_forward_ref(type_=type_.__bound__): return type(obj).__name__ == type_.__bound__.__forward_arg__ if type_.__bound__ is not None and not isinstance(obj, type_.__bound__): return False if type_ in type_vars: other = type_vars[type_] if type_.__contravariant__: if not _is_subtype(sub_type=other, super_type=obj.__class__): raise PedanticTypeVarMismatchException( f'For TypeVar {type_} exists a type conflict: value {obj} has type {type(obj)} but TypeVar {type_} ' f'was previously matched to type {other}') else: if not _is_instance(obj=obj, type_=other, type_vars=type_vars): raise PedanticTypeVarMismatchException( f'For TypeVar {type_} exists a type conflict: value {obj} has type {type(obj)} but TypeVar {type_} ' f'was previously matched to type {other}') type_vars[type_] = type(obj) return True if _is_forward_ref(type_=type_): return type(obj).__name__ == type_.__forward_arg__ if _is_type_new_type(type_): return isinstance(obj, type_.__supertype__) if hasattr(obj, '_asdict'): if hasattr(type_, '_field_types'): field_types = type_._field_types elif hasattr(type_, '__annotations__'): field_types = type_.__annotations__ else: return False if not obj._asdict().keys() == field_types.keys(): return False return all([_is_instance(obj=obj._asdict()[k], type_=v, type_vars=type_vars) for k, v in field_types.items()]) return isinstance(obj, type_)