From a3c9b82f77d39ce4fc448e690e199485e9ea4fc4 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 6 Jul 2021 14:00:16 +0300 Subject: [PATCH 1/4] WIP --- classes/_typeclass.py | 30 +++++- classes/contrib/mypy/features/typeclass.py | 18 ++-- .../mypy/validation/validate_instance_args.py | 96 +++++++++++++++++++ .../mypy/validation/validate_runtime.py | 83 +++++++--------- 4 files changed, 170 insertions(+), 57 deletions(-) create mode 100644 classes/contrib/mypy/validation/validate_instance_args.py diff --git a/classes/_typeclass.py b/classes/_typeclass.py index b48273a..f40d4d9 100644 --- a/classes/_typeclass.py +++ b/classes/_typeclass.py @@ -503,14 +503,36 @@ def instance( # TODO: at one point I would like to remove `is_protocol` # and make this function decide whether this type is protocol or not. is_protocol: bool = False, + delegate: Optional[type] = None, ) -> '_TypeClassInstanceDef[_NewInstanceType, _TypeClassType]': """ We use this method to store implementation for each specific type. - The only setting we provide is ``is_protocol`` which is required - when passing protocols. See our ``mypy`` plugin for that. + Args: + is_protocol - required when passing protocols. + delegate - required when using concrete generics like ``List[str]``. + + Returns: + Decorator for instance handler. + + .. note:: + + ``is_protocol`` and ``delegate`` are mutually exclusive. + + We don't use ``@overload`` decorator here + (which makes our ``mypy`` plugin even more complex) + because ``@overload`` functions do not + work well with ``ctx.api.fail`` inside the plugin. + They start to try other overloads, which produces wrong results. """ - typ = type_argument or type(None) # `None` is a special case + # This might seem like a strange line at first, let's dig into it: + # + # First, if `delegate` is passed, then we use delegate, not a real type. + # We use delegates for concrete generics. + # Then, we have a regular `type_argument`. It is used for most types. + # Lastly, we have `type(None)` to handle cases + # when we want to register `None` as a type / singleton value. + typ = delegate or type_argument or type(None) # That's how we check for generics, # generics that look like `List[int]` or `set[T]` will fail this check, @@ -531,9 +553,9 @@ def decorator(implementation): if self._cache_token is None: # pragma: no cover if getattr(typ, '__abstractmethods__', None): self._cache_token = get_cache_token() - self._dispatch_cache.clear() return implementation + return decorator def _control_abc_cache(self) -> None: diff --git a/classes/contrib/mypy/features/typeclass.py b/classes/contrib/mypy/features/typeclass.py index 8de7ec8..44a88f2 100644 --- a/classes/contrib/mypy/features/typeclass.py +++ b/classes/contrib/mypy/features/typeclass.py @@ -11,7 +11,7 @@ TupleType, ) from mypy.types import Type as MypyType -from mypy.types import TypeOfAny +from mypy.types import TypeOfAny, UninhabitedType from typing_extensions import final from classes.contrib.mypy.typeops import ( @@ -150,15 +150,21 @@ def instance_return_type(ctx: MethodContext) -> MypyType: assert isinstance(ctx.default_return_type, Instance) assert isinstance(ctx.type, Instance) + # We need to unify how we represent passed arguments to our internals. + # We use this convention: passed args are added as-is, + # missing ones are passed as `NoReturn` (because we cannot pass `None`). + passed_types = [] + for arg_pos in ctx.arg_types: + if arg_pos: + passed_types.extend(arg_pos) + else: + passed_types.append(UninhabitedType()) + instance_args.mutate_typeclass_instance_def( ctx.default_return_type, ctx=ctx, typeclass=ctx.type, - passed_types=[ - type_ - for args in ctx.arg_types - for type_ in args - ], + passed_types=passed_types, ) return ctx.default_return_type diff --git a/classes/contrib/mypy/validation/validate_instance_args.py b/classes/contrib/mypy/validation/validate_instance_args.py new file mode 100644 index 0000000..84719cd --- /dev/null +++ b/classes/contrib/mypy/validation/validate_instance_args.py @@ -0,0 +1,96 @@ +from typing import NamedTuple, Optional, Tuple + +from mypy.plugin import MethodContext +from mypy.types import FunctionLike, Instance, LiteralType, TupleType +from mypy.types import Type as MypyType +from mypy.types import UninhabitedType +from typing_extensions import Final, final + +# Messages: + +_IS_PROTOCOL_LITERAL_BOOL_MSG: Final = ( + 'Use literal bool for "is_protocol" argument, got: "{0}"' +) + +_PROTOCOL_AND_DELEGATE_PASSED_MSG: Final = ( + 'Both "is_protocol" and "delegate" arguments passed, they are exclusive' +) + + +@final +class _ArgValidationContext(NamedTuple): + """""" + + is_protocol: bool + delegate: Optional[MypyType] + check_result: bool + + +def check_type( + passed_types: TupleType, + ctx: MethodContext, +) -> _ArgValidationContext: + passed_args = passed_types.items + + is_protocol, protocol_check = _check_protocol_arg(passed_args[1], ctx) + delegate, delegate_check = _check_delegate_arg(passed_args[2], ctx) + + return _ArgValidationContext( + is_protocol=is_protocol, + delegate=delegate, + check_result=all([ + protocol_check, + delegate_check, + _check_all_args(passed_types, ctx), + ]), + ) + + +def _check_protocol_arg( + is_protocol: MypyType, + ctx: MethodContext, +) -> Tuple[bool, bool]: + if isinstance(is_protocol, UninhabitedType): + return False, True + + is_protocol_bool = ( + isinstance(is_protocol, Instance) and + isinstance(is_protocol.last_known_value, LiteralType) and + isinstance(is_protocol.last_known_value.value, bool) + ) + if is_protocol_bool: + return is_protocol.last_known_value.value, True # type: ignore + + ctx.api.fail( + _IS_PROTOCOL_LITERAL_BOOL_MSG.format(is_protocol), + ctx.context, + ) + return False, False + + +def _check_delegate_arg( + delegate: MypyType, + ctx: MethodContext, +) -> Tuple[Optional[MypyType], bool]: + # TODO: maybe we need to inforce that `delegate` should be + # similar to `runtime_type`? + # For example, we can ask for subtypes of `runtime_type`. + # However, motivation is not clear for now. + if isinstance(delegate, FunctionLike) and delegate.is_type_obj(): + return delegate.items()[-1].ret_type, True + return None, True + + +def _check_all_args( + passed_types: TupleType, + ctx: MethodContext, +) -> bool: + fake_args = [ + passed_arg + for passed_arg in passed_types.items[1:] + if isinstance(passed_arg, UninhabitedType) + ] + if not fake_args: + ctx.api.fail(_PROTOCOL_AND_DELEGATE_PASSED_MSG, ctx.context) + return False + return True diff --git a/classes/contrib/mypy/validation/validate_runtime.py b/classes/contrib/mypy/validation/validate_runtime.py index b8d37f0..690dfbf 100644 --- a/classes/contrib/mypy/validation/validate_runtime.py +++ b/classes/contrib/mypy/validation/validate_runtime.py @@ -1,22 +1,21 @@ -from typing import NamedTuple, Tuple +from typing import NamedTuple, Optional from mypy.erasetype import erase_type from mypy.plugin import MethodContext from mypy.sametypes import is_same_type -from mypy.types import CallableType, Instance, LiteralType, TupleType +from mypy.types import CallableType, Instance, TupleType from mypy.types import Type as MypyType -from typing_extensions import Final +from typing_extensions import Final, final from classes.contrib.mypy.typeops import inference, type_queries +from classes.contrib.mypy.validation import validate_instance_args + +# Messages: _INSTANCE_RUNTIME_MISMATCH_MSG: Final = ( 'Instance "{0}" does not match runtime type "{1}"' ) -_IS_PROTOCOL_LITERAL_BOOL_MSG: Final = ( - 'Use literal bool for "is_protocol" argument, got: "{0}"' -) - _IS_PROTOCOL_MISSING_MSG: Final = ( 'Protocols must be passed with "is_protocol=True"' ) @@ -39,7 +38,10 @@ ) +@final class _RuntimeValidationContext(NamedTuple): + """Structure to return required things into other validations.""" + runtime_type: MypyType is_protocol: bool check_result: bool @@ -70,11 +72,7 @@ def check_instance_definition( ctx=ctx, ) - if len(passed_types.items) == 2: - is_protocol, protocol_arg_check = _check_protocol_arg(passed_types, ctx) - else: - is_protocol = False - protocol_arg_check = True + args_check = validate_instance_args.check_type(passed_types, ctx) instance_type = instance_signature.arg_types[0] instance_check = is_same_type( @@ -87,33 +85,22 @@ def check_instance_definition( ctx.context, ) - return _RuntimeValidationContext(runtime_type, is_protocol, all([ - _check_runtime_protocol(runtime_type, ctx, is_protocol=is_protocol), - _check_concrete_generics(runtime_type, instance_type, ctx), - _check_tuple_size(instance_type, ctx), - protocol_arg_check, - instance_check, - ])) + return _RuntimeValidationContext( + runtime_type=runtime_type, + is_protocol=args_check.is_protocol, + check_result=all([ + args_check.check_result, + instance_check, - -def _check_protocol_arg( - passed_types: TupleType, - ctx: MethodContext, -) -> Tuple[bool, bool]: - passed_arg = passed_types.items[1] - is_literal_bool = ( - isinstance(passed_arg, Instance) and - isinstance(passed_arg.last_known_value, LiteralType) and - isinstance(passed_arg.last_known_value.value, bool) - ) - if is_literal_bool: - return passed_arg.last_known_value.value, True # type: ignore - - ctx.api.fail( - _IS_PROTOCOL_LITERAL_BOOL_MSG.format(passed_types.items[1]), - ctx.context, - ) - return False, False + _check_runtime_protocol( + runtime_type, ctx, is_protocol=args_check.is_protocol, + ), + _check_concrete_generics( + runtime_type, instance_type, args_check.delegate, ctx, + ), + _check_tuple_size(instance_type, ctx), + ], + )) def _check_runtime_protocol( @@ -135,6 +122,7 @@ def _check_runtime_protocol( def _check_concrete_generics( runtime_type: MypyType, instance_type: MypyType, + delegate: Optional[MypyType], ctx: MethodContext, ) -> bool: has_concrete_type = False @@ -143,15 +131,16 @@ def _check_concrete_generics( (runtime_type, True), ) - for type_, forbid_explicit_any in type_settings: - local_check = type_queries.has_concrete_type( - type_, - ctx, - forbid_explicit_any=forbid_explicit_any, - ) - if local_check: - ctx.api.fail(_CONCRETE_GENERIC_MSG.format(type_), ctx.context) - has_concrete_type = has_concrete_type or local_check + if delegate is None: + for type_, forbid_explicit_any in type_settings: + local_check = type_queries.has_concrete_type( + type_, + ctx, + forbid_explicit_any=forbid_explicit_any, + ) + if local_check: + ctx.api.fail(_CONCRETE_GENERIC_MSG.format(type_), ctx.context) + has_concrete_type = has_concrete_type or local_check if type_queries.has_unbound_type(runtime_type, ctx): ctx.api.fail(_UNBOUND_TYPE_MSG.format(runtime_type), ctx.context) From c59c4a55540e1b11aa6dbfe4f4e7961e5ed375c5 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 6 Jul 2021 14:00:22 +0300 Subject: [PATCH 2/4] Adds concrete generics support --- CHANGELOG.md | 1 + classes/_registry.py | 44 +++++ classes/_typeclass.py | 97 +++++----- .../mypy/validation/validate_instance_args.py | 14 +- .../mypy/validation/validate_runtime.py | 28 ++- docs/pages/api-docs.rst | 6 + docs/pages/concept.rst | 173 +++++++++++++++++- docs/requirements.txt | 1 + setup.cfg | 4 +- tests/conftest.py | 14 ++ tests/test_suppots.py | 51 +++++- tests/test_typeclass/test_cache.py | 33 ++-- tests/test_typeclass/test_call.py | 68 +++++-- 13 files changed, 430 insertions(+), 104 deletions(-) create mode 100644 classes/_registry.py create mode 100644 tests/conftest.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 78f1e64..1f4398d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ We follow Semantic Versions since the `0.1.0` release. ### Features +- Adds support for concrete generic types like `List[str]` and `Set[int]` #24 - Adds support for multiple type arguments in `Supports` type #244 - Adds support for types that have `__instancecheck__` defined #248 diff --git a/classes/_registry.py b/classes/_registry.py new file mode 100644 index 0000000..b6d35f9 --- /dev/null +++ b/classes/_registry.py @@ -0,0 +1,44 @@ +from types import MethodType +from typing import Callable, Dict, NoReturn, Optional + +TypeRegistry = Dict[type, Callable] + + +def choose_registry( # noqa: WPS211 + # It has multiple argumnets, but I don't see an easy and performant way + # to refactor it: I don't want to create extra structures + # and I don't want to create a class with methods. + typ: type, + is_protocol: bool, + delegate: Optional[type], + concretes: TypeRegistry, + instances: TypeRegistry, + protocols: TypeRegistry, +) -> TypeRegistry: + """ + Returns the appropriate registry to store the passed type. + + It depends on how ``instance`` method is used and also on the type itself. + """ + if is_protocol: + return protocols + + is_concrete = ( + delegate is not None or + isinstance(getattr(typ, '__instancecheck__', None), MethodType) + ) + if is_concrete: + # This means that this type has `__instancecheck__` defined, + # which allows dynamic checks of what `isinstance` of this type. + # That's why we also treat this type as a conrete. + return concretes + return instances + + +def default_implementation(instance, *args, **kwargs) -> NoReturn: + """By default raises an exception.""" + raise NotImplementedError( + 'Missing matched typeclass instance for type: {0}'.format( + type(instance).__qualname__, + ), + ) diff --git a/classes/_typeclass.py b/classes/_typeclass.py index f40d4d9..2ecf3ee 100644 --- a/classes/_typeclass.py +++ b/classes/_typeclass.py @@ -114,16 +114,12 @@ See our `official docs `_ to learn more! """ - -from abc import get_cache_token from functools import _find_impl # type: ignore # noqa: WPS450 -from types import MethodType from typing import ( # noqa: WPS235 TYPE_CHECKING, Callable, Dict, Generic, - NoReturn, Optional, Type, TypeVar, @@ -134,6 +130,12 @@ from typing_extensions import TypeGuard, final +from classes._registry import ( + TypeRegistry, + choose_registry, + default_implementation, +) + _InstanceType = TypeVar('_InstanceType') _SignatureType = TypeVar('_SignatureType', bound=Callable) _AssociatedType = TypeVar('_AssociatedType') @@ -305,12 +307,17 @@ class _TypeClass( # noqa: WPS214 """ __slots__ = ( + # Str: '_signature', '_associated_type', + + # Registry: + '_concretes', '_instances', '_protocols', + + # Cache: '_dispatch_cache', - '_cache_token', ) _dispatch_cache: Dict[type, Callable] @@ -349,16 +356,17 @@ def __init__( The only exception is the first argument: it is polymorfic. """ - self._instances: Dict[type, Callable] = {} - self._protocols: Dict[type, Callable] = {} - # We need this for `repr`: self._signature = signature self._associated_type = associated_type + # Registries: + self._concretes: TypeRegistry = {} + self._instances: TypeRegistry = {} + self._protocols: TypeRegistry = {} + # Cache parts: self._dispatch_cache = WeakKeyDictionary() # type: ignore - self._cache_token = None def __call__( self, @@ -410,7 +418,16 @@ def __call__( And all typeclasses that match ``Callable[[int, int], int]`` signature will typecheck. """ - self._control_abc_cache() + # At first, we try all our conrete types, + # we don't cache it, because we cannot. + # We only have runtime type info: `type([1]) == type(['a'])`. + # It might be slow! + # Don't add concrete types unless + # you are absolutely know what you are doing. + impl = self._dispatch_concrete(instance) + if impl is not None: + return impl(instance, *args, **kwargs) + instance_type = type(instance) try: @@ -419,7 +436,7 @@ def __call__( impl = self._dispatch( instance, instance_type, - ) or self._default_implementation + ) or default_implementation self._dispatch_cache[instance_type] = impl return impl(instance, *args, **kwargs) @@ -481,16 +498,24 @@ def supports( See also: https://www.python.org/dev/peps/pep-0647 """ - self._control_abc_cache() - + # Here we first check that instance is already in the cache + # and only then we check concrete types. + # Why? + # Because if some type is already in the cache, + # it means that it is not concrete. + # So, this is simply faster. instance_type = type(instance) if instance_type in self._dispatch_cache: return True - # This only happens when we don't have a cache in place: + # We never cache concrete types. + if self._dispatch_concrete(instance) is not None: + return True + + # This only happens when we don't have a cache in place + # and this is not a concrete generic: impl = self._dispatch(instance, instance_type) if impl is None: - self._dispatch_cache[instance_type] = self._default_implementation return False self._dispatch_cache[instance_type] = impl @@ -541,35 +566,21 @@ def instance( isinstance(object(), typ) def decorator(implementation): - container = self._protocols if is_protocol else self._instances + container = choose_registry( + typ=typ, + is_protocol=is_protocol, + delegate=delegate, + concretes=self._concretes, + instances=self._instances, + protocols=self._protocols, + ) container[typ] = implementation - if isinstance(getattr(typ, '__instancecheck__', None), MethodType): - # This means that this type has `__instancecheck__` defined, - # which allows dynamic checks of what `isinstance` of this type. - # That's why we also treat this type as a protocol. - self._protocols[typ] = implementation - - if self._cache_token is None: # pragma: no cover - if getattr(typ, '__abstractmethods__', None): - self._cache_token = get_cache_token() self._dispatch_cache.clear() return implementation return decorator - def _control_abc_cache(self) -> None: - """ - Required to drop cache if ``abc`` type got new subtypes in runtime. - - Copied from ``cpython``. - """ - if self._cache_token is not None: - current_token = get_cache_token() - if self._cache_token != current_token: - self._dispatch_cache.clear() - self._cache_token = current_token - def _dispatch(self, instance, instance_type: type) -> Optional[Callable]: """ Dispatches a function by its type. @@ -589,13 +600,11 @@ def _dispatch(self, instance, instance_type: type) -> Optional[Callable]: return _find_impl(instance_type, self._instances) - def _default_implementation(self, instance, *args, **kwargs) -> NoReturn: - """By default raises an exception.""" - raise NotImplementedError( - 'Missing matched typeclass instance for type: {0}'.format( - type(instance).__qualname__, - ), - ) + def _dispatch_concrete(self, instance) -> Optional[Callable]: + for concrete, callback in self._concretes.items(): + if isinstance(instance, concrete): + return callback + return None if TYPE_CHECKING: diff --git a/classes/contrib/mypy/validation/validate_instance_args.py b/classes/contrib/mypy/validation/validate_instance_args.py index 84719cd..0b0a05a 100644 --- a/classes/contrib/mypy/validation/validate_instance_args.py +++ b/classes/contrib/mypy/validation/validate_instance_args.py @@ -19,7 +19,7 @@ @final class _ArgValidationContext(NamedTuple): - """""" + """Context for instance arg validation.""" is_protocol: bool delegate: Optional[MypyType] @@ -30,6 +30,14 @@ def check_type( passed_types: TupleType, ctx: MethodContext, ) -> _ArgValidationContext: + """ + Checks that args to ``.instance`` method are correct. + + We cannot use ``@overload`` on ``.instance`` because ``mypy`` + does not correctly handle ``ctx.api.fail`` on ``@overload`` items: + it then tries new ones, which produce incorrect results. + So, that's why we need this custom checker. + """ passed_args = passed_types.items is_protocol, protocol_check = _check_protocol_arg(passed_args[1], ctx) @@ -72,10 +80,6 @@ def _check_delegate_arg( delegate: MypyType, ctx: MethodContext, ) -> Tuple[Optional[MypyType], bool]: - # TODO: maybe we need to inforce that `delegate` should be - # similar to `runtime_type`? - # For example, we can ask for subtypes of `runtime_type`. - # However, motivation is not clear for now. if isinstance(delegate, FunctionLike) and delegate.is_type_obj(): return delegate.items()[-1].ret_type, True return None, True diff --git a/classes/contrib/mypy/validation/validate_runtime.py b/classes/contrib/mypy/validation/validate_runtime.py index 690dfbf..456ce30 100644 --- a/classes/contrib/mypy/validation/validate_runtime.py +++ b/classes/contrib/mypy/validation/validate_runtime.py @@ -85,22 +85,18 @@ def check_instance_definition( ctx.context, ) - return _RuntimeValidationContext( - runtime_type=runtime_type, - is_protocol=args_check.is_protocol, - check_result=all([ - args_check.check_result, - instance_check, - - _check_runtime_protocol( - runtime_type, ctx, is_protocol=args_check.is_protocol, - ), - _check_concrete_generics( - runtime_type, instance_type, args_check.delegate, ctx, - ), - _check_tuple_size(instance_type, ctx), - ], - )) + return _RuntimeValidationContext(runtime_type, args_check.is_protocol, all([ + args_check.check_result, + instance_check, + + _check_runtime_protocol( + runtime_type, ctx, is_protocol=args_check.is_protocol, + ), + _check_concrete_generics( + runtime_type, instance_type, args_check.delegate, ctx, + ), + _check_tuple_size(instance_type, ctx), + ])) def _check_runtime_protocol( diff --git a/docs/pages/api-docs.rst b/docs/pages/api-docs.rst index 8fe6df5..23f32aa 100644 --- a/docs/pages/api-docs.rst +++ b/docs/pages/api-docs.rst @@ -1,6 +1,12 @@ Typeclass ========= +Caching +------- + +API +--- + Here are the technical docs about ``typeclass`` and how to use it. .. automodule:: classes._typeclass diff --git a/docs/pages/concept.rst b/docs/pages/concept.rst index 9a60757..a599f90 100644 --- a/docs/pages/concept.rst +++ b/docs/pages/concept.rst @@ -130,22 +130,183 @@ Example: >>> assert isinstance(argument, Some) >>> assert some(argument) == 2 -.. note:: +.. warning:: It is impossible for ``mypy`` to understand that ``1`` has ``Some`` type in this example. Be careful, it might break your code! +This example is not really useful on its own, +because as it was said, it can break things. + +Instead, we are going to learn about +how this feature can be used to model +your domain model precisely with delegates. + +Performance considerations +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Types that are matched via ``__instancecheck__`` are the first one we try. +So, the worst case complexity of this is ``O(n)`` +where ``n`` is the number of types to try. + +We also always try them first and do not cache the result. +This feature is here because we need to handle concrete generics. +But, we recommend to think at least +twice about the performance side of this feature. +Maybe you can just write a function? + + +Delegates +--------- + +Let's say that you want to handle types like ``List[int]`` with ``classes``. +The simple approach won't work, because Python cannot tell +that some ``list`` is ``List[int]`` or ``List[str]``: + +.. code:: python + + >>> from typing import List + + >>> isinstance([1, 2, 3], List[int]) + Traceback (most recent call last): + ... + TypeError: Subscripted generics cannot be used with class and instance checks + +We need some custom type inference mechanism: + +.. code:: python + + >>> from typing import List + + >>> class _ListOfIntMeta(type): + ... def __instancecheck__(self, arg) -> bool: + ... return ( + ... isinstance(arg, list) and + ... bool(arg) and # we need to have at least one `int` element + ... all(isinstance(item, int) for item in arg) + ... ) + + >>> class ListOfInt(List[int], metaclass=_ListOfIntMeta): + ... ... + +Now we can be sure that our ``List[int]`` can be checked in runtime: + +.. code:: python + + >>> assert isinstance([1, 2, 3], ListOfInt) is True + >>> assert isinstance([1, 'a'], ListOfInt) is False + +And now we can use it with ``classes``: + +.. code:: python + + >>> from classes import typeclass + + >>> @typeclass + ... def sum_all(instance) -> int: + ... ... + + >>> @sum_all.instance(ListOfInt) + ... def _sum_all_list_int(instance: ListOfInt) -> int: + ... return sum(instance) + + >>> your_list = [1, 2, 3] + >>> if isinstance(your_list, ListOfInt): + ... assert sum_all(your_list) == 6 + +This solution still has several problems: + +1. Notice, that you have to use ``if isinstance`` or ``assert isinstance`` here. + Because otherwise ``mypy`` won't be happy without it, + type won't be narrowed to ``ListOfInt`` from ``List[int]``. + This does not feel right. +2. ``ListOfInt`` is very verbose, it even has a metaclass! +3. There's a typing mismatch: in runtime ``your_list`` would be ``List[int]`` + and ``mypy`` thinks that it is ``ListOfInt`` + (a fake type that we are not ever using directly) + +To solve all these problems we recommend to use ``phantom-types`` package. + +First, you need to define a "phantom" type +(it is called "phantom" because it does not exist in runtime): + +.. code:: python + + >>> from phantom import Phantom + >>> from phantom.predicates import boolean, collection, generic, numeric + + >>> class ListOfInt( + ... List[int], + ... Phantom, + ... predicate=boolean.both( + ... collection.count(numeric.greater(0)), + ... collection.every(generic.of_type(int)), + ... ), + ... ): + ... ... + + >>> assert isinstance([1, 2, 3], ListOfInt) + >>> assert type([1, 2, 3]) is list + +Short, easy, and readable: + +- By defining ``predicate`` we ensure + that all non-empty lists with ``int`` elements + will be treated as ``ListOfInt`` +- In runtime ``ListOfInt`` does not exist, because it is phantom! + In reality it is just ``List[int]`` + +Now, we can define our typeclass with ``phantom`` type support: + +.. code:: python + + >>> from classes import typeclass + + >>> @typeclass + ... def sum_all(instance) -> int: + ... ... + + >>> @sum_all.instance(List[int], delegate=ListOfInt) + ... def _sum_all_list_int(instance: List[int]) -> int: + ... return sum(instance) + + >>> assert sum_all([1, 2, 3]) == 6 + +That's why we need a ``delegate=`` argument here: +we don't really work with ``List[int]``, +we delegate all the runtime type checking to ``ListOfInt`` phantom type. + +Performance considerations +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Traversing the whole list to check that all elements +are of the given type can be really slow. + +You might need a different algorithm. +Take a look at `beartype `_. +It promises runtime type checking with ``O(1)`` non-amortized worst-case time +with negligible constant factors. + +Take a look at their docs to learn more. + Type resolution order --------------------- Here's how typeclass resolve types: -1. We try to resolve exact match by a passed type -2. Then we try to match passed type a given protocols, first match wins -3. Then we traverse ``mro`` entries of a given type, first match wins - -We use cache, so calling typeclasses with same object types is fast. +1. At first we try to resolve types via delegates and ``isinstance`` checks +2. We try to resolve exact match by a passed type +3. Then we try to match passed type with ``isinstance`` + against protocol types, + first match wins +4. Then we traverse ``mro`` entries of a given type, + looking for ones we can handle, + first match wins + +We use cache for all parts of algorithm except the first step +(it is never cached), +so calling typeclasses with same object types is fast. In other words, it can fallback to more common types: diff --git a/docs/requirements.txt b/docs/requirements.txt index 3662fd9..8b24bfe 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -11,6 +11,7 @@ tomlkit==0.7.2 # Dependencies of our project: typing-extensions==3.10.0.0 +phantom-types==0.9.1 # TODO: Remove this lock when we found and fix the route case. # See: https://github.com/typlog/sphinx-typlog-theme/issues/22 diff --git a/setup.cfg b/setup.cfg index e7fea11..fbabcb1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,11 +35,11 @@ ignore = D100, D104, D401, W504, X100, WPS121, RST299, RST303, RST304, DAR103, D per-file-ignores = classes/__init__.py: F401, WPS113, WPS436 - classes/_typeclass.py: WPS320 + classes/_typeclass.py: WPS320, WPS436 # We need `assert`s to please mypy: classes/contrib/mypy/*.py: S101 # There are multiple assert's in tests: - tests/*.py: S101, WPS226, WPS431, WPS432, WPS436 + tests/*.py: S101, WPS202, WPS226, WPS431, WPS432, WPS436 [isort] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2515535 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,14 @@ +from contextlib import contextmanager + +import pytest + + +@pytest.fixture(scope='session') +def clear_cache(): + """Fixture to clear typeclass'es cache before and after.""" + @contextmanager + def factory(typeclass): + typeclass._dispatch_cache.clear() # noqa: WPS437 + yield + typeclass._dispatch_cache.clear() # noqa: WPS437 + return factory diff --git a/tests/test_suppots.py b/tests/test_suppots.py index 8f34f31..618ac87 100644 --- a/tests/test_suppots.py +++ b/tests/test_suppots.py @@ -1,10 +1,27 @@ -from typing import Sized +from typing import List, Sized import pytest from classes import typeclass +class _ListOfStrMeta(type): + def __instancecheck__(cls, other) -> bool: + return ( + isinstance(other, list) and + len(other) and + all(isinstance(list_item, str) for list_item in other) + ) + + +class _ListOfStr(List[str], metaclass=_ListOfStrMeta): + """We use this for testing concrete type calls.""" + + +class _MyList(list): # noqa: WPS600 + """We use it to test mro.""" + + @typeclass def my_len(instance) -> int: """Returns a length of an object.""" @@ -20,17 +37,37 @@ def _my_len_list(instance: list) -> int: return 1 -class _MyList(list): # noqa: WPS600 - """We use it to test mro.""" +@my_len.instance(List[str], delegate=_ListOfStr) +def _my_len_list_str(instance: List[str]) -> int: + return 2 @pytest.mark.parametrize(('data_type', 'expected'), [ ([], True), # direct list call - ('', True), # sized protocol) + ('', True), # sized protocol (1, False), # default impl (_MyList(), True), # mro fallback + (_ListOfStr(), True), # mro fallback + (_ListOfStr([1, 2, 3]), True), # mro fallback ]) -def test_supports(data_type, expected): +def test_supports(data_type, expected: bool, clear_cache) -> None: """Ensures that ``.supports`` works correctly.""" - assert my_len.supports(data_type) is expected - assert type(data_type) in my_len._dispatch_cache # noqa: WPS437, WPS516 + with clear_cache(my_len): + assert my_len.supports(data_type) is expected + + +def test_supports_twice_regular(clear_cache) -> None: + """Ensures that calling ``supports`` twice for regular type is cached.""" + with clear_cache(my_len): + assert list not in my_len._dispatch_cache # noqa: WPS437 + assert my_len.supports([]) is True + assert list in my_len._dispatch_cache # noqa: WPS437 + assert my_len.supports([]) is True + + +def test_supports_twice_concrete(clear_cache) -> None: + """Ensures that calling ``supports`` twice for concrete type is ignored.""" + with clear_cache(my_len): + for _ in range(2): + assert not my_len._dispatch_cache # noqa: WPS437 + assert my_len.supports(['a', 'b']) is True diff --git a/tests/test_typeclass/test_cache.py b/tests/test_typeclass/test_cache.py index 58bc3ad..92c0306 100644 --- a/tests/test_typeclass/test_cache.py +++ b/tests/test_typeclass/test_cache.py @@ -14,7 +14,7 @@ def get_number(self) -> int: """Example abstract method.""" -class _MyConcete(_MyABC): +class _MyConcrete(_MyABC): def get_number(self) -> int: """Concrete method.""" return 1 @@ -34,18 +34,27 @@ def _my_abc(instance: _MyABC) -> int: return instance.get_number() -def test_cache_invalidation(): # noqa: WPS218 +def test_cache_concrete(clear_cache): # noqa: WPS218 """Ensures that cache invalidation for ABC types work correctly.""" - assert not my_typeclass._dispatch_cache # noqa: WPS437 - assert not my_typeclass._cache_token # noqa: WPS437 + with clear_cache(my_typeclass): + assert not my_typeclass._dispatch_cache # noqa: WPS437 - my_typeclass.instance(_MyABC)(_my_abc) - assert not my_typeclass._dispatch_cache # noqa: WPS437 - assert my_typeclass._cache_token # noqa: WPS437 + my_typeclass.instance(_MyABC)(_my_abc) + assert not my_typeclass._dispatch_cache # noqa: WPS437 - assert my_typeclass(_MyConcete()) == 1 - assert my_typeclass._dispatch_cache # noqa: WPS437 + assert my_typeclass(_MyConcrete()) == 1 + assert not my_typeclass._dispatch_cache # noqa: WPS437 - _MyABC.register(_MyRegistered) - assert my_typeclass(_MyRegistered()) == 2 # type: ignore - assert my_typeclass._dispatch_cache # noqa: WPS437 + _MyABC.register(_MyRegistered) + assert my_typeclass(_MyRegistered()) == 2 # type: ignore + assert not my_typeclass._dispatch_cache # noqa: WPS437 + + +def test_cached_calls(clear_cache): + """Ensures that regular types trigger cache.""" + with clear_cache(my_typeclass): + my_typeclass.instance(int)(_my_int) + assert not my_typeclass._dispatch_cache # noqa: WPS437 + + assert my_typeclass(1) + assert my_typeclass._dispatch_cache # noqa: WPS437 diff --git a/tests/test_typeclass/test_call.py b/tests/test_typeclass/test_call.py index 50b0e3d..304d886 100644 --- a/tests/test_typeclass/test_call.py +++ b/tests/test_typeclass/test_call.py @@ -1,22 +1,30 @@ -from typing import Sized +from typing import List, Sized import pytest from classes import typeclass +class _ListOfStrMeta(type): + def __instancecheck__(cls, other) -> bool: + return ( + isinstance(other, list) and + len(other) and + all(isinstance(list_item, str) for list_item in other) + ) + + +class _ListOfStr(List[str], metaclass=_ListOfStrMeta): + """We use this for testing concrete type calls.""" + + @typeclass def my_len(instance) -> int: """Returns a length of an object.""" -@my_len.instance(object) -def _my_len_object(instance: object) -> int: - return -1 - - -@my_len.instance(Sized, is_protocol=True) -def _my_len_sized(instance: Sized) -> int: +@my_len.instance(List[str], delegate=_ListOfStr) +def _my_len_list_str(instance: List[str]) -> int: return 0 @@ -25,11 +33,47 @@ def _my_len_list(instance: list) -> int: return 1 +@my_len.instance(Sized, is_protocol=True) +def _my_len_sized(instance: Sized) -> int: + return 2 + + +@my_len.instance(object) +def _my_len_object(instance: object) -> int: + return 3 + + +@pytest.mark.parametrize('clear_initial_cache', [True, False]) +@pytest.mark.parametrize('check_supports', [True, False]) +@pytest.mark.parametrize('clear_supports_cache', [True, False]) @pytest.mark.parametrize(('data_type', 'expected'), [ + (['a', 'b'], 0), # conrete type ([], 1), # direct list call - ('', 0), # sized protocol - (1, -1), # object fallback + ([1, 2, 3], 1), # direct list call + ('', 2), # sized protocol + (1, 3), # object fallback ]) -def test_call_order(data_type, expected): - """Ensures that call order is correct.""" +def test_call_order( + data_type, + expected, + clear_initial_cache: bool, + check_supports: bool, + clear_supports_cache: bool, +) -> None: + """ + Ensures that call order is correct. + + This is a very tricky test. + It tests all dispatching order. + Moreover, it also tests how cache + interacts with ``__call__`` and ``supports``. + + We literally model all possible cases here. + """ + if clear_initial_cache: + my_len._dispatch_cache.clear() # noqa: WPS437 + if check_supports: + assert my_len.supports(data_type) + if clear_supports_cache: + my_len._dispatch_cache.clear() # noqa: WPS437 assert my_len(data_type) == expected From d80bac8b5006215ddd948cc3d6904c3b6d0f0be5 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 8 Jul 2021 12:43:08 +0300 Subject: [PATCH 3/4] Fixes bug with `ABC` instances, annotates tests --- .readthedocs.yml | 2 +- classes/_registry.py | 4 +-- classes/_typeclass.py | 2 +- .../contrib/mypy/typeops/call_signatures.py | 2 +- tests/conftest.py | 7 +++-- tests/test_suppots.py | 4 +-- tests/test_typeclass/test_cache.py | 4 +-- tests/test_typeclass/test_call.py | 4 +-- tests/test_typeclass/test_callback.py | 2 +- tests/test_typeclass/test_protocols.py | 4 +-- tests/test_typeclass/test_regular_types.py | 2 +- tests/test_typeclass/test_repr.py | 2 +- .../test_generics/test_generics_regular.yml | 27 +++++++++++++++++++ 13 files changed, 48 insertions(+), 18 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index dcf8243..7fe73d3 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,7 +1,7 @@ version: 2 python: - version: 3.7 + version: 3.8 install: - requirements: docs/requirements.txt diff --git a/classes/_registry.py b/classes/_registry.py index b6d35f9..6c25699 100644 --- a/classes/_registry.py +++ b/classes/_registry.py @@ -5,7 +5,7 @@ def choose_registry( # noqa: WPS211 - # It has multiple argumnets, but I don't see an easy and performant way + # It has multiple arguments, but I don't see an easy and performant way # to refactor it: I don't want to create extra structures # and I don't want to create a class with methods. typ: type, @@ -30,7 +30,7 @@ def choose_registry( # noqa: WPS211 if is_concrete: # This means that this type has `__instancecheck__` defined, # which allows dynamic checks of what `isinstance` of this type. - # That's why we also treat this type as a conrete. + # That's why we also treat this type as a concrete. return concretes return instances diff --git a/classes/_typeclass.py b/classes/_typeclass.py index 2ecf3ee..4b9e0f2 100644 --- a/classes/_typeclass.py +++ b/classes/_typeclass.py @@ -418,7 +418,7 @@ def __call__( And all typeclasses that match ``Callable[[int, int], int]`` signature will typecheck. """ - # At first, we try all our conrete types, + # At first, we try all our concrete types, # we don't cache it, because we cannot. # We only have runtime type info: `type([1]) == type(['a'])`. # It might be slow! diff --git a/classes/contrib/mypy/typeops/call_signatures.py b/classes/contrib/mypy/typeops/call_signatures.py index 884f264..64ae1c4 100644 --- a/classes/contrib/mypy/typeops/call_signatures.py +++ b/classes/contrib/mypy/typeops/call_signatures.py @@ -43,7 +43,7 @@ def __init__( ctx: MethodSigContext, ) -> None: """Context that we need.""" - self._signature = signature + self._signature = signature.copy_modified() self._instance_type = instance_type self._associated_type = associated_type self._ctx = ctx diff --git a/tests/conftest.py b/tests/conftest.py index 2515535..4275ffe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,16 @@ from contextlib import contextmanager +from typing import Callable, ContextManager, Iterator import pytest +from classes._typeclass import _TypeClass # noqa: WPS450 + @pytest.fixture(scope='session') -def clear_cache(): +def clear_cache() -> Callable[[_TypeClass], ContextManager]: """Fixture to clear typeclass'es cache before and after.""" @contextmanager - def factory(typeclass): + def factory(typeclass: _TypeClass) -> Iterator[None]: typeclass._dispatch_cache.clear() # noqa: WPS437 yield typeclass._dispatch_cache.clear() # noqa: WPS437 diff --git a/tests/test_suppots.py b/tests/test_suppots.py index 618ac87..cf7c0b5 100644 --- a/tests/test_suppots.py +++ b/tests/test_suppots.py @@ -9,7 +9,7 @@ class _ListOfStrMeta(type): def __instancecheck__(cls, other) -> bool: return ( isinstance(other, list) and - len(other) and + bool(other) and all(isinstance(list_item, str) for list_item in other) ) @@ -48,7 +48,7 @@ def _my_len_list_str(instance: List[str]) -> int: (1, False), # default impl (_MyList(), True), # mro fallback (_ListOfStr(), True), # mro fallback - (_ListOfStr([1, 2, 3]), True), # mro fallback + (_ListOfStr(['a']), True), # mro fallback ]) def test_supports(data_type, expected: bool, clear_cache) -> None: """Ensures that ``.supports`` works correctly.""" diff --git a/tests/test_typeclass/test_cache.py b/tests/test_typeclass/test_cache.py index 92c0306..6706776 100644 --- a/tests/test_typeclass/test_cache.py +++ b/tests/test_typeclass/test_cache.py @@ -34,7 +34,7 @@ def _my_abc(instance: _MyABC) -> int: return instance.get_number() -def test_cache_concrete(clear_cache): # noqa: WPS218 +def test_cache_concrete(clear_cache) -> None: # noqa: WPS218 """Ensures that cache invalidation for ABC types work correctly.""" with clear_cache(my_typeclass): assert not my_typeclass._dispatch_cache # noqa: WPS437 @@ -50,7 +50,7 @@ def test_cache_concrete(clear_cache): # noqa: WPS218 assert not my_typeclass._dispatch_cache # noqa: WPS437 -def test_cached_calls(clear_cache): +def test_cached_calls(clear_cache) -> None: """Ensures that regular types trigger cache.""" with clear_cache(my_typeclass): my_typeclass.instance(int)(_my_int) diff --git a/tests/test_typeclass/test_call.py b/tests/test_typeclass/test_call.py index 304d886..b0889d3 100644 --- a/tests/test_typeclass/test_call.py +++ b/tests/test_typeclass/test_call.py @@ -9,7 +9,7 @@ class _ListOfStrMeta(type): def __instancecheck__(cls, other) -> bool: return ( isinstance(other, list) and - len(other) and + bool(other) and all(isinstance(list_item, str) for list_item in other) ) @@ -47,7 +47,7 @@ def _my_len_object(instance: object) -> int: @pytest.mark.parametrize('check_supports', [True, False]) @pytest.mark.parametrize('clear_supports_cache', [True, False]) @pytest.mark.parametrize(('data_type', 'expected'), [ - (['a', 'b'], 0), # conrete type + (['a', 'b'], 0), # concrete type ([], 1), # direct list call ([1, 2, 3], 1), # direct list call ('', 2), # sized protocol diff --git a/tests/test_typeclass/test_callback.py b/tests/test_typeclass/test_callback.py index a727b51..692ab06 100644 --- a/tests/test_typeclass/test_callback.py +++ b/tests/test_typeclass/test_callback.py @@ -20,7 +20,7 @@ def _callback( return callback(instance) -def test_callback(): +def test_callback() -> None: """Tests that callback works.""" assert _callback('a', example) == 1 assert _callback('abcd', example) == 4 diff --git a/tests/test_typeclass/test_protocols.py b/tests/test_typeclass/test_protocols.py index 4331542..16c3bb4 100644 --- a/tests/test_typeclass/test_protocols.py +++ b/tests/test_typeclass/test_protocols.py @@ -23,12 +23,12 @@ def __len__(self) -> int: return 2 -def test_sized_protocol(): +def test_sized_protocol() -> None: """Ensure that sized protocol works.""" assert protocols(_CustomSized(), '1') == '21' assert protocols([1, 2, 3], '0') == '30' -def test_type_takes_over(): +def test_type_takes_over() -> None: """Ensure that int protocol works.""" assert protocols('a', 'b') == 'ab' diff --git a/tests/test_typeclass/test_regular_types.py b/tests/test_typeclass/test_regular_types.py index 2c3219c..9e9e0d6 100644 --- a/tests/test_typeclass/test_regular_types.py +++ b/tests/test_typeclass/test_regular_types.py @@ -18,7 +18,7 @@ def _example_int(instance: int) -> str: return 'a' * instance -def test_regular_type(): +def test_regular_type() -> None: """Ensures that types correctly work.""" assert example([1, 2, 3]) == '123' assert example(['a', 'b', 'c']) == 'abc' diff --git a/tests/test_typeclass/test_repr.py b/tests/test_typeclass/test_repr.py index 8d065f9..40df2bb 100644 --- a/tests/test_typeclass/test_repr.py +++ b/tests/test_typeclass/test_repr.py @@ -15,7 +15,7 @@ def my_typeclass(instance) -> str: """Docs.""" -def test_str(): +def test_str() -> None: """Ensures that ``str`` is correct.""" assert str(my_typeclass) == '' assert str( diff --git a/typesafety/test_typeclass/test_generics/test_generics_regular.yml b/typesafety/test_typeclass/test_generics/test_generics_regular.yml index bd7af84..44a7aeb 100644 --- a/typesafety/test_typeclass/test_generics/test_generics_regular.yml +++ b/typesafety/test_typeclass/test_generics/test_generics_regular.yml @@ -272,3 +272,30 @@ ... out: | main:10: error: Expected variadic tuple "Tuple[X`-1, ...]", got "Tuple[X`-1, X`-1]" + + +- case: typeclass_regression259_mutated_signature + disable_cache: false + main: | + from abc import ABCMeta, abstractmethod + from classes import typeclass + + @typeclass + def my_typeclass(instance) -> int: + ... + + class _MyABC(object, metaclass=ABCMeta): + ... + + class _MyConcrete(_MyABC): + ... + + @my_typeclass.instance(_MyABC) + def _my_abc(instance: _MyABC) -> int: + ... + + my_typeclass(_MyConcrete()) + + @my_typeclass.instance(int) + def _my_int(instance: int) -> int: + ... From 03624134b285b8953c24f54ecbafad99fda9c10a Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 8 Jul 2021 13:01:22 +0300 Subject: [PATCH 4/4] Ignores phantom-types for python3.7 --- CHANGELOG.md | 1 + docs/conftest.py | 32 ++++ poetry.lock | 173 ++++++++++++-------- pyproject.toml | 3 + tests/{test_suppots.py => test_supports.py} | 0 5 files changed, 137 insertions(+), 72 deletions(-) create mode 100644 docs/conftest.py rename tests/{test_suppots.py => test_supports.py} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f4398d..d4ee488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ We follow Semantic Versions since the `0.1.0` release. - Fixes that types referenced in multiple typeclasses were not handling `Supports` properly #249 +- Fixes typing bug with `ABC` and mutable typeclass signature #259 ## Version 0.3.0 diff --git a/docs/conftest.py b/docs/conftest.py new file mode 100644 index 0000000..5f85da6 --- /dev/null +++ b/docs/conftest.py @@ -0,0 +1,32 @@ +import sys +from pathlib import Path +from types import MappingProxyType + +from typing_extensions import Final + +PYTHON_VERSION: Final = (sys.version_info.major, sys.version_info.minor) +ENABLE_SINCE: Final = MappingProxyType({ + (3, 8): frozenset(( + Path('docs/pages/concept.rst'), + )), +}) +PATHS_TO_IGNORE_NOW: Final = frozenset( + path.absolute() + for since_python, to_ignore in ENABLE_SINCE.items() + for path in to_ignore + if PYTHON_VERSION < since_python +) + + +# TODO: remove after `phantom-types` release with `python3.7` support +def pytest_collection_modifyitems(items) -> None: # noqa: WPS110 + """Conditionally removes some collected docstests.""" + to_ignore_items = [] + for test_item in items: + if not getattr(test_item, 'dtest', None): + continue + if Path(test_item.dtest.filename) in PATHS_TO_IGNORE_NOW: + to_ignore_items.append(test_item) + + for to_ignore in to_ignore_items: + items.remove(to_ignore) diff --git a/poetry.lock b/poetry.lock index f0b8057..3ae02c2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -275,7 +275,7 @@ flake8 = ">=3.5,<4.0" [[package]] name = "flake8-bugbear" -version = "20.11.1" +version = "21.4.3" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." category = "dev" optional = false @@ -338,7 +338,7 @@ pydocstyle = ">=2.1" [[package]] name = "flake8-eradicate" -version = "1.0.0" +version = "1.1.0" description = "Flake8 plugin to find commented out code" category = "dev" optional = false @@ -443,11 +443,11 @@ smmap = ">=3.0.1,<5" [[package]] name = "gitpython" -version = "3.1.17" +version = "3.1.18" description = "Python Git Library" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] gitdb = ">=4.0.1,<5" @@ -482,7 +482,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.5.0" +version = "4.6.1" description = "Read metadata from Python packages" category = "dev" optional = false @@ -494,7 +494,8 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +perf = ["ipython"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -506,16 +507,17 @@ python-versions = "*" [[package]] name = "isort" -version = "5.8.0" +version = "5.9.2" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.6.1,<4.0" [package.extras] pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] [[package]] name = "jinja2" @@ -576,16 +578,16 @@ python-versions = ">=3.6" [[package]] name = "marshmallow" -version = "3.12.1" +version = "3.12.2" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." category = "dev" optional = false python-versions = ">=3.5" [package.extras] -dev = ["pytest", "pytz", "simplejson", "mypy (==0.812)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)", "tox"] -docs = ["sphinx (==4.0.0)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.4)"] -lint = ["mypy (==0.812)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)"] +dev = ["pytest", "pytz", "simplejson", "mypy (==0.910)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["sphinx (==4.0.3)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.6)"] +lint = ["mypy (==0.910)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)"] tests = ["pytest", "pytz", "simplejson"] [[package]] @@ -687,11 +689,11 @@ doc = ["sphinx", "sphinx-rtd-theme", "sphobjinv"] [[package]] name = "packaging" -version = "20.9" +version = "21.0" description = "Core utilities for Python packages" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2" @@ -715,6 +717,23 @@ python-versions = "*" [package.dependencies] flake8-polyfill = ">=1.0.2,<2" +[[package]] +name = "phantom-types" +version = "0.9.1" +description = "Phantom types for Python" +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +typeguard = ">=2.10" + +[package.extras] +iso3166 = ["iso3166 (>=1.0.1)"] +phonenumbers = ["phonenumbers (>=8.12.11)"] +pydantic = ["pydantic (>=1.8.1)"] +test = ["mypy", "pytest", "pytest-mypy-plugins", "coverage"] + [[package]] name = "pluggy" version = "0.13.1" @@ -932,7 +951,7 @@ docutils = ">=0.11,<1.0" [[package]] name = "ruamel.yaml" -version = "0.17.9" +version = "0.17.10" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" category = "dev" optional = false @@ -947,11 +966,11 @@ jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] [[package]] name = "ruamel.yaml.clib" -version = "0.2.2" +version = "0.2.6" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" [[package]] name = "safety" @@ -1192,6 +1211,18 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "typeguard" +version = "2.12.1" +description = "Run-time type checker for Python" +category = "dev" +optional = false +python-versions = ">=3.5.3" + +[package.extras] +doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["pytest", "typing-extensions", "mypy"] + [[package]] name = "typing-extensions" version = "3.10.0.0" @@ -1202,7 +1233,7 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.26.5" +version = "1.26.6" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false @@ -1256,7 +1287,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] [[package]] name = "zipp" -version = "3.4.1" +version = "3.5.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false @@ -1264,12 +1295,12 @@ python-versions = ">=3.6" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "2858fe955fd32c6d174cd2bdf37bc5798e401718ef087a91211b772cdce6b9cc" +content-hash = "9bdebde82a832e42520837d6a6dafe36e2f68e164020c34654913f31d012f532" [metadata.files] alabaster = [ @@ -1422,8 +1453,8 @@ flake8-broken-line = [ {file = "flake8_broken_line-0.3.0-py3-none-any.whl", hash = "sha256:611f79c7f27118e7e5d3dc098ef7681c40aeadf23783700c5dbee840d2baf3af"}, ] flake8-bugbear = [ - {file = "flake8-bugbear-20.11.1.tar.gz", hash = "sha256:528020129fea2dea33a466b9d64ab650aa3e5f9ffc788b70ea4bc6cf18283538"}, - {file = "flake8_bugbear-20.11.1-py36.py37.py38-none-any.whl", hash = "sha256:f35b8135ece7a014bc0aee5b5d485334ac30a6da48494998cc1fabf7ec70d703"}, + {file = "flake8-bugbear-21.4.3.tar.gz", hash = "sha256:2346c81f889955b39e4a368eb7d508de723d9de05716c287dc860a4073dc57e7"}, + {file = "flake8_bugbear-21.4.3-py36.py37.py38-none-any.whl", hash = "sha256:4f305dca96be62bf732a218fe6f1825472a621d3452c5b994d8f89dae21dbafa"}, ] flake8-commas = [ {file = "flake8-commas-2.0.0.tar.gz", hash = "sha256:d3005899466f51380387df7151fb59afec666a0f4f4a2c6a8995b975de0f44b7"}, @@ -1442,8 +1473,8 @@ flake8-docstrings = [ {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, ] flake8-eradicate = [ - {file = "flake8-eradicate-1.0.0.tar.gz", hash = "sha256:fe7167226676823d50cf540532302a6f576c5a398c5260692571a05ef72c5f5b"}, - {file = "flake8_eradicate-1.0.0-py3-none-any.whl", hash = "sha256:0fc4ab858a18c7ed630621b5345254c8f55be6060ea5c44a25e384d613618d1f"}, + {file = "flake8-eradicate-1.1.0.tar.gz", hash = "sha256:f5917d6dbca352efcd10c15fdab9c55c48f0f26f6a8d47898b25d39101f170a8"}, + {file = "flake8_eradicate-1.1.0-py3-none-any.whl", hash = "sha256:d8e39b684a37c257a53cda817d86e2d96c9ba3450ddc292742623a5dfee04d9e"}, ] flake8-isort = [ {file = "flake8-isort-4.0.0.tar.gz", hash = "sha256:2b91300f4f1926b396c2c90185844eb1a3d5ec39ea6138832d119da0a208f4d9"}, @@ -1477,8 +1508,8 @@ gitdb = [ {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, ] gitpython = [ - {file = "GitPython-3.1.17-py3-none-any.whl", hash = "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135"}, - {file = "GitPython-3.1.17.tar.gz", hash = "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"}, + {file = "GitPython-3.1.18-py3-none-any.whl", hash = "sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8"}, + {file = "GitPython-3.1.18.tar.gz", hash = "sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b"}, ] identify = [ {file = "identify-2.2.10-py2.py3-none-any.whl", hash = "sha256:18d0c531ee3dbc112fa6181f34faa179de3f57ea57ae2899754f16a7e0ff6421"}, @@ -1493,16 +1524,16 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.5.0-py3-none-any.whl", hash = "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00"}, - {file = "importlib_metadata-4.5.0.tar.gz", hash = "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139"}, + {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, + {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, - {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, + {file = "isort-5.9.2-py3-none-any.whl", hash = "sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813"}, + {file = "isort-5.9.2.tar.gz", hash = "sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e"}, ] jinja2 = [ {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, @@ -1557,8 +1588,8 @@ markupsafe = [ {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] marshmallow = [ - {file = "marshmallow-3.12.1-py2.py3-none-any.whl", hash = "sha256:b45cde981d1835145257b4a3c5cb7b80786dcf5f50dd2990749a50c16cb48e01"}, - {file = "marshmallow-3.12.1.tar.gz", hash = "sha256:8050475b70470cc58f4441ee92375db611792ba39ca1ad41d39cad193ea9e040"}, + {file = "marshmallow-3.12.2-py2.py3-none-any.whl", hash = "sha256:d4090ca9a36cd129126ad8b10c3982c47d4644a6e3ccb20534b512badce95f35"}, + {file = "marshmallow-3.12.2.tar.gz", hash = "sha256:77368dfedad93c3a041cbbdbce0b33fac1d8608c9e2e2288408a43ce3493d2ff"}, ] marshmallow-polyfield = [ {file = "marshmallow-polyfield-5.10.tar.gz", hash = "sha256:75d0e31b725650e91428f975a66ed30f703cc6f9fcfe45b8436ee6d676921691"}, @@ -1610,8 +1641,8 @@ nitpick = [ {file = "nitpick-0.26.0.tar.gz", hash = "sha256:b11009c77975990d7776ea6d307ed4272f5122e83b2fc16a6bad557222b6d809"}, ] packaging = [ - {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, - {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, + {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] pbr = [ {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, @@ -1621,6 +1652,10 @@ pep8-naming = [ {file = "pep8-naming-0.11.1.tar.gz", hash = "sha256:a1dd47dd243adfe8a83616e27cf03164960b507530f155db94e10b36a6cd6724"}, {file = "pep8_naming-0.11.1-py2.py3-none-any.whl", hash = "sha256:f43bfe3eea7e0d73e8b5d07d6407ab47f2476ccaeff6937c84275cd30b016738"}, ] +phantom-types = [ + {file = "phantom-types-0.9.1.tar.gz", hash = "sha256:83834b29d0b0ed18ce2b69830fad204b9479ebadc75e7ac4c7d33011e3b8d768"}, + {file = "phantom_types-0.9.1-py3-none-any.whl", hash = "sha256:9cc01385d8b027cac6df84f328dab84eea82f265f6e598f04fe3e9ff29d364da"}, +] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, @@ -1739,41 +1774,31 @@ restructuredtext-lint = [ {file = "restructuredtext_lint-1.3.2.tar.gz", hash = "sha256:d3b10a1fe2ecac537e51ae6d151b223b78de9fafdd50e5eb6b08c243df173c80"}, ] "ruamel.yaml" = [ - {file = "ruamel.yaml-0.17.9-py3-none-any.whl", hash = "sha256:8873a6f5516e0d848c92418b0b006519c0566b6cd0dcee7deb9bf399e2bd204f"}, - {file = "ruamel.yaml-0.17.9.tar.gz", hash = "sha256:374373b4743aee9f6d9f40bea600fe020a7ac7ae36b838b4a6a93f72b584a14c"}, + {file = "ruamel.yaml-0.17.10-py3-none-any.whl", hash = "sha256:ffb9b703853e9e8b7861606dfdab1026cf02505bade0653d1880f4b2db47f815"}, + {file = "ruamel.yaml-0.17.10.tar.gz", hash = "sha256:106bc8d6dc6a0ff7c9196a47570432036f41d556b779c6b4e618085f57e39e67"}, ] "ruamel.yaml.clib" = [ - {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:28116f204103cb3a108dfd37668f20abe6e3cafd0d3fd40dba126c732457b3cc"}, - {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:daf21aa33ee9b351f66deed30a3d450ab55c14242cfdfcd377798e2c0d25c9f1"}, - {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-win32.whl", hash = "sha256:30dca9bbcbb1cc858717438218d11eafb78666759e5094dd767468c0d577a7e7"}, - {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-win_amd64.whl", hash = "sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f"}, - {file = "ruamel.yaml.clib-0.2.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:73b3d43e04cc4b228fa6fa5d796409ece6fcb53a6c270eb2048109cbcbc3b9c2"}, - {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:53b9dd1abd70e257a6e32f934ebc482dac5edb8c93e23deb663eac724c30b026"}, - {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:839dd72545ef7ba78fd2aa1a5dd07b33696adf3e68fae7f31327161c1093001b"}, - {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1236df55e0f73cd138c0eca074ee086136c3f16a97c2ac719032c050f7e0622f"}, - {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-win32.whl", hash = "sha256:b1e981fe1aff1fd11627f531524826a4dcc1f26c726235a52fcb62ded27d150f"}, - {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e52c96ca66de04be42ea2278012a2342d89f5e82b4512fb6fb7134e377e2e62"}, - {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a873e4d4954f865dcb60bdc4914af7eaae48fb56b60ed6daa1d6251c72f5337c"}, - {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ab845f1f51f7eb750a78937be9f79baea4a42c7960f5a94dde34e69f3cce1988"}, - {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:2fd336a5c6415c82e2deb40d08c222087febe0aebe520f4d21910629018ab0f3"}, - {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-win32.whl", hash = "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2"}, - {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:2602e91bd5c1b874d6f93d3086f9830f3e907c543c7672cf293a97c3fabdcd91"}, - {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:44c7b0498c39f27795224438f1a6be6c5352f82cb887bc33d962c3a3acc00df6"}, - {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8e8fd0a22c9d92af3a34f91e8a2594eeb35cba90ab643c5e0e643567dc8be43e"}, - {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:75f0ee6839532e52a3a53f80ce64925ed4aed697dd3fa890c4c918f3304bd4f4"}, - {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-win32.whl", hash = "sha256:464e66a04e740d754170be5e740657a3b3b6d2bcc567f0c3437879a6e6087ff6"}, - {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:52ae5739e4b5d6317b52f5b040b1b6639e8af68a5b8fd606a8b08658fbd0cab5"}, - {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df5019e7783d14b79217ad9c56edf1ba7485d614ad5a385d1b3c768635c81c0"}, - {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5254af7d8bdf4d5484c089f929cb7f5bafa59b4f01d4f48adda4be41e6d29f99"}, - {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8be05be57dc5c7b4a0b24edcaa2f7275866d9c907725226cdde46da09367d923"}, - {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-win32.whl", hash = "sha256:74161d827407f4db9072011adcfb825b5258a5ccb3d2cd518dd6c9edea9e30f1"}, - {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:058a1cc3df2a8aecc12f983a48bda99315cebf55a3b3a5463e37bb599b05727b"}, - {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6ac7e45367b1317e56f1461719c853fd6825226f45b835df7436bb04031fd8a"}, - {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b4b0d31f2052b3f9f9b5327024dc629a253a83d8649d4734ca7f35b60ec3e9e5"}, - {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1f8c0a4577c0e6c99d208de5c4d3fd8aceed9574bb154d7a2b21c16bb924154c"}, - {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-win32.whl", hash = "sha256:46d6d20815064e8bb023ea8628cfb7402c0f0e83de2c2227a88097e239a7dffd"}, - {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:6c0a5dc52fc74eb87c67374a4e554d4761fd42a4d01390b7e868b30d21f4b8bb"}, - {file = "ruamel.yaml.clib-0.2.2.tar.gz", hash = "sha256:2d24bd98af676f4990c4d715bcdc2a60b19c56a3fb3a763164d2d8ca0e806ba7"}, + {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"}, + {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"}, + {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"}, + {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"}, + {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"}, + {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"}, + {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"}, + {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"}, + {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"}, + {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"}, + {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"}, + {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"}, + {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"}, + {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"}, + {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"}, + {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"}, + {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"}, + {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"}, + {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"}, + {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"}, + {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"}, ] safety = [ {file = "safety-1.10.3-py2.py3-none-any.whl", hash = "sha256:5f802ad5df5614f9622d8d71fedec2757099705c2356f862847c58c6dfe13e84"}, @@ -1886,14 +1911,18 @@ typed-ast = [ {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] +typeguard = [ + {file = "typeguard-2.12.1-py3-none-any.whl", hash = "sha256:cc15ef2704c9909ef9c80e19c62fb8468c01f75aad12f651922acf4dbe822e02"}, + {file = "typeguard-2.12.1.tar.gz", hash = "sha256:c2af8b9bdd7657f4bd27b45336e7930171aead796711bc4cfc99b4731bb9d051"}, +] typing-extensions = [ {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] urllib3 = [ - {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, - {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, + {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, + {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, ] wemake-python-styleguide = [ {file = "wemake-python-styleguide-0.15.3.tar.gz", hash = "sha256:8b89aedabae67b7b915908ed06c178b702068137c0d8afe1fb59cdc829cd2143"}, @@ -1904,6 +1933,6 @@ win32-setctime = [ {file = "win32_setctime-1.0.3.tar.gz", hash = "sha256:4e88556c32fdf47f64165a2180ba4552f8bb32c1103a2fafd05723a0bd42bd4b"}, ] zipp = [ - {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, - {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, + {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, + {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, ] diff --git a/pyproject.toml b/pyproject.toml index ab3bf1d..5fba78c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,3 +69,6 @@ doc8 = "^0.8" m2r2 = "^0.2" tomlkit = "^0.7" codespell = "^2.1" + +# Test deps: +phantom-types = { version = "^0.9", python = ">=3.8" } diff --git a/tests/test_suppots.py b/tests/test_supports.py similarity index 100% rename from tests/test_suppots.py rename to tests/test_supports.py