diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index 4f8f330..0000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-# These are supported funding model platforms
-
-patreon: sobolevn
diff --git a/.github/ISSUE_TEMPLATE/Bug.md b/.github/ISSUE_TEMPLATE/Bug.md
new file mode 100644
index 0000000..45557ce
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/Bug.md
@@ -0,0 +1,37 @@
+---
+name: Bug
+about: Create a report to help us improve
+labels: 'bug'
+---
+
+# Bug report
+
+
+
+## What's wrong
+
+
+
+## How is that should be
+
+
+
+
+
+## System information
+
+- `python` version:
+- `classes` version:
+- `mypy` version:
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..ed248ba
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,7 @@
+# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
+blank_issues_enabled: true # default
+contact_links:
+- name: >-
+ 💬 Telegram: @drypython
+ url: https://t.me/drypython
+ about: Chat with dry-python devs
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..4a15f2a
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,12 @@
+version: 2
+updates:
+- package-ecosystem: pip
+ directory: "/"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
+- package-ecosystem: github-actions
+ directory: "/"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..80f0c6d
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,40 @@
+# I have made things!
+
+
+
+## Checklist
+
+
+
+- [ ] I have double checked that there are no unrelated changes in this pull request (old patches, accidental config files, etc)
+- [ ] I have created at least one test case for the changes I have made
+- [ ] I have updated the documentation for the changes I have made
+- [ ] I have added my changes to the `CHANGELOG.md`
+
+## Related issues
+
+
+
+
+
+🙏 Please, if you or your company finds `dry-python` valuable, help us sustain the project by sponsoring it transparently on https://github.com/sponsors/dry-python. As a thank you, your profile/company logo will be added to our main README which receives hundreds of unique visitors per day.
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..41573ad
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,54 @@
+name: test
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ['3.7', '3.8', '3.9', '3.10']
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install poetry
+ run: |
+ curl -sSL \
+ "https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py" | python
+
+ # Adding `poetry` to `$PATH`:
+ echo "$HOME/.poetry/bin" >> $GITHUB_PATH
+
+ - name: Install dependencies
+ run: |
+ poetry config virtualenvs.in-project true
+ poetry run pip install -U pip
+ poetry install
+
+ - name: Run tests
+ run: |
+ poetry run flake8 .
+ poetry run mypy classes ./tests/**/*.py
+ poetry run codespell classes tests docs typesafety README.md CONTRIBUTING.md CHANGELOG.md
+ poetry run pytest classes tests docs/pages README.md
+ poetry run doc8 -q docs
+ poetry run poetry check
+ poetry run pip check
+ poetry run safety check --full-report
+ # We do this to speed up the build:
+ poetry run pytest typesafety -p no:cov -o addopts="" --mypy-ini-file=setup.cfg
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v3
+ with:
+ file: ./coverage.xml
diff --git a/.gitignore b/.gitignore
index 6a670ab..57cff0b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -198,3 +198,4 @@ fabric.properties
### Custom ###
ex.py
+experiments/
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..90d4adb
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,24 @@
+# .readthedocs.yml
+version: 2
+
+# Set the version of Python and other tools you might need
+build:
+ os: ubuntu-20.04
+ tools: {python: "3.9"}
+ jobs:
+ pre_create_environment:
+ - asdf plugin add poetry
+ - asdf install poetry latest
+ - asdf global poetry latest
+ - poetry export --with dev --format=requirements.txt --output=requirements.txt
+
+python:
+ install:
+ - requirements: requirements.txt
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+ configuration: docs/conf.py
+ fail_on_warning: true
+
+formats: all
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index a261cf0..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-language: python
-dist: xenial
-
-python:
- - 3.6
- - 3.7.3
-
-before_install:
- - pip freeze | xargs pip uninstall -y
- - curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python
- - source "$HOME/.poetry/env"
-
-install: poetry install
-
-script:
- - poetry run flake8 .
- - poetry run mypy classes tests/**/*.py
- - poetry run pytest . docs/pages
- - poetry run doc8 -q docs
- - poetry check
- - poetry run pip check
- - poetry run safety check --bare --full-report
-
-after_success:
- - pip install coveralls
- - coveralls
-
-notifications:
- email:
- on_success: never
- on_failure: change
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e05499d..dcd0921 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,68 @@
We follow Semantic Versions since the `0.1.0` release.
+## Version 0.4.1
+
+### Bugfixes
+
+- Fixes `typing_extensions` version resolution
+
+
+## Version 0.4.0
+
+### Features
+
+- **Breaking**: removes `is_protocol` boolean argument from `.instance`,
+ now use `protocol=YourProtocol` instead
+- Adds support for concrete generic types like `List[str]` and `Set[int]` #24
+- Adds support for types that have `__instancecheck__` defined
+ via `delegate` argument #248
+- Adds support for multiple type arguments in `Supports` type #244
+
+### Bugfixes
+
+- Fixes that types referenced in multiple typeclasses
+ were not handling `Supports` properly #249
+- Fixes typing bug with `ABC` and mutable typeclass signature #259
+- Fixes that `mypy` plugin was failing
+ on calling a typeclass without arguments #270
+
+
+## Version 0.3.0
+
+### Features
+
+- **Breaking**: drops `python3.6` support
+- **Breaking**: now requires `typing_extensions>=3.10` and `mypy>=0.902`
+- **Breaking**: now `classes` traverses `mro` of registered types
+ and fallbacks to super-types if some type is not registered
+- Adds generic typeclasses
+- Adds caching to runtime type dispatch,
+ it allows to call already resolved instances way faster
+- Adds better typeclass validation during `mypy` typechecking
+- Adds `.supports()` method to typeclass to check
+ if some instance is supported in runtime
+- Makes `.supports()` a typeguard
+- Adds `Supports` type
+- Adds `AssociatedType` variadic type
+
+### Misc
+
+- Improves docs
+
+
+## Version 0.2.0
+
+### Features
+
+- **Breaking**: renames mypy `typeclass_plugin` to `classes_plugin`
+- Adds `python3.9` support
+
+### Misc
+
+- Updates dependencies
+
+
## Version 0.1.0
- Initial release
diff --git a/LICENSE b/LICENSE
index 2fb40ca..fa8c538 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2016-2019 dry-python organization
+Copyright 2016-2021 dry-python organization
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
diff --git a/README.md b/README.md
index a66ce0b..8a5fc0f 100644
--- a/README.md
+++ b/README.md
@@ -5,11 +5,11 @@
-----
[](https://travis-ci.org/dry-python/classes)
-[](https://coveralls.io/github/dry-python/classes?branch=master)
+[](https://codecov.io/gh/dry-python/classes)
[](https://classes.readthedocs.io/en/latest/?badge=latest)
[](https://pypi.org/project/classes/)
[](https://github.com/wemake-services/wemake-python-styleguide)
-[](http://mypy-lang.org/)
+[](https://t.me/drypython)
-----
@@ -33,14 +33,13 @@ pip install classes
```
You also need to [configure](https://classes.readthedocs.io/en/latest/pages/container.html#type-safety)
-`mypy` correctly and install our plugin
-to fix [this existing issue](https://github.com/python/mypy/issues/3157):
+`mypy` correctly and install our plugin:
```ini
# In setup.cfg or mypy.ini:
[mypy]
plugins =
- classes.contrib.mypy.typeclass_plugin
+ classes.contrib.mypy.classes_plugin
```
**Without this step**, your project will report type-violations here and there.
@@ -52,11 +51,11 @@ Make sure you know how to get started, [check out our docs](https://classes.read
## Example
-Imagine, that you want to bound implementation to some particular type.
+Imagine, that you want to bind implementation to some particular type.
Like, strings behave like this, numbers behave like that, and so on.
The good realworld example is `djangorestframework`.
-It is build around the idea that different
+It is built around the idea that different
data types should be converted differently to and from `json` format.
What is the "traditional" (or outdated if you will!) approach?
@@ -75,10 +74,11 @@ class IntField(Field):
It literally has a lot of problems:
-- It is hard to type this code. How can I be sure that my `json` will be parsed by the given schema?
-- It contains a lot of boilerplate
-- It has complex API: there are usually several methods to override, some fields to adjust. Moreover, we use a class, not a callable
+- It is hard to type this code. How can I be sure that my `json` is parseable by the given schema?
+- It produces a lot of boilerplate
+- It has complex API: there are usually several methods to override, some fields to adjust. Moreover, we use a class, not a simple function
- It is hard to extend the default library for new custom types you will have in your own project
+- It is hard to override
There should be a better way of solving this problem!
And typeclasses are a better way!
@@ -88,29 +88,29 @@ How would new API look like with this concept?
```python
>>> from typing import Union
>>> from classes import typeclass
+
>>> @typeclass
... def to_json(instance) -> str:
-... """This is a typeclass definition to covert things to json."""
-...
+... """This is a typeclass definition to convert things to json."""
+
>>> @to_json.instance(int)
... @to_json.instance(float)
... def _to_json_int(instance: Union[int, float]) -> str:
... return str(instance)
-...
+
>>> @to_json.instance(bool)
... def _to_json_bool(instance: bool) -> str:
... return 'true' if instance else 'false'
-...
+
>>> @to_json.instance(list)
... def _to_json_list(instance: list) -> str:
... return '[{0}]'.format(
... ', '.join(to_json(list_item) for list_item in instance),
... )
-...
```
-See how easy it is to works with types and implementation?
+See how easy it is to work with types and implementation?
Typeclass is represented as a regular function, so you can use it like one:
@@ -119,30 +119,38 @@ Typeclass is represented as a regular function, so you can use it like one:
'true'
>>> to_json(1)
'1'
->>> to_json([False, 1, 2])
-'[false, 1, 2]'
+>>> to_json([False, 1, 2.5])
+'[false, 1, 2.5]'
```
-And it easy to extend this typeclass with your own classes as well:
+And it is easy to extend this typeclass with your own classes as well:
```python
->>> # Pretending to import the existing library from somewhere:
->>> # from to_json import to_json
+# Pretending to import the existing library from somewhere:
+# from to_json import to_json
+
>>> import datetime as dt
+
>>> @to_json.instance(dt.datetime)
... def _to_json_datetime(instance: dt.datetime) -> str:
... return instance.isoformat()
-...
+
>>> to_json(dt.datetime(2019, 10, 31, 12, 28, 00))
'2019-10-31T12:28:00'
```
That's how simple, safe, and powerful typeclasses are!
-Make sure to [check out our docs](https://github.com/dry-python/classes) to learn more.
+Make sure to [check out our full docs](https://classes.readthedocs.io) to learn more.
+
+
+## More!
+Want more? [Go to the docs!](https://classes.readthedocs.io) Or read these articles:
+- [Typeclasses in Python](https://sobolevn.me/2021/06/typeclasses-in-python)
-## License
-BSD 2-Clause
+
— ⭐️ —
+Drylabs maintains dry-python and helps those who want to use it inside their organizations.
+Read more in our Telegram group
diff --git a/classes/__init__.py b/classes/__init__.py
index 23c6837..98bc4a1 100644
--- a/classes/__init__.py
+++ b/classes/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
Here we define the public API of our module.
@@ -7,4 +5,6 @@
so mypy's ``implicit_reexport`` rule will be happy.
"""
-from classes.typeclass import typeclass as typeclass
+from classes._typeclass import AssociatedType as AssociatedType
+from classes._typeclass import Supports as Supports
+from classes._typeclass import typeclass as typeclass
diff --git a/classes/_registry.py b/classes/_registry.py
new file mode 100644
index 0000000..4bc2cf5
--- /dev/null
+++ b/classes/_registry.py
@@ -0,0 +1,58 @@
+from typing import Callable, Dict, NoReturn, Optional, Tuple
+
+from typing_extensions import Final
+
+TypeRegistry = Dict[type, Callable]
+
+#: We use this to exclude `None` as a default value for `exact_type`.
+DefaultValue: Final = type('DefaultValueType', (object,), {})
+
+#: Used both in runtime and during mypy type-checking.
+INVALID_ARGUMENTS_MSG: Final = (
+ 'Only a single argument can be applied to `.instance`'
+)
+
+
+def choose_registry( # noqa: WPS211
+ # 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.
+ exact_type: Optional[type],
+ protocol: type,
+ delegate: type,
+ delegates: TypeRegistry,
+ exact_types: TypeRegistry,
+ protocols: TypeRegistry,
+) -> Tuple[TypeRegistry, type]:
+ """
+ Returns the appropriate registry to store the passed type.
+
+ It depends on how ``instance`` method is used and also on the type itself.
+ """
+ passed_args = list(filter(
+ _is_not_default_argument_value,
+ (exact_type, protocol, delegate),
+ ))
+ if not passed_args:
+ raise ValueError('At least one argument to `.instance` is required')
+ if len(passed_args) > 1:
+ raise ValueError(INVALID_ARGUMENTS_MSG)
+
+ if _is_not_default_argument_value(delegate):
+ return delegates, delegate
+ elif _is_not_default_argument_value(protocol):
+ return protocols, protocol
+ return exact_types, exact_type if exact_type is not None else type(None)
+
+
+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__,
+ ),
+ )
+
+
+def _is_not_default_argument_value(arg: Optional[type]) -> bool:
+ return arg is not DefaultValue
diff --git a/classes/_typeclass.py b/classes/_typeclass.py
new file mode 100644
index 0000000..aa5f2d8
--- /dev/null
+++ b/classes/_typeclass.py
@@ -0,0 +1,655 @@
+"""
+Typeclasses for Python.
+
+.. rubric:: Basic usage
+
+The first and the simplest example of a typeclass is just its definition:
+
+.. code:: python
+
+ >>> from classes import typeclass
+
+ >>> @typeclass
+ ... def example(instance) -> str:
+ ... '''Example typeclass.'''
+
+ >>> example(1)
+ Traceback (most recent call last):
+ ...
+ NotImplementedError: Missing matched typeclass instance for type: int
+
+In this example we work with the default implementation of a typeclass.
+It raises a ``NotImplementedError`` when no instances match.
+And we don't yet have a special case for ``int``,
+that why we fallback to the default implementation.
+
+It works almost like a regular function right now.
+Let's do the next step and introduce
+the ``int`` instance for our typeclass:
+
+.. code:: python
+
+ >>> @example.instance(int)
+ ... def _example_int(instance: int) -> str:
+ ... return 'int case'
+
+ >>> assert example(1) == 'int case'
+
+Now we have a specific instance for ``int``
+which does something different from the default implementation.
+
+What will happen if we pass something new, like ``str``?
+
+.. code:: python
+
+ >>> example('a')
+ Traceback (most recent call last):
+ ...
+ NotImplementedError: Missing matched typeclass instance for type: str
+
+Because again, we don't yet have
+an instance of this typeclass for ``str`` type.
+Let's fix that.
+
+.. code:: python
+
+ >>> @example.instance(str)
+ ... def _example_str(instance: str) -> str:
+ ... return instance
+
+ >>> assert example('a') == 'a'
+
+Now it works with ``str`` as well. But differently.
+This allows developer to base the implementation on type information.
+
+So, the rule is clear:
+if we have a typeclass instance for a specific type,
+then it will be called,
+otherwise the default implementation will be called instead.
+
+.. rubric:: Protocols
+
+We also support protocols. It has the same limitation as ``Generic`` types.
+It is also dispatched after all regular instances are checked.
+
+To work with protocols, one needs
+to pass ``protocol`` named argument to instance:
+
+.. code:: python
+
+ >>> from typing import Sequence
+
+ >>> @example.instance(protocol=Sequence)
+ ... def _sequence_example(instance: Sequence) -> str:
+ ... return ','.join(str(item) for item in instance)
+
+ >>> assert example([1, 2, 3]) == '1,2,3'
+
+But, ``str`` will still have higher priority over ``Sequence``:
+
+.. code:: python
+
+ >>> assert example('abc') == 'abc'
+
+We also support user-defined protocols:
+
+.. code:: python
+
+ >>> from typing_extensions import Protocol, runtime_checkable
+
+ >>> @runtime_checkable
+ ... class CustomProtocol(Protocol):
+ ... field: str
+
+ >>> @example.instance(protocol=CustomProtocol)
+ ... def _custom_protocol_example(instance: CustomProtocol) -> str:
+ ... return instance.field
+
+Now, let's build a class that match this protocol and test it:
+
+.. code:: python
+
+ >>> class WithField(object):
+ ... field: str = 'with field'
+
+ >>> assert example(WithField()) == 'with field'
+
+See our `official docs `_ to learn more!
+"""
+from functools import _find_impl # type: ignore # noqa: WPS450
+from typing import ( # noqa: WPS235
+ TYPE_CHECKING,
+ Callable,
+ Dict,
+ Generic,
+ Optional,
+ Type,
+ TypeVar,
+ Union,
+ overload,
+)
+from weakref import WeakKeyDictionary
+
+from typing_extensions import TypeGuard, final
+
+from classes._registry import (
+ DefaultValue,
+ TypeRegistry,
+ choose_registry,
+ default_implementation,
+)
+
+_InstanceType = TypeVar('_InstanceType')
+_SignatureType = TypeVar('_SignatureType', bound=Callable)
+_AssociatedType = TypeVar('_AssociatedType')
+_Fullname = TypeVar('_Fullname', bound=str) # Literal value
+
+_NewInstanceType = TypeVar('_NewInstanceType', bound=Type)
+
+_AssociatedTypeDef = TypeVar('_AssociatedTypeDef', contravariant=True)
+_TypeClassType = TypeVar('_TypeClassType', bound='_TypeClass')
+_ReturnType = TypeVar('_ReturnType')
+
+
+@overload
+def typeclass(
+ definition: Type[_AssociatedType],
+) -> '_TypeClassDef[_AssociatedType]':
+ """Function to created typeclasses with associated types."""
+
+
+@overload
+def typeclass(
+ signature: _SignatureType,
+ # By default almost all variables are `nothing`,
+ # but we enhance them via mypy plugin later:
+) -> '_TypeClass[_InstanceType, _SignatureType, _AssociatedType, _Fullname]':
+ """Function to define typeclasses with just functions."""
+
+
+def typeclass(signature):
+ """General case function to create typeclasses."""
+ if isinstance(signature, type):
+ # It means, that it has a associated type with it:
+ return lambda func: _TypeClass(func, associated_type=signature)
+ return _TypeClass(signature) # In this case it is a regular function
+
+
+class AssociatedType(Generic[_InstanceType]):
+ """
+ Base class for all associated types.
+
+ How to use? Just import and subclass it:
+
+ .. code:: python
+
+ >>> from classes import AssociatedType, typeclass
+
+ >>> class Example(AssociatedType):
+ ... ...
+
+ >>> @typeclass(Example)
+ ... def example(instance) -> str:
+ ... ...
+
+ It is special, since it can be used as variadic generic type
+ (generic with any amount of type variables),
+ thanks to our ``mypy`` plugin:
+
+ .. code:: python
+
+ >>> from typing import TypeVar
+
+ >>> A = TypeVar('A')
+ >>> B = TypeVar('B')
+ >>> C = TypeVar('C')
+
+ >>> class WithOne(AssociatedType[A]):
+ ... ...
+
+ >>> class WithTwo(AssociatedType[A, B]):
+ ... ...
+
+ >>> class WithThree(AssociatedType[A, B, C]):
+ ... ...
+
+ At the moment of writing,
+ https://www.python.org/dev/peps/pep-0646/
+ is not accepted and is not supported by ``mypy``.
+
+ Right now it does nothing in runtime, but this can change in the future.
+ """
+
+ if not TYPE_CHECKING: # noqa: WPS604 # pragma: no cover
+ __slots__ = ()
+
+ def __class_getitem__(cls, type_params) -> type:
+ """
+ Not-so-ugly hack to add variadic generic support in runtime.
+
+ What it does?
+ It forces class-level type ``__parameters__`` count
+ and the passed one during runtime subscruption
+ to match during validation.
+
+ Then, we revert everything back.
+ """
+ if not isinstance(type_params, tuple):
+ type_params = (type_params,) # noqa: WPS434
+
+ old_parameters = cls.__parameters__
+ cls.__parameters__ = type_params
+ try: # noqa: WPS501
+ return super().__class_getitem__(type_params)
+ finally:
+ cls.__parameters__ = old_parameters
+
+
+@final
+class Supports(Generic[_AssociatedTypeDef]):
+ """
+ Used to specify that some value is a part of a typeclass.
+
+ For example:
+
+ .. code:: python
+
+ >>> from classes import typeclass, Supports
+
+ >>> class ToJson(object):
+ ... ...
+
+ >>> @typeclass(ToJson)
+ ... def to_json(instance) -> str:
+ ... ...
+
+ >>> @to_json.instance(int)
+ ... def _to_json_int(instance: int) -> str:
+ ... return str(instance)
+
+ >>> def convert_to_json(instance: Supports[ToJson]) -> str:
+ ... return to_json(instance)
+
+ >>> assert convert_to_json(1) == '1'
+ >>> convert_to_json(None)
+ Traceback (most recent call last):
+ ...
+ NotImplementedError: Missing matched typeclass instance for type: NoneType
+
+ You can also annotate values as ``Supports`` if you need to:
+
+ .. code:: python
+
+ >>> my_int: Supports[ToJson] = 1
+
+ But, this will fail in ``mypy``:
+
+ .. code:: python
+
+ my_str: Supports[ToJson] = 'abc'
+ # Incompatible types in assignment
+ # (expression has type "str", variable has type "Supports[ToJson]")
+
+ .. warning::
+ ``Supports`` only works with typeclasses defined with associated types.
+
+ """
+
+ __slots__ = ()
+
+
+@final # noqa: WPS214
+class _TypeClass( # noqa: WPS214
+ Generic[_InstanceType, _SignatureType, _AssociatedType, _Fullname],
+):
+ """
+ That's how we represent typeclasses.
+
+ You probably don't need to use this type directly,
+ use its public methods and public :func:`~typeclass` constructor.
+ """
+
+ __slots__ = (
+ # Str:
+ '_signature',
+ '_associated_type',
+
+ # Registry:
+ '_delegates',
+ '_exact_types',
+ '_protocols',
+
+ # Cache:
+ '_dispatch_cache',
+ )
+
+ _dispatch_cache: Dict[type, Callable]
+ _cache_token: Optional[object]
+
+ def __init__(
+ self,
+ signature: _SignatureType,
+ associated_type=None,
+ ) -> None:
+ """
+ Protected constructor of the typeclass.
+
+ Use public :func:`~typeclass` constructor instead.
+
+ How does this magic work? It heavily relies on custom ``mypy`` plugin.
+ Without it - it is just a nonsense.
+
+ The logic is quite unusual.
+ We use "mypy-plugin-time" variables to construct a typeclass.
+
+ What variables we use and why?
+
+ - ``_TypeclassType`` is a type variable that indicates
+ what type can be passed into this typeclass.
+ This type is updated each time we call ``.instance``,
+ because that how we introduce new types to the typeclass
+
+ - ``_ReturnType`` is used to enforce
+ the same return type for all cases.
+ Only modified once during ``@typeclass`` creation
+
+ - ``_SignatureType`` is used to ensure that all parameters
+ for all type cases are the same.
+ That's how we enforce consistency in all function signatures.
+ The only exception is the first argument: it is polymorfic.
+
+ """
+ # We need this for `repr`:
+ self._signature = signature
+ self._associated_type = associated_type
+
+ # Registries:
+ self._delegates: TypeRegistry = {}
+ self._exact_types: TypeRegistry = {}
+ self._protocols: TypeRegistry = {}
+
+ # Cache parts:
+ self._dispatch_cache = WeakKeyDictionary() # type: ignore
+
+ def __call__(
+ self,
+ instance: Union[ # type: ignore
+ _InstanceType,
+ Supports[_AssociatedType],
+ ],
+ *args,
+ **kwargs,
+ ) -> _ReturnType:
+ """
+ We use this method to actually call a typeclass.
+
+ The resolution order is the following:
+
+ 1. Delegates passed with ``delegate=``
+ 2. Exact types that are passed as ``.instance`` arguments
+ 3. Protocols that are passed with ``protocol=``
+
+ We don't guarantee the order of types inside groups.
+ Use correct types, do not rely on our order.
+
+ .. rubric:: Callbacks
+
+ Since, we define ``__call__`` method for this class,
+ it can be used and typechecked everywhere,
+ where a regular ``Callable`` is expected.
+
+ .. code:: python
+
+ >>> from typing import Callable
+ >>> from classes import typeclass
+
+ >>> @typeclass
+ ... def used(instance, other: int) -> int:
+ ... '''Example typeclass to be used later.'''
+
+ >>> @used.instance(int)
+ ... def _used_int(instance: int, other: int) -> int:
+ ... return instance + other
+
+ >>> def accepts_typeclass(
+ ... callback: Callable[[int, int], int],
+ ... ) -> int:
+ ... return callback(1, 3)
+
+ >>> assert accepts_typeclass(used) == 4
+
+ Take a note, that we use structural subtyping here.
+ And all typeclasses that match ``Callable[[int, int], int]`` signature
+ will typecheck.
+ """
+ # At first, we try all our delegate types,
+ # we don't cache it, because it is impossible.
+ # We only have runtime type info: `type([1]) == type(['a'])`.
+ # It might be slow!
+ # Don't add any delegate types unless
+ # you are absolutely know what you are doing.
+ impl = self._dispatch_delegate(instance)
+ if impl is not None:
+ return impl(instance, *args, **kwargs)
+
+ instance_type = type(instance)
+
+ try:
+ impl = self._dispatch_cache[instance_type]
+ except KeyError:
+ impl = self._dispatch(
+ instance,
+ instance_type,
+ ) or default_implementation
+ self._dispatch_cache[instance_type] = impl
+ return impl(instance, *args, **kwargs)
+
+ def __str__(self) -> str:
+ """Converts typeclass to a string."""
+ associated_type = (
+ ': "{0}"'.format(self._associated_type.__qualname__)
+ if self._associated_type
+ else ''
+ )
+ return ''.format(
+ self._signature.__name__,
+ associated_type,
+ )
+
+ def supports(
+ self,
+ instance,
+ ) -> TypeGuard[_InstanceType]:
+ """
+ Tells whether a typeclass is supported by a given type.
+
+ .. code:: python
+
+ >>> from classes import typeclass
+
+ >>> @typeclass
+ ... def example(instance) -> str:
+ ... '''Example typeclass.'''
+
+ >>> @example.instance(int)
+ ... def _example_int(instance: int) -> str:
+ ... return 'Example: {0}'.format(instance)
+
+ >>> assert example.supports(1) is True
+ >>> assert example.supports('a') is False
+
+ It also works with protocols:
+
+ .. code:: python
+
+ >>> from typing import Sized
+
+ >>> @example.instance(protocol=Sized)
+ ... def _example_sized(instance: Sized) -> str:
+ ... return 'Size is {0}'.format(len(instance))
+
+ >>> assert example.supports([1, 2]) is True
+ >>> assert example([1, 2]) == 'Size is 2'
+
+ We also use new ``TypeGuard`` type to ensure
+ that type is narrowed when ``.supports()`` is used:
+
+ .. code:: python
+
+ some_var: Any
+ if my_typeclass.supports(some_var):
+ reveal_type(some_var) # Revealed type is 'Supports[MyTypeclass]'
+
+ See also: https://www.python.org/dev/peps/pep-0647
+ """
+ # Here we first check that instance is already in the cache
+ # and only then we check delegate types.
+ # Why?
+ # Because if some type is already in the cache,
+ # it means that it is not a delegate.
+ # So, this is simply faster.
+ instance_type = type(instance)
+ if instance_type in self._dispatch_cache:
+ return True
+
+ # We never cache delegate types.
+ if self._dispatch_delegate(instance) is not None:
+ return True
+
+ # This only happens when we don't have a cache in place
+ # and this is not a delegate type:
+ impl = self._dispatch(instance, instance_type)
+ if impl is None:
+ return False
+
+ self._dispatch_cache[instance_type] = impl
+ return True
+
+ def instance(
+ self,
+ exact_type: Optional[_NewInstanceType] = DefaultValue, # type: ignore
+ *,
+ protocol: type = DefaultValue,
+ delegate: type = DefaultValue,
+ ) -> '_TypeClassInstanceDef[_NewInstanceType, _TypeClassType]':
+ """
+ We use this method to store implementation for each specific type.
+
+ Args:
+ protocol: required when passing protocols.
+ delegate: required when using delegate types, for example,
+ when working with concrete generics like ``List[str]``.
+
+ Returns:
+ Decorator for instance handler.
+
+ .. note::
+
+ ``exact_type``, ``protocol``, and ``delegate``
+ are mutually exclusive. Only one argument can be passed.
+
+ 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.
+ """
+ # 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.
+ registry, typ = choose_registry(
+ exact_type=exact_type,
+ protocol=protocol,
+ delegate=delegate,
+ exact_types=self._exact_types,
+ protocols=self._protocols,
+ delegates=self._delegates,
+ )
+
+ # That's how we check for generics,
+ # generics that look like `List[int]` or `set[T]` will fail this check,
+ # because they are `_GenericAlias` instance,
+ # which raises an exception for `__isinstancecheck__`
+ isinstance(object(), typ)
+
+ def decorator(implementation):
+ registry[typ] = implementation
+ self._dispatch_cache.clear()
+ return implementation
+ return decorator
+
+ def _dispatch(self, instance, instance_type: type) -> Optional[Callable]:
+ """
+ Dispatches a function by its type.
+
+ How do we dispatch a function?
+ 1. By direct ``instance`` types
+ 2. By matching protocols
+ 3. By its ``mro``
+ """
+ implementation = self._exact_types.get(instance_type, None)
+ if implementation is not None:
+ return implementation
+
+ for protocol, callback in self._protocols.items():
+ if isinstance(instance, protocol):
+ return callback
+
+ return _find_impl(instance_type, self._exact_types)
+
+ def _dispatch_delegate(self, instance) -> Optional[Callable]:
+ for delegate, callback in self._delegates.items():
+ if isinstance(instance, delegate):
+ return callback
+ return None
+
+
+if TYPE_CHECKING:
+ from typing_extensions import Protocol
+
+ class _TypeClassDef(Protocol[_AssociatedType]):
+ """
+ Callable protocol to help us with typeclass definition.
+
+ This protocol does not exist in real life,
+ we just need it because we use it in ``mypy`` plugin.
+ That's why we define it under ``if TYPE_CHECKING:``.
+ It should not be used directly.
+
+ See ``TypeClassDefReturnType`` for more information.
+ """
+
+ def __call__(
+ self,
+ signature: _SignatureType,
+ ) -> _TypeClass[
+ _InstanceType,
+ _SignatureType,
+ _AssociatedType,
+ _Fullname,
+ ]:
+ """It can be called, because in real life it is a function."""
+
+ class _TypeClassInstanceDef( # type: ignore
+ Protocol[_InstanceType, _TypeClassType],
+ ):
+ """
+ Callable protocol to help us with typeclass instance callbacks.
+
+ This protocol does not exist in real life,
+ we just need it because we use it in ``mypy`` plugin.
+ That's why we define it under ``if TYPE_CHECKING:``.
+ It should not be used directly.
+
+ See ``InstanceDefReturnType`` for more information.
+
+ One more important thing here: we fill its type vars inside our plugin,
+ so, don't even care about its definition.
+ """
+
+ def __call__(self, callback: _SignatureType) -> _SignatureType:
+ """It can be called, because in real life it is a function."""
diff --git a/classes/contrib/mypy/classes_plugin.py b/classes/contrib/mypy/classes_plugin.py
new file mode 100644
index 0000000..20b405b
--- /dev/null
+++ b/classes/contrib/mypy/classes_plugin.py
@@ -0,0 +1,109 @@
+"""
+Custom mypy plugin to enable typeclass concept to work.
+
+Features:
+
+- We return a valid ``typeclass`` generic instance
+ from ``@typeclass`` constructor
+- We force ``.instance()`` calls to extend the union of allowed types
+- We ensure that when calling the typeclass'es function
+ we know what values can be used as inputs
+
+``mypy`` API docs are here:
+https://mypy.readthedocs.io/en/latest/extending_mypy.html
+
+We use ``pytest-mypy-plugins`` to test that it works correctly, see:
+https://github.com/TypedDjango/pytest-mypy-plugins
+
+"""
+
+from typing import Callable, Optional, Type
+
+from mypy.plugin import (
+ AnalyzeTypeContext,
+ FunctionContext,
+ MethodContext,
+ MethodSigContext,
+ Plugin,
+)
+from mypy.types import CallableType
+from mypy.types import Type as MypyType
+from typing_extensions import Final, final
+
+from classes.contrib.mypy.features import associated_type, supports, typeclass
+
+_ASSOCIATED_TYPE_FULLNAME: Final = 'classes._typeclass.AssociatedType'
+_TYPECLASS_FULLNAME: Final = 'classes._typeclass._TypeClass'
+_TYPECLASS_DEF_FULLNAME: Final = 'classes._typeclass._TypeClassDef'
+_TYPECLASS_INSTANCE_DEF_FULLNAME: Final = (
+ 'classes._typeclass._TypeClassInstanceDef'
+)
+
+
+@final
+class _TypeClassPlugin(Plugin):
+ """
+ Our plugin for typeclasses.
+
+ It has four steps:
+ - Creating typeclasses via ``typeclass`` function
+ - Adding cases for typeclasses via ``.instance()`` calls with explicit types
+ - Adding callbacks functions after the ``.instance()`` decorator
+ - Converting typeclasses to simple callable via ``__call__`` method
+
+ Hooks are in the logical order.
+ """
+
+ def get_type_analyze_hook(
+ self,
+ fullname: str,
+ ) -> Optional[Callable[[AnalyzeTypeContext], MypyType]]:
+ """Hook that works on type analyzer phase."""
+ if fullname == _ASSOCIATED_TYPE_FULLNAME:
+ return associated_type.variadic_generic
+ if fullname == 'classes._typeclass.Supports':
+ associated_type_node = self.lookup_fully_qualified(
+ _ASSOCIATED_TYPE_FULLNAME,
+ )
+ assert associated_type_node
+ return supports.VariadicGeneric(associated_type_node)
+ return None
+
+ def get_function_hook(
+ self,
+ fullname: str,
+ ) -> Optional[Callable[[FunctionContext], MypyType]]:
+ """Here we adjust the typeclass constructor."""
+ if fullname == 'classes._typeclass.typeclass':
+ return typeclass.TypeClassReturnType(
+ typeclass=_TYPECLASS_FULLNAME,
+ typeclass_def=_TYPECLASS_DEF_FULLNAME,
+ )
+ return None
+
+ def get_method_hook(
+ self,
+ fullname: str,
+ ) -> Optional[Callable[[MethodContext], MypyType]]:
+ """Here we adjust the typeclass with new allowed types."""
+ if fullname == '{0}.__call__'.format(_TYPECLASS_DEF_FULLNAME):
+ return typeclass.TypeClassDefReturnType(_ASSOCIATED_TYPE_FULLNAME)
+ if fullname == '{0}.__call__'.format(_TYPECLASS_INSTANCE_DEF_FULLNAME):
+ return typeclass.InstanceDefReturnType()
+ if fullname == '{0}.instance'.format(_TYPECLASS_FULLNAME):
+ return typeclass.instance_return_type
+ return None
+
+ def get_method_signature_hook(
+ self,
+ fullname: str,
+ ) -> Optional[Callable[[MethodSigContext], CallableType]]:
+ """Here we fix the calling method types to accept only valid types."""
+ if fullname == '{0}.__call__'.format(_TYPECLASS_FULLNAME):
+ return typeclass.call_signature
+ return None
+
+
+def plugin(version: str) -> Type[Plugin]:
+ """Plugin's public API and entrypoint."""
+ return _TypeClassPlugin
diff --git a/docs/_static/.gitkeep b/classes/contrib/mypy/features/__init__.py
similarity index 100%
rename from docs/_static/.gitkeep
rename to classes/contrib/mypy/features/__init__.py
diff --git a/classes/contrib/mypy/features/associated_type.py b/classes/contrib/mypy/features/associated_type.py
new file mode 100644
index 0000000..b398d73
--- /dev/null
+++ b/classes/contrib/mypy/features/associated_type.py
@@ -0,0 +1,11 @@
+from mypy.plugin import AnalyzeTypeContext
+from mypy.types import Type as MypyType
+
+from classes.contrib.mypy.semanal.variadic_generic import (
+ analize_variadic_generic,
+)
+
+
+def variadic_generic(ctx: AnalyzeTypeContext) -> MypyType:
+ """Variadic generic support for ``AssociatedType`` type."""
+ return analize_variadic_generic(ctx)
diff --git a/classes/contrib/mypy/features/supports.py b/classes/contrib/mypy/features/supports.py
new file mode 100644
index 0000000..01d5378
--- /dev/null
+++ b/classes/contrib/mypy/features/supports.py
@@ -0,0 +1,46 @@
+from mypy.nodes import SymbolTableNode
+from mypy.plugin import AnalyzeTypeContext
+from mypy.types import Instance
+from mypy.types import Type as MypyType
+from mypy.types import UnionType
+from typing_extensions import final
+
+from classes.contrib.mypy.semanal.variadic_generic import (
+ analize_variadic_generic,
+)
+from classes.contrib.mypy.validation import validate_supports
+
+
+@final
+class VariadicGeneric(object):
+ """
+ Variadic generic support for ``Supports`` type.
+
+ We also need to validate that
+ all type args of ``Supports`` are subtypes of ``AssociatedType``.
+ """
+
+ __slots__ = ('_associated_type_node',)
+
+ def __init__(self, associated_type_node: SymbolTableNode) -> None:
+ """We need ``AssociatedType`` fullname here."""
+ self._associated_type_node = associated_type_node
+
+ def __call__(self, ctx: AnalyzeTypeContext) -> MypyType:
+ """Main entry point."""
+ analyzed_type = analize_variadic_generic(
+ validate_callback=self._validate,
+ ctx=ctx,
+ )
+ if isinstance(analyzed_type, Instance):
+ return analyzed_type.copy_modified(
+ args=[UnionType.make_union(analyzed_type.args)],
+ )
+ return analyzed_type
+
+ def _validate(self, instance: Instance, ctx: AnalyzeTypeContext) -> bool:
+ return validate_supports.check_type(
+ instance,
+ self._associated_type_node,
+ ctx,
+ )
diff --git a/classes/contrib/mypy/features/typeclass.py b/classes/contrib/mypy/features/typeclass.py
new file mode 100644
index 0000000..69b4ad0
--- /dev/null
+++ b/classes/contrib/mypy/features/typeclass.py
@@ -0,0 +1,291 @@
+from typing import Tuple
+
+from mypy.nodes import Decorator
+from mypy.plugin import FunctionContext, MethodContext, MethodSigContext
+from mypy.types import (
+ AnyType,
+ CallableType,
+ FunctionLike,
+ Instance,
+ LiteralType,
+ TupleType,
+)
+from mypy.types import Type as MypyType
+from mypy.types import TypeOfAny, UninhabitedType
+from typing_extensions import final
+
+from classes.contrib.mypy.typeops import (
+ call_signatures,
+ fallback,
+ instance_type_args,
+ mro,
+ type_loader,
+)
+from classes.contrib.mypy.typeops.instance_context import InstanceContext
+from classes.contrib.mypy.validation import (
+ validate_associated_type,
+ validate_instance,
+ validate_typeclass_def,
+)
+
+
+@final
+class TypeClassReturnType(object):
+ """
+ Adjust argument types when we define typeclasses via ``typeclass`` function.
+
+ It has two modes:
+ 1. As a decorator ``@typeclass``
+ 2. As a regular call with a class definition: ``typeclass(SomeProtocol)``
+
+ It also checks how typeclasses are defined.
+ """
+
+ __slots__ = ('_typeclass', '_typeclass_def')
+
+ def __init__(self, typeclass: str, typeclass_def: str) -> None:
+ """We pass exact type names as the context."""
+ self._typeclass = typeclass
+ self._typeclass_def = typeclass_def
+
+ def __call__(self, ctx: FunctionContext) -> MypyType:
+ """Main entry point."""
+ defn = ctx.arg_types[0][0]
+
+ is_typeclass_def = (
+ isinstance(ctx.default_return_type, Instance) and
+ ctx.default_return_type.type.fullname == self._typeclass_def and
+ isinstance(defn, FunctionLike) and
+ defn.is_type_obj()
+ )
+ is_typeclass = (
+ isinstance(ctx.default_return_type, Instance) and
+ ctx.default_return_type.type.fullname == self._typeclass and
+ isinstance(defn, CallableType) and
+ defn.definition
+ )
+
+ if is_typeclass_def:
+ assert isinstance(ctx.default_return_type, Instance)
+ assert isinstance(defn, FunctionLike)
+ return self._process_typeclass_def_return_type(
+ ctx.default_return_type,
+ defn,
+ ctx,
+ )
+ elif is_typeclass:
+ assert isinstance(ctx.default_return_type, Instance)
+ assert isinstance(defn, CallableType)
+ assert defn.definition
+
+ instance_type_args.mutate_typeclass_def(
+ typeclass=ctx.default_return_type,
+ definition_fullname=defn.definition.fullname,
+ ctx=ctx,
+ )
+
+ validate_typeclass_def.check_type(
+ typeclass=ctx.default_return_type,
+ ctx=ctx,
+ )
+
+ return ctx.default_return_type
+ return AnyType(TypeOfAny.from_error)
+
+ def _process_typeclass_def_return_type(
+ self,
+ typeclass_intermediate_def: Instance,
+ defn: FunctionLike,
+ ctx: FunctionContext,
+ ) -> MypyType:
+ type_info = defn.type_object()
+ instance = Instance(type_info, [])
+ typeclass_intermediate_def.args = (instance,)
+ return typeclass_intermediate_def
+
+
+@final
+class TypeClassDefReturnType(object):
+ """
+ Callback for cases like ``@typeclass(SomeType)``.
+
+ What it does? It works with the associated types.
+ It checks that ``SomeType`` is correct, modifies the current typeclass.
+ And returns it back.
+ """
+
+ __slots__ = ('_associated_type',)
+
+ def __init__(self, associated_type: str) -> None:
+ """We need ``AssociatedType`` fullname here."""
+ self._associated_type = associated_type
+
+ def __call__(self, ctx: MethodContext) -> MypyType:
+ """Main entry point."""
+ assert isinstance(ctx.default_return_type, Instance)
+ assert isinstance(ctx.context, Decorator)
+
+ instance_type_args.mutate_typeclass_def(
+ typeclass=ctx.default_return_type,
+ definition_fullname=ctx.context.func.fullname,
+ ctx=ctx,
+ )
+
+ validate_typeclass_def.check_type(
+ typeclass=ctx.default_return_type,
+ ctx=ctx,
+ )
+
+ if isinstance(ctx.default_return_type.args[2], Instance):
+ validate_associated_type.check_type(
+ associated_type=ctx.default_return_type.args[2],
+ associated_type_fullname=self._associated_type,
+ typeclass=ctx.default_return_type,
+ ctx=ctx,
+ )
+
+ return ctx.default_return_type
+
+
+def instance_return_type(ctx: MethodContext) -> MypyType:
+ """Adjusts the typing signature on ``.instance(type)`` call."""
+ 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_type_args.mutate_typeclass_instance_def(
+ ctx.default_return_type,
+ ctx=ctx,
+ typeclass=ctx.type,
+ passed_types=passed_types,
+ )
+ return ctx.default_return_type
+
+
+@final
+class InstanceDefReturnType(object):
+ """
+ Class to check how instance definition is created.
+
+ When it is called?
+ It is called on the second call of ``.instance(str)(callback)``.
+
+ We do a lot of stuff here:
+ 1. Typecheck usage correctness
+ 2. Adding new instance types to typeclass definition
+ 3. Adding ``Supports[]`` metadata
+
+ """
+
+ @fallback.error_to_any({
+ # TODO: later we can use a custom exception type for this:
+ KeyError: 'Typeclass cannot be loaded, it must be a global declaration',
+ })
+ def __call__(self, ctx: MethodContext) -> MypyType:
+ """Main entry point."""
+ assert isinstance(ctx.type, Instance)
+ assert isinstance(ctx.type.args[0], TupleType)
+ assert isinstance(ctx.type.args[1], Instance)
+
+ typeclass, fullname = self._load_typeclass(ctx.type.args[1], ctx)
+ assert isinstance(typeclass.args[1], CallableType)
+
+ instance_signature = ctx.arg_types[0][0]
+ if not isinstance(instance_signature, CallableType):
+ return ctx.default_return_type
+
+ instance_context = InstanceContext.build(
+ typeclass_signature=typeclass.args[1],
+ instance_signature=instance_signature,
+ passed_args=ctx.type.args[0],
+ associated_type=typeclass.args[2],
+ fullname=fullname,
+ ctx=ctx,
+ )
+ if not self._run_validation(instance_context):
+ return AnyType(TypeOfAny.from_error)
+
+ # If typeclass is checked, than it is safe to add new instance types:
+ self._add_new_instance_type(
+ typeclass=typeclass,
+ new_type=instance_context.instance_type,
+ ctx=ctx,
+ )
+ return ctx.default_return_type
+
+ def _load_typeclass(
+ self,
+ typeclass_ref: Instance,
+ ctx: MethodContext,
+ ) -> Tuple[Instance, str]:
+ assert isinstance(typeclass_ref.args[3], LiteralType)
+ assert isinstance(typeclass_ref.args[3].value, str)
+
+ typeclass = type_loader.load_typeclass(
+ fullname=typeclass_ref.args[3].value,
+ ctx=ctx,
+ )
+ assert isinstance(typeclass, Instance)
+ return typeclass, typeclass_ref.args[3].value
+
+ def _run_validation(self, instance_context: InstanceContext) -> bool:
+ # When delegate is passed, we use it instead of instance type.
+ # Why? Because `delegate` can repre
+ instance_or_delegate = (
+ instance_context.inferred_args.delegate
+ if instance_context.inferred_args.delegate is not None
+ else instance_context.instance_type
+ )
+ # We need to add `Supports` metadata before typechecking,
+ # because it will affect type hierarchies.
+ metadata = mro.MetadataInjector(
+ associated_type=instance_context.associated_type,
+ instance_type=instance_or_delegate,
+ ctx=instance_context.ctx,
+ )
+ metadata.add_supports_metadata()
+
+ is_proper_instance = validate_instance.check_type(instance_context)
+ if not is_proper_instance:
+ # Since the typeclass is not valid,
+ # we undo the metadata manipulation,
+ # otherwise we would spam with invalid `Supports[]` base types:
+ metadata.remove_supports_metadata()
+ return is_proper_instance
+
+ def _add_new_instance_type(
+ self,
+ typeclass: Instance,
+ new_type: MypyType,
+ ctx: MethodContext,
+ ) -> None:
+ typeclass.args = (
+ instance_type_args.add_unique(new_type, typeclass.args[0]),
+ *typeclass.args[1:],
+ )
+
+
+def call_signature(ctx: MethodSigContext) -> CallableType:
+ """Returns proper ``__call__`` signature of a typeclass."""
+ assert isinstance(ctx.type, Instance)
+
+ real_signature = ctx.type.args[1]
+ if not isinstance(real_signature, CallableType) or not ctx.args[0]:
+ return ctx.default_signature
+
+ passed_type = ctx.api.expr_checker.accept(ctx.args[0][0]) # type: ignore
+ return call_signatures.SmartCallSignature(
+ signature=real_signature,
+ instance_type=ctx.type.args[0],
+ associated_type=ctx.type.args[2],
+ ctx=ctx,
+ ).mutate_and_infer(passed_type)
diff --git a/classes/contrib/mypy/semanal/__init__.py b/classes/contrib/mypy/semanal/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/classes/contrib/mypy/semanal/variadic_generic.py b/classes/contrib/mypy/semanal/variadic_generic.py
new file mode 100644
index 0000000..7ba7034
--- /dev/null
+++ b/classes/contrib/mypy/semanal/variadic_generic.py
@@ -0,0 +1,35 @@
+from typing import Callable, Optional
+
+from mypy.plugin import AnalyzeTypeContext
+from mypy.types import Instance
+from mypy.types import Type as MypyType
+
+_ValidateCallback = Callable[[Instance, AnalyzeTypeContext], bool]
+
+
+def analize_variadic_generic(
+ ctx: AnalyzeTypeContext,
+ validate_callback: Optional[_ValidateCallback] = None,
+) -> MypyType:
+ """
+ Variadic generic support.
+
+ What is "variadic generic"?
+ It is a generic type with any amount of type variables.
+ Starting with 0 up to infinity.
+
+ We also conditionally validate types of passed arguments.
+ """
+ sym = ctx.api.lookup_qualified(ctx.type.name, ctx.context) # type: ignore
+ if not sym or not sym.node:
+ # This will happen if `Supports[IsNotDefined]` will be called.
+ return ctx.type
+
+ instance = Instance(
+ sym.node,
+ ctx.api.anal_array(ctx.type.args), # type: ignore
+ )
+
+ if validate_callback is not None:
+ validate_callback(instance, ctx)
+ return instance
diff --git a/classes/contrib/mypy/typeclass_plugin.py b/classes/contrib/mypy/typeclass_plugin.py
deleted file mode 100644
index a638ec3..0000000
--- a/classes/contrib/mypy/typeclass_plugin.py
+++ /dev/null
@@ -1,118 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-Custom mypy plugin to enable typeclass concept to work.
-
-Features:
-
-- We return a valid ``typeclass`` generic instance
- from ``@typeclass`` contructor
-- We force ``.instance()`` calls to extend the union of allowed types
-- We ensure that when calling the typeclass'es function
- we know what values can be used as inputs
-
-``mypy`` API docs are here:
-https://mypy.readthedocs.io/en/latest/extending_mypy.html
-
-We use ``pytest-mypy-plugins`` to test that it works correctly, see:
-https://github.com/TypedDjango/pytest-mypy-plugins
-
-"""
-
-from typing import Type
-
-from mypy.plugin import Plugin
-from mypy.types import AnyType, CallableType, TypeOfAny, TypeVarType, UnionType
-
-
-def _adjust_arguments(ctx):
- typeclass_def = ctx.default_return_type.args[2]
- if not isinstance(typeclass_def, CallableType):
- return ctx.default_return_type
-
- ctx.default_return_type.args = [
- typeclass_def.arg_types[0],
- typeclass_def.ret_type,
- ]
-
- # We use `Any` here to filter it later in `_add_new_type` method:
- typeclass_def.arg_types[0] = AnyType(TypeOfAny.unannotated)
- ctx.default_return_type.args.append(typeclass_def)
- return ctx.default_return_type
-
-
-def _adjust_call_signature(ctx):
- real_signature = ctx.type.args[2]
- if not isinstance(real_signature, CallableType):
- return ctx.default_signature
-
- real_signature.arg_types[0] = ctx.type.args[0]
- return real_signature
-
-
-class _AdjustInstanceSignature(object):
- def instance(self, ctx):
- instance_type = self._adjust_typeclass_callable(ctx)
- self._adjust_typeclass_type(ctx, instance_type)
- return ctx.default_return_type
-
- def _adjust_typeclass_callable(self, ctx):
- real_signature = ctx.type.args[2]
- to_adjust = ctx.default_return_type.arg_types[0]
-
- instance_type = to_adjust.arg_types[0]
- instance_kind = to_adjust.arg_kinds[0]
- instance_name = to_adjust.arg_names[0]
-
- to_adjust.arg_types = real_signature.arg_types
- to_adjust.arg_kinds = real_signature.arg_kinds
- to_adjust.arg_names = real_signature.arg_names
- to_adjust.variables = real_signature.variables
- to_adjust.is_ellipsis_args = real_signature.is_ellipsis_args
-
- to_adjust.arg_types[0] = instance_type
- to_adjust.arg_kinds[0] = instance_kind
- to_adjust.arg_names[0] = instance_name
-
- return instance_type
-
- def _adjust_typeclass_type(self, ctx, instance_type):
- unified = list(filter(
- # It means that function was defined without annotation
- # or with explicit `Any`, we prevent our Union from polution.
- # Because `Union[Any, int]` is just `Any`.
- # We also clear accidential type vars.
- self._filter_out_unified_types,
- [instance_type, ctx.type.args[0]],
- ))
-
- if not isinstance(instance_type, TypeVarType):
- ctx.type.args[0] = UnionType.make_union(unified)
-
- def _filter_out_unified_types(self, type_) -> bool:
- return not isinstance(type_, (AnyType, TypeVarType))
-
-
-class _TypedDecoratorPlugin(Plugin):
- def get_method_signature_hook(self, fullname: str):
- """Here we fix the calling method types to accept only valid types."""
- if fullname == 'classes.typeclass._TypeClass.__call__':
- return _adjust_call_signature
- return None
-
- def get_method_hook(self, fullname: str):
- """Here we adjust the typeclass with new allowed types."""
- if fullname == 'classes.typeclass._TypeClass.instance':
- return _AdjustInstanceSignature().instance
- return None
-
- def get_function_hook(self, fullname: str):
- """Here we adjust the typeclass constructor."""
- if fullname == 'classes.typeclass.typeclass':
- return _adjust_arguments
- return None
-
-
-def plugin(version: str) -> Type[Plugin]:
- """Plugin's public API and entrypoint."""
- return _TypedDecoratorPlugin
diff --git a/classes/contrib/mypy/typeops/__init__.py b/classes/contrib/mypy/typeops/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/classes/contrib/mypy/typeops/call_signatures.py b/classes/contrib/mypy/typeops/call_signatures.py
new file mode 100644
index 0000000..700a60a
--- /dev/null
+++ b/classes/contrib/mypy/typeops/call_signatures.py
@@ -0,0 +1,135 @@
+from typing import Optional
+
+from mypy.messages import callable_name
+from mypy.plugin import MethodSigContext
+from mypy.subtypes import is_subtype
+from mypy.typeops import get_type_vars, make_simplified_union
+from mypy.types import CallableType, Instance, ProperType
+from mypy.types import Type as MypyType
+from mypy.types import TypeVarType, union_items
+from typing_extensions import Final, final
+
+from classes.contrib.mypy.typeops import type_loader
+
+_INCOMPATIBLE_TYPEVAR_MSG: Final = (
+ 'Argument 1 to {0} has incompatible type "{1}"; expected "{2}"'
+)
+
+
+@final
+class SmartCallSignature(object):
+ """
+ Infers the ``__call__`` signature of a typeclass.
+
+ What it does?
+ 1. It handles ``instance: X`` (where ``X`` is a type variable) case properly
+ 2. It handles ``instance: Iterable[X]`` case properly
+ 3. And ``instance: Any`` works as well
+
+ """
+
+ __slots__ = (
+ '_signature',
+ '_instance_type',
+ '_associated_type',
+ '_ctx',
+ )
+
+ def __init__(
+ self,
+ signature: CallableType,
+ instance_type: MypyType,
+ associated_type: MypyType,
+ ctx: MethodSigContext,
+ ) -> None:
+ """Context that we need."""
+ self._signature = signature.copy_modified()
+ self._instance_type = instance_type
+ self._associated_type = associated_type
+ self._ctx = ctx
+
+ def mutate_and_infer(self, passed_type: MypyType) -> CallableType:
+ """Main entry point."""
+ first_arg = self._signature.arg_types[0]
+ # TODO: later we can refactor this to be `TypeTransformer`
+ if isinstance(first_arg, TypeVarType):
+ return self._infer_type_var(first_arg, passed_type)
+ return self._infer_regular(first_arg)
+
+ def _infer_type_var(
+ self,
+ first_arg: TypeVarType,
+ passed_type: MypyType,
+ ) -> CallableType:
+ instance_types = union_items(self._instance_type)
+ if isinstance(self._associated_type, Instance):
+ instance_types.append(_load_supports_type(
+ first_arg,
+ self._associated_type,
+ self._ctx,
+ ))
+
+ instance_type = make_simplified_union(instance_types)
+ if not is_subtype(passed_type, instance_type):
+ # Let's explain: what happens here?
+ # We need to enforce
+ self._ctx.api.fail(
+ _INCOMPATIBLE_TYPEVAR_MSG.format(
+ callable_name(self._signature),
+ passed_type,
+ instance_type,
+ ),
+ self._ctx.context,
+ )
+ return self._signature
+
+ def _infer_regular(self, first_arg: MypyType) -> CallableType:
+ supports_type: Optional[MypyType] = None
+ if isinstance(self._associated_type, Instance):
+ supports_type = _load_supports_type(
+ first_arg,
+ self._associated_type,
+ self._ctx,
+ should_replace_typevars=True,
+ )
+ self._signature.arg_types[0] = make_simplified_union(
+ list(filter(None, [self._instance_type, supports_type])),
+ )
+ return self._signature
+
+
+def _load_supports_type(
+ first_arg: MypyType,
+ associated_type: Instance,
+ ctx: MethodSigContext,
+ *,
+ should_replace_typevars: bool = False,
+) -> ProperType:
+ """
+ Load ``Supports`` type and injects proper type vars into associated type.
+
+ Why do we need this function?
+
+ Let's see what will happen without it:
+ For example, typeclass `ToJson` with `int` and `str` have will have
+ `Union[str, int]` as the first argument type.
+ But, we need `Union[str, int, Supports[ToJson]]`
+ That's why we are loading this type if the definition is there.
+ """
+ if should_replace_typevars:
+ # Why do we replace typevars here?
+ # Well, because of how `mypy` treats different type variables.
+ # `mypy` compares type variables by their ids.
+ # We might end with this case:
+ #
+ # instance: Iterable[X`1]
+ # associated_type: List[Y`2]
+ #
+ # These variables won't match. And we will break inference.
+ # Something like "Y`2" will be returned as the result.
+ # That's why we need to have proper type variables
+ # injected into the associated type instance.
+ associated_type = associated_type.copy_modified(
+ args=set(get_type_vars(first_arg)),
+ )
+ return type_loader.load_supports_type(associated_type, ctx)
diff --git a/classes/contrib/mypy/typeops/fallback.py b/classes/contrib/mypy/typeops/fallback.py
new file mode 100644
index 0000000..8ff31e0
--- /dev/null
+++ b/classes/contrib/mypy/typeops/fallback.py
@@ -0,0 +1,31 @@
+from typing import Any, Callable, Mapping, Type
+
+from mypy.plugin import MethodContext
+from mypy.types import AnyType
+from mypy.types import Type as MypyType
+from mypy.types import TypeOfAny
+
+_MethodCallback = Callable[[Any, MethodContext], MypyType] # type: ignore
+
+
+def error_to_any(
+ error_map: Mapping[Type[Exception], str],
+) -> Callable[[_MethodCallback], _MethodCallback]:
+ """
+ Decorator for ``mypy`` callbacks to catch given errors.
+
+ We use to show custom messages for given exceptions.
+ If other exceptions are present, then we show just an error message.
+ """
+ def decorator(func: _MethodCallback) -> _MethodCallback:
+ def factory(self, ctx: MethodContext) -> MypyType:
+ try:
+ return func(self, ctx)
+ except Exception as ex:
+ ctx.api.fail(
+ error_map.get(type(ex), str(ex)),
+ ctx.context,
+ )
+ return AnyType(TypeOfAny.from_error)
+ return factory
+ return decorator
diff --git a/classes/contrib/mypy/typeops/inference.py b/classes/contrib/mypy/typeops/inference.py
new file mode 100644
index 0000000..3bfaa83
--- /dev/null
+++ b/classes/contrib/mypy/typeops/inference.py
@@ -0,0 +1,182 @@
+
+from typing import List, Optional
+
+from mypy.nodes import Decorator, Expression
+from mypy.plugin import MethodContext
+from mypy.typeops import make_simplified_union
+from mypy.types import (
+ CallableType,
+ FunctionLike,
+ Instance,
+ LiteralType,
+ TupleType,
+)
+from mypy.types import Type as MypyType
+from mypy.types import TypeVarType, UninhabitedType
+from typing_extensions import Final
+
+_TYPECLASS_DEF_FULLNAMES: Final = frozenset((
+ 'classes._typeclass._TypeClassInstanceDef',
+))
+
+
+def try_to_apply_generics(
+ signature: CallableType,
+ instance_type: MypyType,
+ ctx: MethodContext,
+) -> CallableType:
+ """
+ Conditionally applies instance type on typeclass'es signature.
+
+ What we are looking for here?
+ We are interested in typeclass where instance is a type variable.
+ For example:
+
+ .. code:: python
+
+ def copy(instance: X) -> X:
+ ...
+
+ In this case, we would apply instance type on top of this signature
+ to get the real one for this instance.
+ """
+ if not isinstance(signature.arg_types[0], TypeVarType):
+ return signature
+
+ type_args: List[Optional[MypyType]] = [instance_type]
+ if len(signature.variables) > 1:
+ # `None` here means that a type variable won't be replaced
+ type_args.extend(None for _ in range(len(signature.variables) - 1))
+
+ checker = ctx.api.expr_checker # type: ignore
+ return checker.apply_type_arguments_to_callable(
+ signature,
+ type_args,
+ ctx.context,
+ )
+
+
+def all_same_instance_calls(
+ fullname: str,
+ ctx: MethodContext,
+) -> bool:
+ """
+ Checks whether a given context has instance calls of only one typeclass.
+
+ In other words, it checks that all decorators come from the same typeclass.
+ """
+ if isinstance(ctx.context, Decorator) and len(ctx.context.decorators) > 1:
+ return all(
+ _get_typeclass_instance_type(func_dec, fullname, ctx) is not None
+ for func_dec in ctx.context.decorators
+ )
+ return True
+
+
+def type_obj(type_: MypyType) -> MypyType:
+ """Returns type object from a function like."""
+ if isinstance(type_, FunctionLike) and type_.is_type_obj():
+ # What happens here?
+ # Let's say you define a function like this:
+ #
+ # @some.instance(Sized)
+ # (instance: Sized, b: int) -> str: ...
+ #
+ # So, you will receive callable type
+ # `def () -> Sized` as `runtime_type` in this case.
+ # We need to convert it back to regular `Instance`.
+ #
+ # It can also be `Overloaded` type,
+ # but they are safe to return the same `type_object`,
+ # however we still use `ret_type`,
+ # because it is practically the same thing,
+ # but with proper type arguments.
+ return type_.items[0].ret_type
+ return type_
+
+
+def infer_instance_type_from_context(
+ passed_args: TupleType,
+ fullname: str,
+ ctx: MethodContext,
+) -> MypyType:
+ """
+ Infers instance type from several ``@some.instance()`` decorators.
+
+ We have a problem: when user has two ``.instance()`` decorators
+ on a single function, inference will work only
+ for a single one of them at the time.
+
+ So, let's say you have this:
+
+ .. code:: python
+
+ @some.instance(str)
+ @some.instance(int)
+ def _some_int_str(instance: Union[str, int]): ...
+
+ Your instance has ``Union[str, int]`` annotation as it should have.
+ But, our ``fallback`` type would be just ``int`` on the first call
+ and just ``str`` on the second call.
+
+ And this will break our ``is_same_type`` check,
+ because ``Union[str, int]`` is not the same as ``int`` or ``str``.
+
+ In this case we need to fetch all typeclass decorators and infer
+ the resulting type manually.
+
+ The same works for ``protocol=`` and ``delegate=`` types.
+ """
+ if isinstance(ctx.context, Decorator) and len(ctx.context.decorators) > 1:
+ # Why do we only care for this case?
+ # Because if it is a call / or just a single decorator,
+ # then we are fine with regular type inference.
+ # Inferred type from `mypy` is good enough, just return `fallback`.
+ instance_types = []
+ for decorator in ctx.context.decorators:
+ instance_type = _get_typeclass_instance_type(
+ decorator,
+ fullname,
+ ctx,
+ )
+ if instance_type is not None:
+ instance_types.append(type_obj(instance_type))
+
+ # Inferred resulting type:
+ return make_simplified_union(instance_types)
+
+ fallback = _first_real_passed_arg(passed_args)
+ return type_obj(fallback) if fallback is not None else UninhabitedType()
+
+
+def _get_typeclass_instance_type(
+ expr: Expression,
+ fullname: str,
+ ctx: MethodContext,
+) -> Optional[MypyType]:
+ expr_type = ctx.api.expr_checker.accept(expr) # type: ignore
+ is_typeclass_instance_def = (
+ isinstance(expr_type, Instance) and
+ bool(expr_type.type) and
+ expr_type.type.fullname in _TYPECLASS_DEF_FULLNAMES and
+ isinstance(expr_type.args[1], Instance)
+ )
+ if is_typeclass_instance_def:
+ inst = expr_type.args[1]
+ is_same_typeclass = (
+ isinstance(inst.args[3], LiteralType) and
+ inst.args[3].value == fullname
+ )
+ if is_same_typeclass:
+ # We need the first, existing, non-NoReturn argument:
+ return _first_real_passed_arg(expr_type.args[0])
+ return None
+
+
+def _first_real_passed_arg(passed_args: TupleType) -> Optional[MypyType]:
+ usable_args = (
+ type_arg
+ for type_arg in passed_args.items
+ if not isinstance(type_arg, UninhabitedType)
+ )
+ return next(usable_args, None)
diff --git a/classes/contrib/mypy/typeops/instance_context.py b/classes/contrib/mypy/typeops/instance_context.py
new file mode 100644
index 0000000..ac1425e
--- /dev/null
+++ b/classes/contrib/mypy/typeops/instance_context.py
@@ -0,0 +1,105 @@
+from typing import NamedTuple, Optional
+
+from mypy.plugin import MethodContext
+from mypy.types import CallableType, TupleType
+from mypy.types import Type as MypyType
+from typing_extensions import final
+
+from classes.contrib.mypy.typeops import inference
+
+
+@final
+class _InferredArgs(NamedTuple):
+ """Represents our argument inference result."""
+
+ exact_type: Optional[MypyType]
+ protocol: Optional[MypyType]
+ delegate: Optional[MypyType]
+
+ @classmethod
+ def build(cls, passed_args: TupleType) -> '_InferredArgs':
+ exact_type, protocol, delegate = passed_args.items
+ return _InferredArgs(
+ _infer_type_arg(exact_type),
+ _infer_type_arg(protocol),
+ _infer_type_arg(delegate),
+ )
+
+
+@final
+class InstanceContext(NamedTuple):
+ """
+ Instance definition context.
+
+ We use it to store all important types and data in one place
+ to help with validation and type manipulations.
+ """
+
+ # Signatures:
+ typeclass_signature: CallableType
+ instance_signature: CallableType
+ inferred_signature: CallableType
+
+ # Instance and inferred types:
+ instance_type: MypyType
+ inferred_type: MypyType
+
+ # Arguments:
+ passed_args: TupleType
+ inferred_args: _InferredArgs
+
+ # Meta:
+ fullname: str
+ associated_type: MypyType
+
+ # Mypy context:
+ ctx: MethodContext
+
+ @classmethod # noqa: WPS211
+ def build( # noqa: WPS211
+ # It has a lot of arguments, but I don't see how I can simply it.
+ # I don't want to add steps or intermediate types.
+ # It is okay for this method to have a lot arguments,
+ # because it store a lot of data.
+ cls,
+ typeclass_signature: CallableType,
+ instance_signature: CallableType,
+ passed_args: TupleType,
+ associated_type: MypyType,
+ fullname: str,
+ ctx: MethodContext,
+ ) -> 'InstanceContext':
+ """
+ Builds instance context.
+
+ It also infers several missing parts from the present data.
+ Like real instance signature and "ideal" inferred type.
+ """
+ inferred_type = inference.infer_instance_type_from_context(
+ passed_args=passed_args,
+ fullname=fullname,
+ ctx=ctx,
+ )
+ inferred_signature = inference.try_to_apply_generics(
+ signature=typeclass_signature,
+ instance_type=instance_signature.arg_types[0],
+ ctx=ctx,
+ )
+
+ return InstanceContext(
+ typeclass_signature=typeclass_signature,
+ instance_signature=instance_signature,
+ inferred_signature=inferred_signature,
+ instance_type=instance_signature.arg_types[0],
+ inferred_type=inferred_type,
+ passed_args=passed_args,
+ inferred_args=_InferredArgs.build(passed_args),
+ associated_type=associated_type,
+ fullname=fullname,
+ ctx=ctx,
+ )
+
+
+def _infer_type_arg(type_arg: MypyType) -> Optional[MypyType]:
+ inferred = inference.type_obj(type_arg)
+ return inferred if type_arg is not inferred else None
diff --git a/classes/contrib/mypy/typeops/instance_type_args.py b/classes/contrib/mypy/typeops/instance_type_args.py
new file mode 100644
index 0000000..4ddd9fe
--- /dev/null
+++ b/classes/contrib/mypy/typeops/instance_type_args.py
@@ -0,0 +1,75 @@
+from typing import List, Union
+
+from mypy.plugin import FunctionContext, MethodContext
+from mypy.typeops import make_simplified_union
+from mypy.types import AnyType, Instance, LiteralType, TupleType
+from mypy.types import Type as MypyType
+from mypy.types import TypeVarType, UnboundType, UninhabitedType
+from typing_extensions import Final
+
+#: Types that pollute instance args.
+_TYPES_TO_FILTER_OUT: Final = (
+ TypeVarType,
+ UninhabitedType,
+ UnboundType,
+ AnyType,
+)
+
+
+def add_unique(
+ new_instance_type: MypyType,
+ existing_instance_type: MypyType,
+) -> MypyType:
+ """
+ Adds new instance type to existing ones.
+
+ It is smart: filters our junk and uses unique and flat ``Union`` types.
+ """
+ unified = list(filter(
+ # We filter our `NoReturn` and other things like `Any`
+ # that can break our instances union.
+ # TODO: maybe use `has_uninhabited_component`?
+ lambda type_: not isinstance(type_, _TYPES_TO_FILTER_OUT),
+ [new_instance_type, existing_instance_type],
+ ))
+ return make_simplified_union(unified)
+
+
+def mutate_typeclass_def(
+ typeclass: Instance,
+ definition_fullname: str,
+ ctx: Union[FunctionContext, MethodContext],
+) -> None:
+ """Adds definition fullname to the typeclass type."""
+ str_fallback = ctx.api.str_type() # type: ignore
+
+ typeclass.args = (
+ *typeclass.args[:3],
+ LiteralType(definition_fullname, str_fallback),
+ )
+
+
+def mutate_typeclass_instance_def(
+ instance: Instance,
+ *,
+ passed_types: List[MypyType],
+ typeclass: Instance,
+ ctx: Union[MethodContext, FunctionContext],
+) -> None:
+ """
+ Mutates ``TypeClassInstanceDef`` args.
+
+ That's where we fill their values.
+ Why? Because we need all types from ``some.instance()`` call.
+ Including all passed arguments as a tuple for later checks.
+ """
+ tuple_type = TupleType(
+ # We now store passed arg types in a single tuple:
+ passed_types,
+ fallback=ctx.api.named_type('builtins.tuple'), # type: ignore
+ )
+
+ instance.args = (
+ tuple_type, # Passed runtime types, like str in `@some.instance(str)`
+ typeclass, # `_TypeClass` instance itself
+ )
diff --git a/classes/contrib/mypy/typeops/mro.py b/classes/contrib/mypy/typeops/mro.py
new file mode 100644
index 0000000..745bfb1
--- /dev/null
+++ b/classes/contrib/mypy/typeops/mro.py
@@ -0,0 +1,209 @@
+from typing import List, Optional
+
+from mypy.plugin import MethodContext
+from mypy.subtypes import is_equivalent
+from mypy.types import Instance
+from mypy.types import Type as MypyType
+from mypy.types import UnionType, union_items
+from typing_extensions import final
+
+from classes.contrib.mypy.typeops import type_loader
+
+
+@final
+class MetadataInjector(object):
+ """
+ Injects fake ``Supports[TypeClass]`` parent classes into ``mro``.
+
+ Ok, this is wild. Why do we need this?
+ Because, otherwise expressing ``Supports`` is not possible,
+ here's an example:
+
+ .. code:: python
+
+ >>> from classes import AssociatedType, Supports, typeclass
+
+ >>> class ToStr(AssociatedType):
+ ... ...
+
+ >>> @typeclass(ToStr)
+ ... def to_str(instance) -> str:
+ ... ...
+
+ >>> @to_str.instance(int)
+ ... def _to_str_int(instance: int) -> str:
+ ... return 'Number: {0}'.format(instance)
+
+ >>> assert to_str(1) == 'Number: 1'
+
+ Now, let's use ``Supports`` to only pass specific
+ typeclass instances in a function:
+
+ .. code:: python
+
+ >>> def convert_to_string(arg: Supports[ToStr]) -> str:
+ ... return to_str(arg)
+
+ This is possible, due to a fact that we insert ``Supports[ToStr]``
+ into all classes that are mentioned as ``.instance()`` for ``ToStr``
+ typeclass.
+
+ So, we can call:
+
+ .. code:: python
+
+ >>> assert convert_to_string(1) == 'Number: 1'
+
+ But, ``convert_to_string(None)`` will raise a type error.
+ """
+
+ __slots__ = (
+ '_associated_type',
+ '_instance_types',
+ '_ctx',
+ '_added_types',
+ )
+
+ def __init__(
+ self,
+ associated_type: MypyType,
+ instance_type: MypyType,
+ ctx: MethodContext,
+ ) -> None:
+ """
+ Smart constructor for the metadata injector.
+
+ It is smart, because it handles ``instance_type`` properly.
+ It supports ``Instance`` and ``Union`` types.
+ """
+ self._associated_type = associated_type
+ self._ctx = ctx
+ self._instance_types = union_items(instance_type)
+
+ # Why do we store added types in a mutable global state?
+ # Because, these types are hard to replicate without the proper context.
+ # So, we just keep them here. Based on usage, it is fine.
+ self._added_types: List[Instance] = []
+
+ def add_supports_metadata(self) -> None:
+ """Injects ``Supports`` metadata into instance types' mro."""
+ if not isinstance(self._associated_type, Instance):
+ return
+
+ for instance_type in self._instance_types:
+ if not isinstance(instance_type, Instance):
+ continue
+
+ supports_type = _load_supports_type(
+ associated_type=self._associated_type,
+ instance_type=instance_type,
+ ctx=self._ctx,
+ )
+
+ index = self._find_supports_index(instance_type, supports_type)
+ if index is not None:
+ # We already have `Supports` base class inserted,
+ # it means that we need to unify them:
+ # `Supports[A] + Supports[B] == Supports[Union[A, B]]`
+ self._add_unified_type(instance_type, supports_type, index)
+ else:
+ # This is the first time this type is referenced in
+ # a typeclass'es instance defintinion.
+ # Just inject `Supports` with no extra steps:
+ instance_type.type.bases.append(supports_type)
+
+ if supports_type.type not in instance_type.type.mro:
+ # We only need to add `Supports` type to `mro` once:
+ instance_type.type.mro.append(supports_type.type)
+
+ self._added_types.append(supports_type)
+
+ def remove_supports_metadata(self) -> None:
+ """Removes ``Supports`` metadata from instance types' mro."""
+ if not isinstance(self._associated_type, Instance):
+ return
+
+ for instance_type in self._instance_types:
+ if isinstance(instance_type, Instance):
+ self._clean_instance_type(instance_type)
+ self._added_types = []
+
+ def _clean_instance_type(self, instance_type: Instance) -> None:
+ remove_mro = True
+ for added_type in self._added_types:
+ index = self._find_supports_index(instance_type, added_type)
+ if index is not None:
+ remove_mro = self._remove_unified_type(
+ instance_type,
+ added_type,
+ index,
+ )
+
+ if remove_mro and added_type.type in instance_type.type.mro:
+ # We remove `Supports` type from `mro` only if
+ # there are not associated types left.
+ # For example, `Supports[A, B] - Supports[B] == Supports[A]`
+ # then `Supports[A]` stays.
+ # `Supports[A] - Supports[A] == None`
+ # then `Supports` is removed from `mro` as well.
+ instance_type.type.mro.remove(added_type.type)
+
+ def _find_supports_index(
+ self,
+ instance_type: Instance,
+ supports_type: Instance,
+ ) -> Optional[int]:
+ for index, base in enumerate(instance_type.type.bases):
+ if is_equivalent(base, supports_type, ignore_type_params=True):
+ return index
+ return None
+
+ def _add_unified_type(
+ self,
+ instance_type: Instance,
+ supports_type: Instance,
+ index: int,
+ ) -> None:
+ unified_arg = UnionType.make_union([
+ *supports_type.args,
+ *instance_type.type.bases[index].args,
+ ])
+ instance_type.type.bases[index] = supports_type.copy_modified(
+ args=[unified_arg],
+ )
+
+ def _remove_unified_type(
+ self,
+ instance_type: Instance,
+ supports_type: Instance,
+ index: int,
+ ) -> bool:
+ base = instance_type.type.bases[index]
+ union_types = [
+ type_arg
+ for type_arg in union_items(base.args[0])
+ if type_arg not in supports_type.args
+ ]
+ instance_type.type.bases[index] = supports_type.copy_modified(
+ args=[UnionType.make_union(union_types)],
+ )
+ return not bool(union_types)
+
+
+def _load_supports_type(
+ associated_type: Instance,
+ instance_type: Instance,
+ ctx: MethodContext,
+) -> Instance:
+ # Why do have to modify args of `associated_type`?
+ # Because `mypy` requires `type_var.id` to match,
+ # otherwise, they would be treated as different variables.
+ # That's why we copy the typevar definition from instance itself.
+ supports_spec = associated_type.copy_modified(
+ args=instance_type.type.defn.type_vars,
+ )
+
+ return type_loader.load_supports_type(
+ supports_spec,
+ ctx,
+ )
diff --git a/classes/contrib/mypy/typeops/type_loader.py b/classes/contrib/mypy/typeops/type_loader.py
new file mode 100644
index 0000000..1b8edae
--- /dev/null
+++ b/classes/contrib/mypy/typeops/type_loader.py
@@ -0,0 +1,37 @@
+from typing import Union
+
+from mypy.plugin import MethodContext, MethodSigContext
+from mypy.types import Instance
+from mypy.types import Type as MypyType
+from typing_extensions import Final
+
+_SUPPORTS_QUALIFIED_NAME: Final = 'classes.Supports'
+
+
+def load_supports_type(
+ arg_type: MypyType,
+ ctx: Union[MethodContext, MethodSigContext],
+) -> Instance:
+ """
+ Loads ``Supports[]`` type with proper generic type.
+
+ It uses the short name,
+ because for some reason full name is not always loaded.
+ """
+ supports_spec = ctx.api.named_generic_type(
+ _SUPPORTS_QUALIFIED_NAME,
+ [arg_type],
+ )
+ assert supports_spec
+ supports_spec.type._promote = None # noqa: WPS437
+ return supports_spec
+
+
+def load_typeclass(
+ fullname: str,
+ ctx: MethodContext,
+) -> Instance:
+ """Loads given typeclass from a symboltable by a fullname."""
+ typeclass_info = ctx.api.lookup_qualified(fullname) # type: ignore
+ assert isinstance(typeclass_info.type, Instance)
+ return typeclass_info.type
diff --git a/classes/contrib/mypy/typeops/type_queries.py b/classes/contrib/mypy/typeops/type_queries.py
new file mode 100644
index 0000000..3b1393e
--- /dev/null
+++ b/classes/contrib/mypy/typeops/type_queries.py
@@ -0,0 +1,121 @@
+from typing import Callable, Iterable
+
+from mypy.plugin import MethodContext
+from mypy.type_visitor import TypeQuery
+from mypy.types import AnyType, Instance, TupleType
+from mypy.types import Type as MypyType
+from mypy.types import TypeVarType, UnboundType, get_proper_type
+
+
+def has_concrete_type(
+ type_: MypyType,
+ is_delegate: bool,
+ ctx: MethodContext,
+ *,
+ forbid_explicit_any: bool,
+) -> bool:
+ """
+ Queries if your instance has any concrete types.
+
+ What do we call "concrete types"? Some examples:
+
+ ``List[X]`` is generic, ``List[int]`` is concrete.
+ ``List[Union[int, str]]`` is also concrete.
+ ``Dict[str, X]`` is also concrete.
+
+ ``Tuple[X, ...]`` is generic.
+ While ``Tuple[X, X]`` and ``Tuple[int, ...]`` are concrete.
+
+ So, this helps to write code like this:
+
+ .. code:: python
+
+ @some.instance(list)
+ def _some_list(instance: List[X]): ...
+
+ And not like:
+
+ .. code:: python
+
+ @some.instance(list)
+ def _some_list(instance: List[int]): ...
+
+ """
+ def factory(typ) -> bool:
+ return not is_delegate
+
+ type_ = get_proper_type(type_)
+ # TODO: support `Literal`?
+ if isinstance(type_, TupleType):
+ # This allows to have types like:
+ #
+ # @some.instance(delegate=Coords)
+ # def _some_coords(instance: Tuple[int, int]) -> str:
+ # ...
+ return not is_delegate
+ if isinstance(type_, Instance):
+ return any(
+ type_arg.accept(_HasNoConcreteTypes(
+ factory,
+ forbid_explicit_any=forbid_explicit_any,
+ ))
+ for type_arg in type_.args
+ )
+ return False
+
+
+def has_unbound_type(type_: MypyType, ctx: MethodContext) -> bool:
+ """
+ Queries if your instance has any unbound types.
+
+ Note, that you need to understand
+ how semantic and type analyzers work in ``mypy``
+ to understand what "unbound type" is.
+
+ Long story short, this helps to write code like this:
+
+ .. code:: python
+
+ @some.instance(list)
+ def _some_list(instance: List[X]): ...
+
+ And not like:
+
+ .. code:: python
+
+ @some.instance(List[X])
+ def _some_list(instance: List[X]): ...
+
+ """
+ type_ = get_proper_type(type_)
+ if isinstance(type_, Instance):
+ return any(
+ type_arg.accept(_HasUnboundTypes(lambda _: False))
+ for type_arg in type_.args
+ )
+ return False
+
+
+class _HasNoConcreteTypes(TypeQuery[bool]):
+ def __init__(
+ self,
+ strategy: Callable[[Iterable[bool]], bool],
+ *,
+ forbid_explicit_any: bool,
+ ) -> None:
+ super().__init__(strategy)
+ self._forbid_explicit_any = forbid_explicit_any
+
+ def visit_type_var(self, type_: TypeVarType) -> bool:
+ return False
+
+ def visit_unbound_type(self, type_: UnboundType) -> bool:
+ return False
+
+ def visit_any(self, type_: AnyType) -> bool:
+ return self._forbid_explicit_any
+
+
+class _HasUnboundTypes(TypeQuery[bool]):
+ def visit_unbound_type(self, type_: UnboundType) -> bool:
+ return True
diff --git a/classes/contrib/mypy/validation/__init__.py b/classes/contrib/mypy/validation/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/classes/contrib/mypy/validation/validate_associated_type.py b/classes/contrib/mypy/validation/validate_associated_type.py
new file mode 100644
index 0000000..1ab4b01
--- /dev/null
+++ b/classes/contrib/mypy/validation/validate_associated_type.py
@@ -0,0 +1,119 @@
+from mypy.plugin import MethodContext
+from mypy.typeops import get_type_vars
+from mypy.types import CallableType, Instance
+from typing_extensions import Final
+
+# Messages:
+
+_WRONG_SUBCLASS_MSG: Final = (
+ 'Single direct subclass of "{0}" required; got "{1}"'
+)
+
+_TYPE_REUSE_MSG: Final = (
+ 'AssociatedType "{0}" must not be reused, originally associated with "{1}"'
+)
+
+_GENERIC_MISSMATCH_MSG: Final = (
+ 'Generic type "{0}" with "{1}" type arguments does not match ' +
+ 'generic instance declaration "{2}" with "{3}" type arguments'
+)
+
+_REDUNDANT_BODY_MSG: Final = 'Associated types must not have bodies'
+
+
+def check_type(
+ associated_type: Instance,
+ associated_type_fullname: str,
+ typeclass: Instance,
+ ctx: MethodContext,
+) -> bool:
+ """Checks passed ``AssociatedType`` instance."""
+ return all([
+ _check_base_class(associated_type, associated_type_fullname, ctx),
+ _check_body(associated_type, ctx),
+ _check_type_reuse(associated_type, typeclass, ctx),
+ _check_generics(associated_type, typeclass, ctx),
+ # TODO: we also need to check type vars used on definition:
+ # no values, no bounds (?)
+ ])
+
+
+def _check_base_class(
+ associated_type: Instance,
+ associated_type_fullname: str,
+ ctx: MethodContext,
+) -> bool:
+ bases = associated_type.type.bases
+ has_correct_base = (
+ len(bases) == 1 and
+ associated_type_fullname == bases[0].type.fullname
+ )
+ if not has_correct_base:
+ ctx.api.fail(
+ _WRONG_SUBCLASS_MSG.format(
+ associated_type_fullname,
+ associated_type,
+ ),
+ ctx.context,
+ )
+ return has_correct_base
+
+
+def _check_body(
+ associated_type: Instance,
+ ctx: MethodContext,
+) -> bool:
+ if associated_type.type.names:
+ ctx.api.fail(_REDUNDANT_BODY_MSG, ctx.context)
+ return False
+ return True
+
+
+def _check_type_reuse(
+ associated_type: Instance,
+ typeclass: Instance,
+ ctx: MethodContext,
+) -> bool:
+ fullname = getattr(typeclass.args[3], 'value', None)
+ metadata = associated_type.type.metadata.setdefault('classes', {})
+
+ has_reuse = (
+ fullname is not None and
+ 'typeclass' in metadata and
+ metadata['typeclass'] != fullname
+ )
+ if has_reuse:
+ ctx.api.fail(
+ _TYPE_REUSE_MSG.format(associated_type.type.fullname, fullname),
+ ctx.context,
+ )
+
+ metadata['typeclass'] = fullname
+ return has_reuse
+
+
+def _check_generics(
+ associated_type: Instance,
+ typeclass: Instance,
+ ctx: MethodContext,
+) -> bool:
+ assert isinstance(typeclass.args[1], CallableType)
+ instance_decl = typeclass.args[1].arg_types[0]
+ if not isinstance(instance_decl, Instance):
+ return True
+
+ # We use `get_type_vars` here to exclude cases like `Supports[ToJson]`
+ # and `List[int]` from validation:
+ instance_args = get_type_vars(instance_decl)
+ if len(instance_args) != len(associated_type.type.type_vars):
+ ctx.api.fail(
+ _GENERIC_MISSMATCH_MSG.format(
+ associated_type.type.fullname,
+ len(associated_type.type.type_vars),
+ instance_decl,
+ len(instance_args),
+ ),
+ ctx.context,
+ )
+ return False
+ return True
diff --git a/classes/contrib/mypy/validation/validate_instance/__init__.py b/classes/contrib/mypy/validation/validate_instance/__init__.py
new file mode 100644
index 0000000..6f607e5
--- /dev/null
+++ b/classes/contrib/mypy/validation/validate_instance/__init__.py
@@ -0,0 +1,15 @@
+from classes.contrib.mypy.typeops.instance_context import InstanceContext
+from classes.contrib.mypy.validation.validate_instance import (
+ validate_instance_args,
+ validate_runtime,
+ validate_signature,
+)
+
+
+def check_type(instance_context: InstanceContext) -> bool:
+ """Checks that instance definition is correct."""
+ return all([
+ validate_signature.check_type(instance_context),
+ validate_runtime.check_type(instance_context),
+ validate_instance_args.check_type(instance_context),
+ ])
diff --git a/classes/contrib/mypy/validation/validate_instance/validate_instance_args.py b/classes/contrib/mypy/validation/validate_instance/validate_instance_args.py
new file mode 100644
index 0000000..c8a6ac3
--- /dev/null
+++ b/classes/contrib/mypy/validation/validate_instance/validate_instance_args.py
@@ -0,0 +1,36 @@
+from mypy.plugin import MethodContext
+from mypy.types import TupleType, UninhabitedType
+
+from classes._registry import INVALID_ARGUMENTS_MSG # noqa: WPS436
+from classes.contrib.mypy.typeops.instance_context import InstanceContext
+
+
+def check_type(
+ instance_context: InstanceContext,
+) -> bool:
+ """
+ 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.
+ """
+ return all([
+ _check_all_args(instance_context.passed_args, instance_context.ctx),
+ ])
+
+
+def _check_all_args(
+ passed_args: TupleType,
+ ctx: MethodContext,
+) -> bool:
+ fake_args = [
+ passed_arg
+ for passed_arg in passed_args.items[1:]
+ if isinstance(passed_arg, UninhabitedType)
+ ]
+ if not fake_args:
+ ctx.api.fail(INVALID_ARGUMENTS_MSG, ctx.context)
+ return False
+ return True
diff --git a/classes/contrib/mypy/validation/validate_instance/validate_runtime.py b/classes/contrib/mypy/validation/validate_instance/validate_runtime.py
new file mode 100644
index 0000000..8d57ead
--- /dev/null
+++ b/classes/contrib/mypy/validation/validate_instance/validate_runtime.py
@@ -0,0 +1,184 @@
+from typing import Optional
+
+from mypy.erasetype import erase_type
+from mypy.plugin import MethodContext
+from mypy.sametypes import is_same_type
+from mypy.subtypes import is_subtype
+from mypy.types import Instance
+from mypy.types import Type as MypyType
+from mypy.types import TypedDictType
+from typing_extensions import Final
+
+from classes.contrib.mypy.typeops import type_queries
+from classes.contrib.mypy.typeops.instance_context import InstanceContext
+
+# Messages:
+
+_INSTANCE_INFERRED_MISMATCH_MSG: Final = (
+ 'Instance "{0}" does not match inferred type "{1}"'
+)
+
+_IS_PROTOCOL_MISSING_MSG: Final = (
+ 'Protocol type "{0}" is passed as a regular type'
+)
+
+_IS_PROTOCOL_UNWANTED_MSG: Final = (
+ 'Regular type "{0}" passed as a protocol'
+)
+
+_DELEGATE_STRICT_SUBTYPE_MSG: Final = (
+ 'Delegate types used for instance annotation "{0}" ' +
+ 'must be a direct subtype of runtime type "{1}"'
+)
+
+_CONCRETE_GENERIC_MSG: Final = (
+ 'Instance "{0}" has concrete generic type, ' +
+ 'it is not supported during runtime'
+)
+
+_UNBOUND_TYPE_MSG: Final = (
+ 'Runtime type "{0}" has unbound type, use implicit any'
+)
+
+_TUPLE_LENGTH_MSG: Final = (
+ 'Expected variadic tuple "Tuple[{0}, ...]", got "{1}"'
+)
+
+
+def check_type(
+ instance_context: InstanceContext,
+) -> bool:
+ """
+ Checks runtime type.
+
+ We call "runtime types" things that we use at runtime to dispatch our calls.
+ For example:
+
+ 1. We check that type passed in ``some.instance(...)`` matches
+ one defined in a type annotation
+ 2. We check that types don't have any concrete types
+ 3. We check that types don't have any unbound type variables
+ 4. We check that ``protocol`` and ``delegate`` are passed correctly
+
+ """
+ return all([
+ _check_matching_types(
+ instance_context.inferred_type,
+ instance_context.instance_type,
+ instance_context.inferred_args.delegate,
+ instance_context.ctx,
+ ),
+ _check_protocol_usage(
+ instance_context.inferred_args.exact_type,
+ instance_context.inferred_args.protocol,
+ instance_context.inferred_args.delegate,
+ instance_context.ctx,
+ ),
+ _check_concrete_generics(
+ instance_context.inferred_type,
+ instance_context.instance_type,
+ instance_context.inferred_args.delegate,
+ instance_context.ctx,
+ ),
+ ])
+
+
+def _check_matching_types(
+ inferred_type: MypyType,
+ instance_type: MypyType,
+ delegate: Optional[MypyType],
+ ctx: MethodContext,
+) -> bool:
+ if delegate is None:
+ instance_check = is_same_type(
+ erase_type(instance_type),
+ erase_type(inferred_type),
+ )
+ else:
+ # We check for delegate a bit different,
+ # because we want to support cases like:
+ #
+ # class ListOfStr(List[str]):
+ # ...
+ #
+ # @some.instance(delegate=ListOfStr)
+ # def _some_list_str(instance: List[str]) -> str:
+ #
+ # some(['a', 'b', 'c']) # noqa: E800
+ #
+ # Without this check, we would have
+ # to always use `ListOfStr` and not `List[str]`.
+ # This is annoying for the user.
+ instance_check = (
+ is_subtype(instance_type, delegate)
+ # When `instance` is a `TypedDict`, we need to rotate the compare:
+ if isinstance(instance_type, TypedDictType)
+ else is_subtype(delegate, instance_type)
+ )
+
+ if not instance_check:
+ ctx.api.fail(
+ _INSTANCE_INFERRED_MISMATCH_MSG.format(
+ instance_type,
+ inferred_type if delegate is None else delegate,
+ ),
+ ctx.context,
+ )
+ return instance_check
+
+
+def _check_protocol_usage( # noqa: WPS231
+ exact_type: Optional[MypyType],
+ protocol: Optional[MypyType],
+ delegate: Optional[MypyType],
+ ctx: MethodContext,
+) -> bool:
+ validation_rules = (
+ (exact_type, False),
+ (protocol, True),
+ (delegate, False),
+ )
+
+ is_valid = True
+ for passed_type, is_protocol in validation_rules:
+ if isinstance(passed_type, Instance) and passed_type.type:
+ if not is_protocol and passed_type.type.is_protocol:
+ ctx.api.fail(
+ _IS_PROTOCOL_MISSING_MSG.format(passed_type), ctx.context,
+ )
+ is_valid = False
+ elif is_protocol and not passed_type.type.is_protocol:
+ ctx.api.fail(
+ _IS_PROTOCOL_UNWANTED_MSG.format(passed_type), ctx.context,
+ )
+ is_valid = False
+ return is_valid
+
+
+def _check_concrete_generics(
+ inferred_type: MypyType,
+ instance_type: MypyType,
+ delegate: Optional[MypyType],
+ ctx: MethodContext,
+) -> bool:
+ has_concrete_type = False
+ type_settings = ( # Not a dict, because of `hash` problems
+ (instance_type, False),
+ (inferred_type, True),
+ )
+
+ for type_, forbid_explicit_any in type_settings:
+ local_check = type_queries.has_concrete_type(
+ type_,
+ is_delegate=delegate is not None,
+ forbid_explicit_any=forbid_explicit_any,
+ ctx=ctx,
+ )
+ 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(inferred_type, ctx):
+ ctx.api.fail(_UNBOUND_TYPE_MSG.format(inferred_type), ctx.context)
+ return False
+ return not has_concrete_type
diff --git a/classes/contrib/mypy/validation/validate_instance/validate_signature.py b/classes/contrib/mypy/validation/validate_instance/validate_signature.py
new file mode 100644
index 0000000..11392a4
--- /dev/null
+++ b/classes/contrib/mypy/validation/validate_instance/validate_signature.py
@@ -0,0 +1,190 @@
+
+from mypy.plugin import MethodContext
+from mypy.subtypes import is_subtype
+from mypy.types import AnyType, CallableType, TypeOfAny
+from typing_extensions import Final
+
+from classes.contrib.mypy.typeops import inference
+from classes.contrib.mypy.typeops.instance_context import InstanceContext
+
+_INCOMPATIBLE_INSTANCE_SIGNATURE_MSG: Final = (
+ 'Instance callback is incompatible "{0}"; expected "{1}"'
+)
+
+_INSTANCE_RESTRICTION_MSG: Final = (
+ 'Instance "{0}" does not match original type "{1}"'
+)
+
+_DIFFERENT_INSTANCE_CALLS_MSG: Final = (
+ 'Found different typeclass ".instance" calls, use only "{0}"'
+)
+
+
+def check_type(
+ instance_context: InstanceContext,
+) -> bool:
+ """
+ We need to typecheck passed functions in order to build correct typeclasses.
+
+ Please, see docs on each step.
+ """
+ return all([
+ _check_typeclass_signature(
+ instance_context.inferred_signature,
+ instance_context.instance_signature,
+ instance_context.ctx,
+ ),
+ _check_instance_type(
+ instance_context.inferred_signature,
+ instance_context.instance_signature,
+ instance_context.ctx,
+ ),
+ _check_same_typeclass(instance_context.fullname, instance_context.ctx),
+ ])
+
+
+def _check_typeclass_signature(
+ typeclass_signature: CallableType,
+ instance_signature: CallableType,
+ ctx: MethodContext,
+) -> bool:
+ """
+ Checks that instance signature is compatible with.
+
+ We use contravariant on arguments and covariant on return type logic here.
+ What does this mean?
+
+ Let's say that you have this typeclass signature:
+
+ .. code:: python
+
+ class A: ...
+ class B(A): ...
+ class C(B): ...
+
+ @typeclass
+ def some(instance, arg: B) -> B: ...
+
+ What instance signatures will be compatible?
+
+ .. code:: python
+
+ (instance: ..., arg: B) -> B: ...
+ (instance: ..., arg: A) -> C: ...
+
+ But, any other cases will raise an error.
+
+ .. note::
+ We don't check instance types here at all,
+ we replace it with ``Any``.
+ See special function, where we check instance type.
+
+ """
+ simplified_typeclass_signature = typeclass_signature.copy_modified(
+ arg_types=[
+ AnyType(TypeOfAny.implementation_artifact),
+ *typeclass_signature.arg_types[1:],
+ ],
+ )
+ simplified_instance_signature = instance_signature.copy_modified(
+ arg_types=[
+ AnyType(TypeOfAny.implementation_artifact),
+ *instance_signature.arg_types[1:],
+ ],
+ )
+ signature_check = is_subtype(
+ simplified_instance_signature,
+ simplified_typeclass_signature,
+ )
+ if not signature_check:
+ ctx.api.fail(
+ _INCOMPATIBLE_INSTANCE_SIGNATURE_MSG.format(
+ instance_signature,
+ typeclass_signature.copy_modified(arg_types=[
+ instance_signature.arg_types[0], # Better error message
+ *typeclass_signature.arg_types[1:],
+ ]),
+ ),
+ ctx.context,
+ )
+ return signature_check
+
+
+def _check_instance_type(
+ typeclass_signature: CallableType,
+ instance_signature: CallableType,
+ ctx: MethodContext,
+) -> bool:
+ """
+ Checks instance type, helpful when typeclass has type restrictions.
+
+ We use covariant logic on instance type.
+ What does this mean?
+
+ .. code:: python
+
+ class A: ...
+ class B(A): ...
+ class C(B): ...
+
+ @typeclass
+ def some(instance: B): ...
+
+ What can we use on instance callbacks?
+
+ .. code:: python
+
+ @some.instance(B)
+ def _some_b(instance: B):
+ ...
+
+ @some.instance(C)
+ def _some_c(instance: C):
+ ...
+
+ Any types that are not subtypes of ``B`` will raise a type error.
+ """
+ instance_check = is_subtype(
+ instance_signature.arg_types[0],
+ typeclass_signature.arg_types[0],
+ )
+ if not instance_check:
+ ctx.api.fail(
+ _INSTANCE_RESTRICTION_MSG.format(
+ instance_signature.arg_types[0],
+ typeclass_signature.arg_types[0],
+ ),
+ ctx.context,
+ )
+ return instance_check
+
+
+def _check_same_typeclass(
+ fullname: str,
+ ctx: MethodContext,
+) -> bool:
+ """
+ Checks that only one typeclass can be referenced in all of decorators.
+
+ If we have multiple decorators on a function,
+ it is not safe to assume
+ that we have ``.instance`` calls from the same typeclass.
+ We don't want this:
+
+ .. code:: python
+
+ @some.instance(str)
+ @other.instance(int)
+ def some(instance: Union[str, int]) -> None:
+ ...
+
+ We don't allow this way of instance definition.
+ See "FAQ" in docs for more information.
+ """
+ if not inference.all_same_instance_calls(fullname, ctx):
+ ctx.api.fail(
+ _DIFFERENT_INSTANCE_CALLS_MSG.format(fullname),
+ ctx.context,
+ )
+ return False
+ return True
diff --git a/classes/contrib/mypy/validation/validate_supports.py b/classes/contrib/mypy/validation/validate_supports.py
new file mode 100644
index 0000000..28645a6
--- /dev/null
+++ b/classes/contrib/mypy/validation/validate_supports.py
@@ -0,0 +1,45 @@
+from mypy.nodes import SymbolTableNode, TypeInfo
+from mypy.plugin import AnalyzeTypeContext
+from mypy.subtypes import is_subtype
+from mypy.types import Instance
+from typing_extensions import Final
+
+# Messages:
+
+_ARG_SUBTYPE_MSG: Final = (
+ 'Type argument "{0}" of "{1}" must be a subtype of "{2}"'
+)
+
+
+def check_type(
+ instance: Instance,
+ associated_type_node: SymbolTableNode,
+ ctx: AnalyzeTypeContext,
+) -> bool:
+ """Checks how ``Supports`` is used."""
+ return all([
+ _check_instance_args(instance, associated_type_node, ctx),
+ ])
+
+
+def _check_instance_args(
+ instance: Instance,
+ associated_type_node: SymbolTableNode,
+ ctx: AnalyzeTypeContext,
+) -> bool:
+ assert isinstance(associated_type_node.node, TypeInfo)
+ associated_type = Instance(associated_type_node.node, [])
+
+ is_correct = True
+ for type_arg in instance.args:
+ if not is_subtype(type_arg, associated_type):
+ is_correct = False
+ ctx.api.fail(
+ _ARG_SUBTYPE_MSG.format(
+ type_arg,
+ instance.type.name,
+ associated_type,
+ ),
+ ctx.context,
+ )
+ return is_correct
diff --git a/classes/contrib/mypy/validation/validate_typeclass_def.py b/classes/contrib/mypy/validation/validate_typeclass_def.py
new file mode 100644
index 0000000..08aceca
--- /dev/null
+++ b/classes/contrib/mypy/validation/validate_typeclass_def.py
@@ -0,0 +1,80 @@
+from typing import Union
+
+from mypy.nodes import (
+ ARG_POS,
+ EllipsisExpr,
+ ExpressionStmt,
+ FuncDef,
+ PassStmt,
+ StrExpr,
+)
+from mypy.plugin import FunctionContext, MethodContext
+from mypy.types import CallableType, Instance
+from typing_extensions import Final
+
+_Contexts = Union[MethodContext, FunctionContext]
+
+# Messages:
+
+_AT_LEAST_ONE_ARG_MSG: Final = (
+ 'Typeclass definition must have at least one positional argument'
+)
+_FIRST_ARG_KIND_MSG: Final = (
+ 'First argument in typeclass definition must be positional'
+)
+_REDUNDANT_BODY_MSG: Final = 'Typeclass definitions must not have bodies'
+
+
+def check_type(
+ typeclass: Instance,
+ ctx: _Contexts,
+) -> bool:
+ """Checks typeclass definition."""
+ return all([
+ _check_first_arg(typeclass, ctx),
+ _check_body(typeclass, ctx),
+ ])
+
+
+def _check_first_arg(
+ typeclass: Instance,
+ ctx: _Contexts,
+) -> bool:
+ sig = typeclass.args[1]
+ assert isinstance(sig, CallableType)
+
+ if not len(sig.arg_kinds):
+ ctx.api.fail(_AT_LEAST_ONE_ARG_MSG, ctx.context)
+ return False
+
+ if sig.arg_kinds[0] != ARG_POS:
+ ctx.api.fail(_FIRST_ARG_KIND_MSG, ctx.context)
+ return False
+ return True
+
+
+def _check_body(
+ typeclass: Instance,
+ ctx: _Contexts,
+) -> bool:
+ sig = typeclass.args[1]
+ assert isinstance(sig, CallableType)
+ assert isinstance(sig.definition, FuncDef)
+
+ body = sig.definition.body.body
+ if body:
+ is_useless_body = (
+ len(body) == 1 and
+ (isinstance(body[0], PassStmt) or (
+ isinstance(body[0], ExpressionStmt) and
+ isinstance(body[0].expr, (EllipsisExpr, StrExpr))
+ ))
+ )
+ if is_useless_body:
+ # We allow a single ellipsis in function a body.
+ # We also allow just a docstring.
+ return True
+
+ ctx.api.fail(_REDUNDANT_BODY_MSG, ctx.context)
+ return False
+ return True
diff --git a/classes/typeclass.py b/classes/typeclass.py
deleted file mode 100644
index 33e9047..0000000
--- a/classes/typeclass.py
+++ /dev/null
@@ -1,330 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from typing import Callable, Dict, Generic, NoReturn, Type, TypeVar, overload
-
-from typing_extensions import Literal
-
-_TypeClassType = TypeVar('_TypeClassType')
-_ReturnType = TypeVar('_ReturnType')
-_CallbackType = TypeVar('_CallbackType', bound=Callable)
-_InstanceType = TypeVar('_InstanceType')
-
-
-class _TypeClass(Generic[_TypeClassType, _ReturnType, _CallbackType]):
- """
- That's how we represent typeclasses.
-
- You should also use this type to annotate places
- where you expect some specific typeclass to be used.
-
- .. code:: python
-
- >>> from typing import Callable
- >>> from classes import typeclass
- >>> @typeclass
- ... def used(instance, other: int) -> int:
- ... '''Example typeclass to be used later.'''
- ...
- >>> @used.instance(int)
- ... def _used_int(instance: int, other: int) -> int:
- ... return instance + other
- ...
- >>> def accepts_typeclass(
- ... callback: Callable[[int, int], int],
- ... ) -> int:
- ... return callback(1, 3)
- ...
- >>> accepts_typeclass(used)
- 4
-
- Take a note, that we structural subtyping here.
- And all typeclasses that match this signature will typecheck.
-
- """
-
- def __init__(self, signature: _CallbackType) -> None:
- """
- Protected constuctor of the typeclass.
-
- Use public :func:`~typeclass` constructor instead.
-
- How does this magic work? It heavily relies on custom ``mypy`` plugin.
- Without it - it is just a nonsence.
-
- The logic is quite unsual.
- We use "mypy-plugin-time" variables to contruct a typeclass.
-
- What variables we use and why?
-
- - ``_TypeclassType`` is a type variable that indicates
- what type can be passed into this typeclass.
- This type is updated each time we call ``.instance``,
- because that how we introduce new types to the typeclass
-
- - ``_ReturnType`` is used to enforce
- the same return type for all cases.
- Only modified once during ``@typeclass`` creation
-
- - ``_CallbackType`` is used to ensude that all parameters
- for all type cases are the same.
- That's how we enforce consistency in all function signatures.
- The only exception is the first argument: it is polymorfic.
-
- """
- self._signature = signature
- self._instances: Dict[type, Callable] = {}
- self._protocols: Dict[type, Callable] = {}
-
- def __call__(
- self,
- instance: _TypeClassType,
- *args,
- **kwargs,
- ) -> _ReturnType:
- """
- We use this method to actually call a typeclass.
-
- The resolution order is the following:
-
- 1. Exact types that are passed as ``.instance`` arguments
- 2. Protocols that are passed with ``is_protocol=True``
-
- We don't guarantee the order of types inside groups.
- Use correct types, do not rely on our order.
-
- .. rubric:: Callbacks
-
- Since, we define ``__call__`` method for this class,
- it can be used and typechecked everywhere,
- where a regular ``Callable`` is expected.
-
- """
- instance_type = type(instance)
- implementation = self._instances.get(instance_type, None)
- if implementation is not None:
- return implementation(instance, *args, **kwargs)
-
- for protocol, callback in self._protocols.items():
- if isinstance(instance, protocol):
- return callback(instance, *args, **kwargs)
-
- raise NotImplementedError(
- 'Missing matched typeclass instance for type: {0}'.format(
- instance_type.__qualname__,
- ),
- )
-
- @overload
- def instance(
- self,
- type_argument: Type[_InstanceType],
- *,
- is_protocol: Literal[False] = ...,
- ) -> Callable[
- [Callable[[_InstanceType], _ReturnType]],
- NoReturn, # We need this type to disallow direct instance calls
- ]: # pragma: no cover
- ...
-
- @overload
- def instance(
- self,
- type_argument,
- *,
- is_protocol: Literal[True],
- ) -> Callable[
- [Callable[[_InstanceType], _ReturnType]],
- NoReturn, # We need this type to disallow direct instance calls
- ]: # pragma: no cover
- ...
-
- def instance(
- self,
- type_argument,
- *,
- is_protocol: bool = False,
- ):
- """
- 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.
- That's why we also have this ugly ``@overload`` cases.
- Otherwise, ``Protocol`` instances
- would not match ``Type[_InstanceType]`` type due to ``mypy`` rules.
-
- """
- isinstance(object(), type_argument) # That's how we check for generics
-
- def decorator(implementation):
- container = self._protocols if is_protocol else self._instances
- container[type_argument] = implementation
- return implementation
- return decorator
-
-
-def typeclass(
- signature: _CallbackType,
- # By default `_TypeClassType` and `_ReturnType` are `nothing`,
- # but we enhance them via mypy plugin later:
-) -> _TypeClass[_TypeClassType, _ReturnType, _CallbackType]:
- """
- Function to define typeclasses.
-
- Basic usage
- ~~~~~~~~~~~
-
- The first and the simplest example of a typeclass is just its definition:
-
- .. code:: python
-
- >>> from classes import typeclass
- >>> @typeclass
- ... def example(instance) -> str:
- ... '''Example typeclass.'''
- ...
- >>> example(1)
- Traceback (most recent call last):
- ...
- NotImplementedError: Missing matched typeclass instance for type: int
-
- In this example we work with the default implementation of a typeclass.
- It raise a ``NotImplementedError`` when no instances match.
- And we don't yet have a special case for ``int``,
- that why we fallback to the default implementation.
-
- It works like a regular function right now.
- Let's do the next step and introduce
- the ``int`` instance for the typeclass:
-
- .. code:: python
-
- >>> @example.instance(int)
- ... def _example_int(instance: int) -> str:
- ... return 'int case'
- ...
- >>> example(1)
- 'int case'
-
- Now we have a specific instance for ``int``
- which does something different from the default implementation.
-
- What will happen if we pass something new, like ``str``?
-
- .. code:: python
-
- >>> example('a')
- Traceback (most recent call last):
- ...
- NotImplementedError: Missing matched typeclass instance for type: str
-
- Because again, we don't yet have
- an instance of this typeclass for ``str`` type.
- Let's fix that.
-
- .. code:: python
-
- >>> @example.instance(str)
- ... def _example_str(instance: str) -> str:
- ... return instance
- ...
- >>> example('a')
- 'a'
-
- Now it works with ``str`` as well. But differently.
- This allows developer to base the implementation on type information.
-
- So, the rule is clear:
- if we have a typeclass instance for a specific type,
- then it will be called,
- otherwise the default implementation will be called instead.
-
- .. rubric:: Generics
-
- We also support generic, but the support is limited.
- We cannot rely on type parameters of the generic type,
- only on the base generic class:
-
- .. code:: python
-
- >>> from typing import Generic, TypeVar
- >>> T = TypeVar('T')
- >>> class MyGeneric(Generic[T]):
- ... def __init__(self, arg: T) -> None:
- ... self.arg = arg
- ...
-
- Now, let's define the typeclass instance for this type:
-
- .. code:: python
-
- >>> @example.instance(MyGeneric)
- ... def _my_generic_example(instance: MyGeneric) -> str:
- ... return 'generi' + str(instance.arg)
- ...
- >>> example(MyGeneric('c'))
- 'generic'
-
- This case will work for all type parameters of ``MyGeneric``,
- or in other words it can be assumed as ``MyGeneric[Any]``:
-
- .. code:: python
-
- >>> example(MyGeneric(1))
- 'generi1'
-
- In the future, when Python will have new type mechanisms,
- we would like to improve our support for specific generic instances
- like ``MyGeneric[int]`` only. But, that's the best we can do for now.
-
- .. rubric:: Protocols
-
- We also support protocols. It has the same limitation as ``Generic`` types.
- It is also dispatched after all regular ``instance``s are checked.
-
- To work with protocols, one needs to pass ``is_protocol`` flag to instance:
-
- .. code:: python
-
- >>> from typing import Sequence
- >>> @example.instance(Sequence, is_protocol=True)
- ... def _sequence_example(instance: Sequence) -> str:
- ... return ','.join(str(item) for item in instance)
- ...
- >>> example([1, 2, 3])
- '1,2,3'
-
- But, ``str`` will still have higher priority over ``Sequence``:
-
- .. code:: python
-
- >>> example('abc')
- 'abc'
-
- We also support user-defined protocols:
-
- .. code:: python
-
- >>> from typing_extensions import Protocol
- >>> class CustomProtocol(Protocol):
- ... field: str
- ...
- >>> @example.instance(CustomProtocol, is_protocol=True)
- ... def _custom_protocol_example(instance: CustomProtocol) -> str:
- ... return instance.field
- ...
-
- Now, let's build a class that match this protocol and test it:
-
- .. code:: python
-
- >>> class WithField(object):
- ... field: str = 'with field'
- ...
- >>> example(WithField())
- 'with field'
-
- Remember, that generic protocols have the same limitation as generic types.
-
- """
- return _TypeClass(signature)
diff --git a/docs/_static/overrides.css b/docs/_static/overrides.css
new file mode 100644
index 0000000..6b596bb
--- /dev/null
+++ b/docs/_static/overrides.css
@@ -0,0 +1,14 @@
+.globaltoc > p.caption {
+ display: block;
+ font-size: 1.05em;
+ font-weight: 700;
+ text-decoration: none;
+ margin-bottom: 1em;
+ border: 0;
+}
+
+/* For some reason it did not have a scroll attached. */
+
+.mermaid {
+ overflow: scroll;
+}
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
new file mode 100644
index 0000000..93ffb17
--- /dev/null
+++ b/docs/_templates/layout.html
@@ -0,0 +1,5 @@
+{# layout.html #}
+{# Import the layout of the theme. #}
+{% extends "!layout.html" %}
+
+{% set css_files = css_files + ['_static/overrides.css'] %}
diff --git a/docs/conf.py b/docs/conf.py
index 7f5d778..d9b8bca 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -30,12 +30,12 @@ def _get_project_meta():
pkg_meta = _get_project_meta()
-project = pkg_meta['name']
-copyright = '2019, dry-python team' # noqa: A001
+project = str(pkg_meta['name'])
+copyright = '2019, dry-python team'
author = 'dry-python team'
# The short X.Y version
-version = pkg_meta['version']
+version = str(pkg_meta['version'])
# The full version, including alpha/beta/rc tags
release = version
@@ -55,7 +55,7 @@ def _get_project_meta():
'sphinx.ext.napoleon',
# Used to include .md files:
- 'm2r',
+ 'm2r2',
# Used to insert typehints into the final docs:
'sphinx_autodoc_typehints',
@@ -94,7 +94,7 @@ def _get_project_meta():
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
-language = None
+language = 'en'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
@@ -115,7 +115,6 @@ def _get_project_meta():
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
-#
html_theme = 'sphinx_typlog_theme'
# Theme options are theme-specific and customize the look and feel of a theme
@@ -123,9 +122,7 @@ def _get_project_meta():
# documentation.
html_theme_options = {
'logo_name': 'classes',
- 'description': (
- 'Make your functions return something meaningful, typed, and safe!'
- ),
+ 'description': 'Smart, pythonic, ad-hoc, typed polymorphism for Python',
'github_user': 'dry-python',
'github_repo': 'classes',
}
diff --git a/docs/index.rst b/docs/index.rst
index 5b9c2fc..d2807b9 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -14,9 +14,22 @@ Contents
:maxdepth: 2
:caption: Userguide
+ pages/why.rst
pages/concept.rst
- pages/typeclass.rst
- pages/typesafety.rst
+ pages/supports.rst
+ pages/generics.rst
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Tooling
+
+ pages/dry-python.rst
+
+.. toctree::
+ :maxdepth: 1
+ :caption: API
+
+ pages/api-docs.rst
.. toctree::
:maxdepth: 1
diff --git a/docs/pages/typeclass.rst b/docs/pages/api-docs.rst
similarity index 62%
rename from docs/pages/typeclass.rst
rename to docs/pages/api-docs.rst
index a9a4ede..23f32aa 100644
--- a/docs/pages/typeclass.rst
+++ b/docs/pages/api-docs.rst
@@ -1,7 +1,13 @@
Typeclass
=========
+Caching
+-------
+
+API
+---
+
Here are the technical docs about ``typeclass`` and how to use it.
-.. automodule:: classes.typeclass
+.. automodule:: classes._typeclass
:members:
diff --git a/docs/pages/concept.rst b/docs/pages/concept.rst
index ca27c5e..c2e7a0d 100644
--- a/docs/pages/concept.rst
+++ b/docs/pages/concept.rst
@@ -1,52 +1,20 @@
-The concept
-===========
+.. _concept:
-Typeclasses are another form of polymorphism
-that is widely used in some functional languages.
+Concept
+=======
-What's the point?
-Well, we need to do different logic based on input type.
+Typeclass
+---------
-Like ``len()`` function which
-works differently for ``"string"`` and ``[1, 2]``.
-Or ``+`` operator that works for numbers like "add"
-and for strings it works like "concatenate".
-
-Classes and interfaces
-~~~~~~~~~~~~~~~~~~~~~~
-
-Traditionally, object oriented languages solve it via classes.
-
-And classes are hard.
-They have internal state, inheritance, methods (including static ones),
-strong type, structure, class-level constants, life-cycle, and etc.
-
-Magic methods
-~~~~~~~~~~~~~
-
-That's why Python is not purely built around this idea.
-It also has protocols: ``__len__``, ``__iter__``, ``__add__``, etc.
-Which are called "magic mathods" most of the time.
-
-This really helps and keeps the language easy.
-But, have some serious problem:
-we cannot add new protocols / magic methods to the existing data types.
-
-You cannot add new methods to the ``list`` type (and that's a good thing!),
-you cannot also change how ``__len__`` for example work there.
-
-But, sometimes we really need this!
-One of the most simple example is ``json`` serialisation and deserialisation.
-Each type should be covered, each one works differently, they can nest.
-And moreover, it is 100% fine and expected
-to add your own types to this process.
-
-So, how does it work?
+So, typeclasses help us to build new abstractions near the existing types,
+not inside them.
+Basically, we will learn how to dispatch
+different logic based on predefined set of types.
Steps
------
+~~~~~
To use typeclasses you should understand these steps:
@@ -59,15 +27,16 @@ To use typeclasses you should understand these steps:
F2 --> F3["Calling"]
Let's walk through this process step by step.
-The first on is "Typeclass definition", where we create a new typeclass:
+The first one is "Typeclass definition", where we create a new typeclass:
.. code:: python
>>> from classes import typeclass
+ >>> from typing import Union
+
>>> @typeclass
- ... def json(instance) -> str:
- ... """That's definition!"""
- ...
+ ... def to_json(instance) -> str:
+ ... """That's a definition!"""
When typeclass is defined it only has a name and a signature
that all instances will share.
@@ -75,15 +44,18 @@ Let's define some instances:
.. code:: python
- >>> @json.instance(str)
- ... def _json_str(instance: str) -> str:
+ >>> @to_json.instance(str)
+ ... def _to_json_str(instance: str) -> str:
... return '"{0}"'.format(instance)
- ...
- >>> @json.instance(int)
- ... @json.instance(float)
- ... def _json_int_float(instance) -> str:
+
+ >>> @to_json.instance(int)
+ ... @to_json.instance(float)
+ ... def _to_json_int_float(instance: Union[float, int]) -> str:
... return str(instance)
- ...
+
+ >>> @to_json.instance(None)
+ ... def _to_json_none(instance: None) -> str:
+ ... return 'null'
That's how we define instances for our typeclass.
These instances will be executed when the corresponding type will be supplied.
@@ -93,12 +65,10 @@ with different value of different types:
.. code:: python
- >>> json('text')
- '"text"'
- >>> json(1)
- '1'
- >>> json(1.5)
- '1.5'
+ >>> assert to_json('text') == '"text"'
+ >>> assert to_json(1) == '1'
+ >>> assert to_json(1.5) == '1.5'
+ >>> assert to_json(None) == 'null'
That's it. There's nothing extra about typeclasses. They can be:
@@ -107,43 +77,468 @@ That's it. There's nothing extra about typeclasses. They can be:
- and called
-Related concepts
-----------------
+Protocols
+---------
+
+We also support ``Protocol`` items to be registered,
+the only difference is that they do require ``protocol=`` named argument
+to be specified on ``.instance()`` call:
+
+.. code:: python
+
+ >>> from typing import Sequence
+
+ >>> @to_json.instance(protocol=Sequence)
+ ... def _to_json_sequence(instance: Sequence) -> str:
+ ... return '[{0}]'.format(', '.join(to_json(i) for i in instance))
+
+ >>> assert to_json([1, 'a', None]) == '[1, "a", null]'
+
+
+Delegates
+---------
+
+Let's say that you want to handle types like ``Sequence[int]`` with ``classes``.
+The simple approach won't work, because Python cannot tell
+that some object is ``Sequence[int]`` or ``Sequence[str]``:
+
+.. code:: python
+
+ >>> from typing import Sequence
+
+ >>> isinstance([1, 2, 3], Sequence[int])
+ Traceback (most recent call last):
+ ...
+ TypeError: Subscripted generics cannot be used with class and instance checks
+
+``__instancecheck__`` magic method
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We need some custom type inference mechanism.
+For this purpose we use
+`__instancecheck__ `_
+magic method:
+
+.. code:: python
+
+ >>> from typing import List
+
+ >>> class _SequenceOfIntMeta(type):
+ ... def __instancecheck__(self, arg) -> bool:
+ ... try:
+ ... return (
+ ... # We need to have at least one `int` element
+ ... bool(arg) and
+ ... all(isinstance(item, int) for item in arg)
+ ... )
+ ... except TypeError:
+ ... return False
+
+ >>> class SequenceOfInt(List[int], metaclass=_SequenceOfIntMeta):
+ ... ...
+
+Now we can be sure that our ``List[int]`` can be checked in runtime:
+
+.. code:: python
+
+ >>> assert isinstance([1, 2, 3], SequenceOfInt) is True
+ >>> assert isinstance([1, 'a'], SequenceOfInt) is False
+ >>> assert isinstance([], SequenceOfInt) is False # empty
+
+``delegate`` argument
+~~~~~~~~~~~~~~~~~~~~~
+
+And now we can use it with ``classes``:
+
+.. code:: python
+
+ >>> from classes import typeclass
+ >>> from typing import Sequence
+
+ >>> @typeclass
+ ... def sum_all(instance) -> int:
+ ... ...
+
+ >>> @sum_all.instance(delegate=SequenceOfInt)
+ ... def _sum_all_list_int(instance: Sequence[int]) -> int:
+ ... return sum(instance)
+
+ >>> your_list = [1, 2, 3]
+ >>> assert sum_all(your_list) == 6
-singledispatch
-~~~~~~~~~~~~~~
+What happens here? When defining an instance with ``delegate`` argument,
+what we really do is: we add our ``delegate``
+into a special registry inside ``sum_all`` typeclass.
-One may ask, what is the difference
-with `singledispatch `_
-function from the standard library?
+This registry is using ``isinstance`` function
+to find handler that fits the defined predicate.
+It has the highest priority among other dispatch methods.
-The thing about ``singledispatch`` is that it allows almost the same features.
-But, it lacks type-safety.
-For example, it does not check for the same
-function signatures and return types in all cases:
+This allows us to sync both runtime and ``mypy`` behavior:
.. code:: python
- >>> from functools import singledispatch
- >>> @singledispatch
+ >>> # Mypy will raise a type error:
+ >>> # Argument 1 to "sum_all" has incompatible type "List[str]"; expected "List[int]"
+
+ >>> sum_all(['a', 'b'])
+ Traceback (most recent call last):
+ ...
+ NotImplementedError: Missing matched typeclass instance for type: list
+
+Phantom types
+~~~~~~~~~~~~~
+
+Notice, that ``SequenceOfInt`` is very verbose,
+it even has an explicit metaclass!
+
+There's a better way, 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 SequenceOfInt(
+ ... Sequence[int],
+ ... Phantom,
+ ... predicate=boolean.both(
+ ... collection.count(numeric.greater(0)),
+ ... collection.every(generic.of_type(int)),
+ ... ),
+ ... ):
+ ... ...
+
+ >>> assert isinstance([1, 2, 3], SequenceOfInt)
+ >>> 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 ``SequenceOfInt``
+- In runtime ``SequenceOfInt`` does not exist, because it is phantom!
+ In reality it is just ``Sequence[int]``.
+
+.. note::
+ Notice that newer versions of ``phantom-types`` do not accept the mutable
+ ``List`` type constructor for this purpose because you can add items of other
+ types to the list after the validation is done, which makes it unsafe.
+ Use immutable types like ``Sequence``.
+
+Now, we can define our typeclass with ``phantom`` type support:
+
+.. code:: python
+
+ >>> from phantom import Phantom
+ >>> from phantom.predicates import boolean, collection, generic, numeric
+
+ >>> class SequenceOfInt(
+ ... Sequence[int],
+ ... Phantom,
+ ... predicate=boolean.both(
+ ... collection.count(numeric.greater(0)),
+ ... collection.every(generic.of_type(int)),
+ ... ),
+ ... ):
+ ... ...
+
+ >>> from classes import typeclass
+
+ >>> @typeclass
+ ... def sum_all(instance) -> int:
+ ... ...
+
+ >>> @sum_all.instance(delegate=SequenceOfInt)
+ ... def _sum_all_list_int(instance: Sequence[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 ``Sequence[int]``,
+we delegate all the runtime type checking to ``SequenceOfInt`` phantom type.
+
+Performance considerations
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Types that are matched via ``__instancecheck__`` are the first one we try.
+Traversing the whole list to check that all elements
+are of the given type can be really slow.
+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.
+
+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.
+
+We recommend to think at least
+twice about the performance side of this feature.
+Maybe you can just write a function?
+
+
+Type resolution order
+---------------------
+
+Here's how typeclass resolve types:
+
+1. At first we try to resolve types via delegates and ``isinstance`` check
+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:
+
+.. code:: python
+
+ >>> from classes import typeclass
+
+ >>> @typeclass
... def example(instance) -> str:
- ... return 'default'
- ...
- >>> @example.register(int)
- ... def _example_int(instance: int, other: int) -> int:
- ... return instance + other
- ...
- >>> @example.register(str)
- ... def _example_str(instance: str) -> bool:
- ... return bool(instance)
- ...
- >>> bool(example(1, 0)) == example('a')
- True
+ ... ...
+
+ >>> class A(object):
+ ... ...
+
+ >>> class B(A):
+ ... ...
+
+ >>> @example.instance(A)
+ ... def _example_a(instance: A) -> str:
+ ... return 'a'
+
+Now, let's test that the fallback to more common types work:
+
+ >>> assert example(A()) == 'a'
+ >>> assert example(B()) == 'a'
+
+And now, let's specify a special case for ``B``:
+
+.. code:: python
+
+ >>> @example.instance(B)
+ ... def _example_b(instance: B) -> str:
+ ... return 'b'
+
+ >>> assert example(A()) == 'a'
+ >>> assert example(B()) == 'b'
+
+How it fallback works?
+We traverse the ``mro`` of a given type and find the closest supported type.
+This helps us to still treat first typeclass argument as covariant.
+
+There's even a pattern to allow all objects in:
+
+.. code:: python
+
+ >>> @example.instance(object)
+ ... def _example_all_in(instance: object) -> str:
+ ... return 'obj'
+
+ >>> assert example(A()) == 'a'
+ >>> assert example(B()) == 'b'
+
+ >>> assert example(1) == 'obj'
+ >>> assert example(None) == 'obj'
+ >>> assert example('a') == 'obj'
+
+
+Overriding and extending existing instances
+-------------------------------------------
+
+Sometimes we really need to override how things work.
+With objects and classes this can be problematic,
+because we would need to define a new subclass
+and chances are that it won't be used in some situations.
+
+With ``@typeclass`` overriding something is as easy.
+Let's define a typeclass with an instance to be overridden later:
+
+.. code:: python
+
+ >>> from classes import typeclass
+
+ >>> @typeclass
+ ... def example(instance) -> str:
+ ... ...
+
+ >>> @example.instance(str)
+ ... def _example_str(instance: str) -> str:
+ ... return instance.lower()
+
+ >>> assert example('Hello') == 'hello'
+
+Now, let's change how ``example`` behaves for ``str``.
+The only thing we need to do is to define ``.instance(str)`` once again:
+
+.. code:: python
+
+ >>> @example.instance(str)
+ ... def _example_str_new(instance: str) -> str:
+ ... return instance.upper()
+
+ >>> assert example('Hello') == 'HELLO'
+
+Note, that we can reuse the original implementation
+by calling the instance case directly:
+
+.. code:: python
+
+ >>> @example.instance(str)
+ ... def _example_str_new(instance: str) -> str:
+ ... return _example_str(instance) + '!'
+
+ >>> assert example('Hello') == 'hello!'
+
+
+supports typeguard
+------------------
+
+You can check if a typeclass is supported via ``.supports()`` method.
+Example:
+
+.. code:: python
+
+ >>> from classes import typeclass
+
+ >>> @typeclass
+ ... def convert_to_number(instance) -> int:
+ ... ...
+
+ >>> @convert_to_number.instance(int)
+ ... def _convert_int(instance: int) -> int:
+ ... return instance
+
+ >>> @convert_to_number.instance(float)
+ ... def _convert_float(instance: float) -> int:
+ ... return int(instance)
+
+ >>> assert convert_to_number.supports(1) is True
+ >>> assert convert_to_number.supports(1.5) is True
+ >>> assert convert_to_number.supports({}) is False
+
+It uses the same runtime dispatching mechanism as calling a typeclass directly,
+but returns a boolean.
+
+It also uses `TypeGuard `_ type
+to narrow types inside ``if convert_to_number.supports(item)`` blocks:
+
+.. code:: python
+
+ >>> from typing import Union
+ >>> from random import randint
+
+ >>> def get_random_item() -> Union[int, dict]:
+ ... return {'example': 1} if randint(0, 1) else 1
+
+ >>> item: Union[int, dict] = get_random_item()
+
+So, if you try to call ``convert_to_number(item)`` right now,
+it won't pass ``mypy`` typecheck and will possibly throw runtime exception,
+because ``dict`` is not supported by ``convert_to_number`` typeclass.
+
+So, you can narrow the type with our ``TypeGuard``:
+
+ >>> if convert_to_number.supports(item):
+ ... # `reveal_type(item)` will produce `Union[int, float]`,
+ ... # or basically all the types that are supported by `to_json`,
+ ... # now you can safely call `to_json`, `mypy` will be happy:
+ ... assert convert_to_number(1.5) == 1
+
+
+Typeclasses with associated types
+---------------------------------
+
+You can also define typeclasses with associated types.
+It will allow you to use ``Supports`` type later on.
+
+The syntax looks like this:
+
+.. code:: python
+
+ >>> from classes import AssociatedType, typeclass
+
+ >>> class CanBeTrimmed(AssociatedType): # Associated type definition
+ ... ...
+
+ >>> @typeclass(CanBeTrimmed)
+ ... def can_be_trimmed(instance, length: int) -> str:
+ ... ...
+
+The instance definition syntax is the same:
+
+.. code:: python
+
+ >>> @can_be_trimmed.instance(str)
+ ... def _can_be_trimmed_str(instance: str, length: int) -> str:
+ ... return instance[:length]
+
+ >>> assert can_be_trimmed('abcde', 3) == 'abc'
+
+Defining typeclasses as Python classes
+will be the only option if you need to use :ref:`Supports ` type.
+
+
+.. _type-restrictions:
+
+Type restrictions
+-----------------
+
+You can restrict typeclasses
+to have only subtypes of some specific types during typechecking
+(we will still accept all types in runtime).
+
+.. code:: python
+
+ >>> from classes import typeclass
+
+ >>> class A(object):
+ ... ...
+
+ >>> class B(A):
+ ... ...
+
+ >>> @typeclass
+ ... def example(instance: A) -> str:
+ ... ...
+
+With this setup, this will typecheck:
+
+.. code:: python
+
+ >>> @example.instance(A)
+ ... def _example_a(instance: A) -> str:
+ ... return 'a'
+
+ >>> @example.instance(B)
+ ... def _example_b(instance: B) -> str:
+ ... return 'b'
+
+ >>> assert example(A()) == 'a'
+ >>> assert example(B()) == 'b'
+
+But, this won't typecheck:
+
+.. code:: python
-As you can see: you are able to create
-instances with different return types and number of parameters.
+ >>> @example.instance(int)
+ ... def _example_int(instance: int) -> str:
+ ... return 'int'
-Good luck working with that!
+ # error: Instance "builtins.int" does not match original type "ex.A"
Further reading
@@ -151,3 +546,4 @@ Further reading
- `Wikipedia `_
- `Typeclasses in Haskell `_
+- `Typeclasses in Swift `_
diff --git a/docs/pages/dry-python.rst b/docs/pages/dry-python.rst
new file mode 100644
index 0000000..07dd2f2
--- /dev/null
+++ b/docs/pages/dry-python.rst
@@ -0,0 +1,34 @@
+dry-python ecosystem
+====================
+
+We support other ``dry-python`` projects.
+
+returns
+-------
+
+This project is designed to work together with `returns `_:
+
+.. code:: python
+
+ from classes import typeclass
+ from returns.result import Result
+
+ @typeclass
+ def result_to_str(instance) -> str:
+ """This is a typeclass definition to convert Result container to str."""
+
+ @result_to_str.instance(Result.success_type)
+ def _result_to_str_success(instance: Result) -> str:
+ """The sad part is that we cannot make this case generic..."""
+ return str(instance.unwrap()) # will always receive `Success` types
+
+ @result_to_str.instance(Result.failure_type)
+ def _result_to_str_failure(instance: Result) -> str:
+ """But we can still use cases to work with top-level types!"""
+ return str(instance.failure()) # will always receive `Failure` types
+
+Combining ``classes`` and ``returns`` allows you
+to write typed and declaratice business logic
+without almost any ``isintance`` calls.
+
+It also helps you with the value unwrapping from the ``Result`` container.
diff --git a/docs/pages/generics.rst b/docs/pages/generics.rst
new file mode 100644
index 0000000..4fe680d
--- /dev/null
+++ b/docs/pages/generics.rst
@@ -0,0 +1,221 @@
+Generics
+========
+
+
+Generic typeclasses
+-------------------
+
+You can define generic typeclasses, just like regular generic functions.
+You have to ways of doing this:
+
+- Via raw type variables like in ``def copy(instance: X) -> X``
+- Via instances with type variables like
+ in ``get_zero_item(instance: Sequence[X]) -> X``
+
+Using typevars
+~~~~~~~~~~~~~~
+
+When defining typeclasses with generic instances,
+we will typecheck that all instance definitions
+match the shape of the typeclass itself:
+
+.. code:: python
+
+ >>> from typing import TypeVar
+ >>> from classes import typeclass
+
+ >>> X = TypeVar('X')
+
+ >>> @typeclass
+ ... def copy(instance: X) -> X:
+ ... ...
+
+This one will typecheck correctly:
+
+.. code:: python
+
+ >>> @copy.instance(int)
+ ... def _copy_int(instance: int) -> int:
+ ... ...
+
+But, this won't:
+
+.. code:: python
+
+ @copy.instance(str)
+ def _copy_str(instance: str) -> bool:
+ ...
+
+ # error: Instance callback is incompatible "def (first: builtins.str, second: builtins.float) -> builtins.bool"; expected "def (first: builtins.str, second: builtins.str*) -> builtins.bool"
+
+Using instances
+~~~~~~~~~~~~~~~
+
+When using instances,
+you can define :ref:`type restrictions `
+to limit typeclass instances to only subtypes of a given restriction.
+
+Here's an example definition:
+
+.. code:: python
+
+ >>> from typing import Iterable, TypeVar, List
+ >>> from classes import typeclass
+
+ >>> X = TypeVar('X')
+
+ >>> @typeclass
+ ... def get_item(instance: Iterable[X], index: int) -> X:
+ ... ...
+
+This instance will match the definition:
+
+.. code:: python
+
+ >>> @get_item.instance(list)
+ ... def _get_item_list(instance: List[X], index: int) -> X:
+ ... ...
+
+ reveal_type(get_item([1, 2, 3], 0)) # Revealed type is "builtins.int*"
+ reveal_type(get_item(['a', 'b'], 0)) # Revealed type is "builtins.str*"
+
+But, this won't match and ``mypy`` will warn you:
+
+.. code:: python
+
+ @get_item.instance(int)
+ def _get_item_int(instance: int, index: int) -> int:
+ ...
+ # error: Instance callback is incompatible "def (instance: builtins.int, index: builtins.int) -> builtins.int"; expected "def [X] (instance: builtins.int, index: builtins.int) -> X`-1"
+ # error: Instance "builtins.int" does not match original type "typing.Iterable[X`-1]"
+
+
+Generic Supports type
+---------------------
+
+You can also use generic ``Supports`` type with generic ``AssociatedType``.
+
+To do so, you will need:
+1. Declare ``AssociatedType`` with type arguments, just like regular ``Generic``
+2. Use correct type arguments to define a variable
+
+Let's get back to ``get_item`` example and use a generic ``Supports`` type:
+
+.. code:: python
+
+ >>> from typing import Iterable, List, TypeVar
+ >>> from classes import AssociatedType, Supports, typeclass
+
+ >>> X = TypeVar('X')
+
+ >>> class GetItem(AssociatedType[X]):
+ ... ...
+
+ >>> @typeclass(GetItem)
+ ... def get_item(instance: Iterable[X], index: int) -> X:
+ ... ...
+
+ >>> numbers: Supports[GetItem[int]]
+ >>> strings: Supports[GetItem[str]]
+
+ reveal_type(get_item(numbers, 0)) # Revealed type is "builtins.int*"
+ reveal_type(get_item(strings, 0)) # Revealed type is "builtins.str*"
+
+
+Complex concrete generics
+-------------------------
+
+There are several advanced techniques
+in using concrete generic types when working with ``delegate`` types.
+
+Here's the collection of them.
+
+TypedDicts
+~~~~~~~~~~
+
+.. warning::
+ This example only works for Python 3.7 and 3.8
+ `Original bug report `_.
+
+At first, we need to define a typed dictionary itself:
+
+.. code:: python
+
+ >>> from typing_extensions import TypedDict
+ >>> from classes import typeclass
+
+ >>> class _User(TypedDict):
+ ... name: str
+ ... registered: bool
+
+Then, we need a special class with ``__instancecheck__`` defined.
+Because original ``TypedDict`` just raises
+a ``TypeError`` on ``isinstance(obj, User)``.
+
+.. code:: python
+
+ class _UserDictMeta(type(TypedDict)):
+ def __instancecheck__(cls, arg: object) -> bool:
+ return (
+ isinstance(arg, dict) and
+ isinstance(arg.get('name'), str) and
+ isinstance(arg.get('registered'), bool)
+ )
+
+ class UserDict(_User, metaclass=_UserDictMeta):
+ ...
+
+And finally we can use it!
+Take a note that we always use the resulting ``UserDict`` type,
+not the base ``_User``.
+
+.. code:: python
+
+ @typeclass
+ def get_name(instance) -> str:
+ ...
+
+ @get_name.instance(delegate=UserDict)
+ def _get_name_user_dict(instance: UserDict) -> str:
+ return instance['name']
+
+ user: UserDict = {'name': 'sobolevn', 'registered': True}
+ assert get_name(user) == 'sobolevn'
+
+Tuples of concrete shapes
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The logic is the same with concrete ``Tuple`` items:
+
+.. code:: python
+
+ >>> from typing import Tuple
+ >>> from classes import typeclass
+
+ >>> class UserTupleMeta(type):
+ ... def __instancecheck__(cls, arg: object) -> bool:
+ ... try:
+ ... return (
+ ... isinstance(arg, tuple) and
+ ... isinstance(arg[0], str) and
+ ... isinstance(arg[1], bool)
+ ... )
+ ... except IndexError:
+ ... return False
+
+ >>> class UserTuple(Tuple[str, bool], metaclass=UserTupleMeta):
+ ... ...
+
+ >>> @typeclass
+ ... def get_name(instance) -> str:
+ ... ...
+
+ >>> @get_name.instance(delegate=UserTuple)
+ ... def _get_name_user_dict(instance: Tuple[str, bool]) -> str:
+ ... return instance[0]
+
+ >>> assert get_name(('sobolevn', True)) == 'sobolevn'
+ >>> get_name((1, 2))
+ Traceback (most recent call last):
+ ...
+ NotImplementedError: Missing matched typeclass instance for type: tuple
diff --git a/docs/pages/supports.rst b/docs/pages/supports.rst
new file mode 100644
index 0000000..c517923
--- /dev/null
+++ b/docs/pages/supports.rst
@@ -0,0 +1,192 @@
+.. _supports:
+
+Supports
+========
+
+We also have a special type to help you specifying
+that you want to work with only types that are a part of a specific typeclass.
+
+.. warning::
+ ``Supports`` only works with typeclasses defined with associated types.
+
+
+Regular types
+-------------
+
+For example, you might want to work with only types
+that are able to be converted to JSON.
+
+You need to do several extra things:
+1. Define unique "associated type" for this typeclass
+2. Pass it during the typeclass definition
+
+.. code:: python
+
+ >>> from classes import AssociatedType, Supports, typeclass
+
+ >>> class ToJson(AssociatedType): # defining associated type
+ ... ...
+
+ >>> @typeclass(ToJson) # passing it to the typeclass
+ ... def to_json(instance) -> str:
+ ... ...
+
+ >>> @to_json.instance(int)
+ ... def _to_json_int(instance: int) -> str:
+ ... return str(instance)
+
+ >>> @to_json.instance(str)
+ ... def _to_json_str(instance: str) -> str:
+ ... return '"{0}"'.format(instance)
+
+ >>> def convert_to_json(
+ ... instance: Supports[ToJson],
+ ... ) -> str:
+ ... return to_json(instance)
+
+ >>> assert convert_to_json(1) == '1'
+ >>> assert convert_to_json('a') == '"a"'
+
+And this will fail (both in runtime and during type checking):
+
+ >>> # This will produce a mypy issue:
+ >>> # error: Argument 1 to "convert_to_json" has incompatible type "None";
+ >>> # expected "Supports[ToJson]"
+
+ >>> convert_to_json(None)
+ Traceback (most recent call last):
+ ...
+ NotImplementedError: Missing matched typeclass instance for type: NoneType
+
+
+Supports for instance annotations
+---------------------------------
+
+You can also use ``Supports`` as a type annotation for defining typeclasses:
+
+.. code:: python
+
+ >>> class MyFeature(AssociatedType):
+ ... ...
+
+ >>> @typeclass(MyFeature)
+ ... def my_feature(instance: 'Supports[MyFeature]') -> str:
+ ... ...
+
+It might be helpful, when you have ``no-untyped-def`` rule enabled.
+
+One more tip, our team would recommend this style:
+
+.. code:: python
+
+ >>> from typing_extensions import final
+
+ >>> @final # This type cannot have sub-types
+ ... class MyFeature(AssociatedType):
+ ... """Tell us, what this typeclass is about."""
+
+
+Supports and delegates
+----------------------
+
+``Supports`` type has a special handling of ``delegate`` types.
+Let's see an example. We would start with defining a ``delegate`` type:
+
+.. code:: python
+
+ >>> from typing import List
+ >>> from classes import AssociatedType, Supports, typeclass
+
+ >>> class ListOfIntMeta(type):
+ ... def __instancecheck__(cls, arg) -> bool:
+ ... return (
+ ... isinstance(arg, list) and
+ ... bool(arg) and
+ ... all(isinstance(list_item, int) for list_item in arg)
+ ... )
+
+ >>> class ListOfInt(List[int], metaclass=ListOfIntMeta):
+ ... ...
+
+Now, let's define a typeclass:
+
+.. code:: python
+
+ >>> class SumAll(AssociatedType):
+ ... ...
+
+ >>> @typeclass(SumAll)
+ ... def sum_all(instance) -> int:
+ ... ...
+
+ >>> @sum_all.instance(delegate=ListOfInt)
+ ... def _sum_all_list_int(
+ ... # It can be either `List[int]` or `ListOfInt`
+ ... instance: List[int],
+ ... ) -> int:
+ ... return sum(instance)
+
+And a function with ``Supports`` type:
+
+.. code:: python
+
+ >>> def test(to_sum: Supports[SumAll]) -> int:
+ ... return sum_all(to_sum)
+
+This will not make ``mypy`` happy:
+
+.. code:: python
+
+ >>> list1 = [1, 2, 3]
+ >>> assert test(list1) == 6 # Argument 1 to "test" has incompatible type "List[int]"; expected "Supports[SumAll]"
+
+It will be treated the same as unsupported cases, like ``List[str]``:
+
+.. code:: python
+
+ list2: List[str]
+ test(list2) # Argument 1 to "test" has incompatible type "List[int]"; expected "Supports[SumAll]"
+
+But, this will work correctly:
+
+.. code:: python
+
+ >>> list_of_int = ListOfInt([1, 2, 3])
+ >>> assert test(list_of_int) == 6 # ok
+
+ >>> list1 = [1, 2, 3]
+ >>> if isinstance(list1, ListOfInt):
+ ... assert test(list1) == 6 # ok
+
+This happens because we don't treat ``List[int]`` as ``Supports[SumAll]``.
+This is by design.
+
+But, we treat ``ListOfInt`` as ``Supports[SumAll]``.
+So, you would need to narrow ``List[int]`` to ``ListOfInt`` to make it work.
+
+Why? Because we insert ``Supports[SumAll]`` as a super-type of ``List``,
+there's no way currently to make ``List[int]`` supported
+and ``List[str]`` not supported.
+That's why we've decided to only make ``ListOfInt`` work.
+
+General cases
+~~~~~~~~~~~~~
+
+One way to make ``List[int]`` to work without explicit type narrowing
+is to define a generic case for all ``list`` subtypes:
+
+.. code:: python
+
+ >>> @sum_all.instance(list)
+ ... def _sum_all_list(instance: list) -> int:
+ ... return 0
+
+Now, this will work:
+
+.. code:: python
+
+ >>> list1 = [1, 2, 3]
+ >>> assert test(list1) == 6 # ok
+
+ >>> list2 = ['a', 'b']
+ >>> assert test(list2) == 0 # ok
diff --git a/docs/pages/typesafety.rst b/docs/pages/typesafety.rst
deleted file mode 100644
index ac3125c..0000000
--- a/docs/pages/typesafety.rst
+++ /dev/null
@@ -1,109 +0,0 @@
-.. _typesafety:
-
-Type-safety
-===========
-
-We try to bring as much type safety as possible.
-
-However, there are some limitations.
-This section will guide you through both
-features and troubles of type-safe typeclasses in Python.
-
-
-Features
---------
-
-First of all, we always check that the signature is the same for all cases.
-
-.. code:: python
-
- >>> from classes import typeclass
- >>> @typeclass
- ... def example(instance, arg: int, *, keyword: str) -> bool:
- ... ...
- ...
- >>> @example.instance(str)
- ... def _example_str(instance: str, arg: int, *, keyword: str) -> bool:
- ... return instance * arg + keyword
- ...
-
-Let's look at this example closely:
-
-1. We create a typeclass with the signature that all cases must share:
- except for the ``instance`` parameter, which is polymorphic.
-2. We check that return type of all cases match the original one:
- ``bool`` in our particular case
-3. We check that ``instance`` has the correct type in ``_example_str``
-
-When calling typeclasses, we also do the type check:
-
-.. code:: python
-
- >>> example('a', 3, keyword='b')
- 'aaab'
-
-Here are features that we support:
-
-1. Typechecker knows that we allow only defined instances
- to be the first (or instance) parameter in a call.
-2. We also check that other parameters do exist in the original signature
-3. We check the return type: it matches that defined one in the signature
-
-
-Limitations
------------
-
-We are limited in generics support.
-We support them, but without type parameters.
-
-- We support: ``list``, ``List``, ``Dict``,
- ``Mapping``, ``Iterable``, ``MyCustomGeneric``
-- We don't support ``List[int]``, ``Dict[str, str]``, ``Iterable[Any]``, etc
-
-Why? Because we cannot tell the difference
-between ``List[int]`` and ``List[str]`` in runtime.
-
-Python just does not have this information. It requires types to be infered.
-And that's currently not possible.
-
-So, this would not work:
-
-.. code:: python
-
- >>> from typing import List
- >>> from classes import typeclass
- >>> @typeclass
- ... def generic_typeclass(instance) -> str:
- ... """We use this example to demonstrate the typing limitation."""
- ...
- >>> @generic_typeclass.instance(List[int])
- ... def _generic_typeclass_list_int(instance: List[int]):
- ... ...
- ...
- Traceback (most recent call last):
- ...
- TypeError: ...
-
-
-But, this will (note that we use ``list`` inside ``.instance()`` call):
-
-.. code:: python
-
- >>> from typing import List
- >>> from classes import typeclass
- >>> @typeclass
- ... def generic_typeclass(instance) -> str:
- ... """We use this example to demonstrate the typing limitation."""
- ...
- >>> @generic_typeclass.instance(list)
- ... def _generic_typeclass_list_int(instance: List):
- ... return ''.join(str(list_item) for list_item in instance)
- ...
- >>> generic_typeclass([1, 2, 3])
- '123'
- >>> generic_typeclass(['a', 1, True])
- 'a1True'
-
-Use primitive generics as they always have ``Any`` inside.
-Annotations should also be bound to any parameters.
-Do not supply any other values there, we cannot even check for it.
diff --git a/docs/pages/why.rst b/docs/pages/why.rst
new file mode 100644
index 0000000..a2a9570
--- /dev/null
+++ b/docs/pages/why.rst
@@ -0,0 +1,242 @@
+What and why?
+=============
+
+Typeclasses are another
+`form of polymorphism `_
+that is widely used in some functional languages.
+
+Let's learn why would you can possibly need it in Python.
+
+
+Problem definition
+------------------
+
+Let's say you want some function to behave differently for different types
+(like ``len()`` does for ``str`` and ``dict`` types).
+
+What options do you traditionally have in Python?
+
+isinstance
+~~~~~~~~~~
+
+The easiest way to do that is to define
+a function with multiple ``isinstance`` cases:
+
+.. code:: python
+
+ >>> from typing import Union
+
+ >>> def my_len(container: Union[str, dict]) -> int:
+ ... if isinstance(container, str):
+ ... return len(container)
+ ... elif isinstance(container, dict):
+ ... return len(container.keys())
+ ... raise TypeError('Type {0} is not supported'.format(type(container)))
+
+ >>> assert my_len('abc') == 3
+ >>> assert my_len({}) == 0
+ >>> my_len(1)
+ Traceback (most recent call last):
+ ...
+ TypeError: Type is not supported
+
+It is a great solution if you know all types you need to support in advance.
+In our case it is ``Union[str, dict]``.
+
+But, it does not work if you want to have extandable set of supported types.
+
+Classes and methods
+~~~~~~~~~~~~~~~~~~~
+
+Traditionally, object oriented languages solve it via classes.
+
+For example, in our case of the custom ``len`` function,
+you will have to subclass some ``HasLength``
+super type to make ``len`` method available to your type:
+
+.. code:: python
+
+ >>> import abc
+
+ >>> class HasLength(object):
+ ... @abc.abstractmethod
+ ... def len(self) -> int:
+ ... """You have to implement this method to get the length."""
+
+And classes are hard.
+They have internal state, inheritance, methods (including static ones),
+structure, class-level constants, name conflicts, life-cycle, and etc.
+
+Moreover, it changes the semantics
+from a simple function to a full-featured classes and methods.
+Which is not always desirable.
+
+That's why Python is not purely built around this idea.
+It also has protocols: ``__len__``, ``__iter__``, ``__add__``, etc.
+Which are called
+`magic methods `_
+most of the time.
+This really helps and keeps the language easy.
+
+But, it also has some serious problem:
+we cannot add new protocols / magic methods to the existing data types.
+For example, we cannot add ``__len__`` to ``int`` even if we need to.
+We would have to create our own subtype of ``int``
+called ``MyInt`` with ``__len__`` defined.
+
+Of course, adding ``__len__`` to ``int`` type is not really useful,
+but, sometimes we really need this with other features!
+Like ``to_json`` or ``from_json`` when using serialization.
+
+One more philosophical problem with methods is that sometimes
+our "utilitary" methods break our abstractions.
+For example, imagine you have a typical domain ``Person`` class:
+
+.. code:: python
+
+ >>> from typing import Sequence
+
+ >>> class Person(object):
+ ... def become_friends(self, friend: 'Person') -> None:
+ ... ...
+ ...
+ ... def is_friend_of(self, person: 'Person') -> bool:
+ ... ...
+ ...
+ ... def get_pets(self) -> Sequence['Pet']:
+ ... ...
+
+And now, we want to make our ``Person`` JSON serializable.
+And our library requires this extra API:
+
+.. code:: diff
+
+ --- class Person(object):
+ +++ class Person(JSONSerializable):
+
+ +++ def to_json(self) -> str:
+ +++ ...
+
+ +++ def from_json(self, json_str: str) -> 'Person':
+ +++ ...
+
+But, now our domain models knows some ugly implementation details.
+And it will become even uglier in the future!
+
+Extra abstractions
+~~~~~~~~~~~~~~~~~~
+
+Ok, we cannot add new methods to the object itself,
+but we can create new extra abstractions. For example:
+
+.. code:: python
+
+ class PersonJSONSerializer(JSONSerializer):
+ """This type can serialize to JSON and deserialize `Person` objects."""
+
+This looks ok, doesn't it?
+Many popular libraries like
+`django-rest-framework `_
+use this approach.
+
+But, once again: we have shifted from a simple single
+function to a complex DSL around such a common task.
+
+It is now really hard to pass parameters and context
+through all abstraction levels,
+it is hard to track what types are supported and which are not.
+And it is impossible to express this with types when you need to do so:
+
+.. code:: python
+
+ def serialize_to_json(instance: '???') -> str:
+ ...
+
+ serialize_to_json(Person())
+
+And I am not even touching how hard it actually is to do some
+non-trivial things with DSLs like this in real life.
+
+singledispatch
+~~~~~~~~~~~~~~
+
+One more option, that is not so common, but native, is
+`functools.singledispatch `_.
+
+It is a great way to express our initial idea:
+different types behave differently for a single function.
+We can rewrite our initial ``my_len`` example like this:
+
+.. code:: python
+
+ >>> from functools import singledispatch
+
+ >>> @singledispatch
+ ... def my_len(container) -> int:
+ ... raise TypeError('Type {0} is not supported'.format(type(container)))
+
+ >>> @my_len.register
+ ... def _(container: str) -> int:
+ ... return len(container)
+
+ >>> @my_len.register
+ ... def _(container: dict) -> int:
+ ... return len(container.keys())
+
+ >>> assert my_len('abc') == 3
+ >>> assert my_len({}) == 0
+ >>> my_len(1)
+ Traceback (most recent call last):
+ ...
+ TypeError: Type is not supported
+
+And that's exactly what we are looking for!
+But, this still has some problems:
+
+1. Currently, ``mypy`` does not support typechecking ``singledispatch`` cases,
+ this is a temporary problem and people are working on this
+
+2. You still cannot express
+ "I need any object that supports ``my_len`` function" with a type annotation
+
+For example, ``mypy`` does not check for the same
+function signatures and return types in all cases:
+
+.. code:: python
+
+ >>> from functools import singledispatch
+
+ >>> @singledispatch
+ ... def example(instance) -> str:
+ ... return 'default'
+
+ >>> @example.register(int)
+ ... def _(instance: int, other: int) -> int:
+ ... return instance + other
+
+ >>> @example.register(str)
+ ... def _(instance: str) -> bool:
+ ... return bool(instance)
+
+ >>> example(2, 3)
+ 5
+ >>> example('a')
+ True
+
+As you can see: you are able to create
+instances with different return types and number of parameters.
+
+Good luck working with that!
+
+
+Typeclasses
+-----------
+
+That's why we are creating this library.
+It allows to:
+
+1. Have functions that behave differently on different types
+2. Express it with types using special :ref:`Supports ` annotation
+3. Be sure that your typings are always correct
+
+Now, let's dive into the :ref:`implementation ` details!
diff --git a/docs/requirements.txt b/docs/requirements.txt
deleted file mode 100644
index 8d76cc2..0000000
--- a/docs/requirements.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-# This file is used to setup env
-# to generate documentation.
-
-sphinx==2.2.0
-sphinx_autodoc_typehints==1.7.0
-sphinxcontrib-mermaid==0.3.1
-sphinx-typlog-theme==0.7.3
-recommonmark==0.6.0
-m2r==0.2.1
-tomlkit==0.5.5
-
-# Dependencies of our project:
-typing-extensions==3.7.4
diff --git a/poetry.lock b/poetry.lock
index 55f6db6..e5be7dc 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,1326 +1,2264 @@
[[package]]
-category = "dev"
-description = "A configurable sidebar-enabled Sphinx theme"
name = "alabaster"
+version = "0.7.12"
+description = "A configurable sidebar-enabled Sphinx theme"
+category = "dev"
optional = false
python-versions = "*"
-version = "0.7.12"
[[package]]
+name = "appdirs"
+version = "1.4.4"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
-description = "Read/rewrite/write Python ASTs"
-name = "astor"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "0.8.0"
+python-versions = "*"
[[package]]
+name = "astor"
+version = "0.8.1"
+description = "Read/rewrite/write Python ASTs"
category = "dev"
-description = "Atomic file writes."
-name = "atomicwrites"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "1.3.0"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
[[package]]
-category = "dev"
-description = "Classes Without Boilerplate"
name = "attrs"
+version = "21.4.0"
+description = "Classes Without Boilerplate"
+category = "dev"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "19.3.0"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.extras]
+dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"]
+docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
+tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"]
+tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"]
[[package]]
+name = "autorepr"
+version = "0.3.0"
+description = "Makes civilized __repr__, __str__, and __unicode__ methods"
category = "dev"
-description = "Internationalization utilities"
+optional = false
+python-versions = "*"
+
+[[package]]
name = "babel"
+version = "2.9.1"
+description = "Internationalization utilities"
+category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "2.7.0"
[package.dependencies]
pytz = ">=2015.7"
[[package]]
-category = "dev"
-description = "Security oriented static analyser for python code."
name = "bandit"
+version = "1.7.4"
+description = "Security oriented static analyser for python code."
+category = "dev"
optional = false
-python-versions = "*"
-version = "1.6.2"
+python-versions = ">=3.7"
[package.dependencies]
+colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""}
GitPython = ">=1.0.1"
-PyYAML = ">=3.13"
-colorama = ">=0.3.9"
-six = ">=1.10.0"
+PyYAML = ">=5.3.1"
stevedore = ">=1.20.0"
+[package.extras]
+test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "toml"]
+toml = ["toml"]
+yaml = ["PyYAML"]
+
[[package]]
+name = "beartype"
+version = "0.10.4"
+description = "Unbearably fast runtime type checking in pure Python."
category = "dev"
-description = "Easily capture stdout/stderr of the current process and subprocesses"
-name = "capturer"
optional = false
-python-versions = "*"
-version = "2.4"
+python-versions = ">=3.6.0"
-[package.dependencies]
-humanfriendly = ">=2.1"
+[package.extras]
+all = ["typing-extensions (>=3.10.0.0)"]
+dev = ["coverage (>=5.5)", "mypy (>=0.800)", "numpy", "pytest (>=4.0.0)", "sphinx", "sphinx (>=4.1.0)", "tox (>=3.20.1)", "typing-extensions"]
+doc-rtd = ["sphinx (==4.1.0)", "sphinx-rtd-theme (==0.5.1)"]
+test-tox = ["mypy (>=0.800)", "numpy", "pytest (>=4.0.0)", "sphinx", "typing-extensions"]
+test-tox-coverage = ["coverage (>=5.5)"]
[[package]]
+name = "cattrs"
+version = "1.10.0"
+description = "Composable complex class support for attrs and dataclasses."
category = "dev"
-description = "Python package for providing Mozilla's CA Bundle."
+optional = false
+python-versions = ">=3.7,<4.0"
+
+[package.dependencies]
+attrs = ">=20"
+typing_extensions = {version = "*", markers = "python_version >= \"3.7\" and python_version < \"3.8\""}
+
+[[package]]
name = "certifi"
+version = "2022.12.7"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "dev"
optional = false
-python-versions = "*"
-version = "2019.9.11"
+python-versions = ">=3.6"
[[package]]
+name = "charset-normalizer"
+version = "2.0.12"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "dev"
-description = "Universal encoding detector for Python 2 and 3"
-name = "chardet"
optional = false
-python-versions = "*"
-version = "3.0.4"
+python-versions = ">=3.5.0"
+
+[package.extras]
+unicode-backport = ["unicodedata2"]
[[package]]
+name = "chevron"
+version = "0.14.0"
+description = "Mustache templating language renderer"
category = "dev"
-description = "Composable command line interface toolkit"
+optional = false
+python-versions = "*"
+
+[[package]]
name = "click"
+version = "8.1.2"
+description = "Composable command line interface toolkit"
+category = "dev"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "7.0"
+python-versions = ">=3.7"
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[[package]]
+name = "codespell"
+version = "2.2.1"
+description = "Codespell"
category = "dev"
-description = "Cross-platform colored terminal text."
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+dev = ["check-manifest", "flake8", "pytest", "pytest-cov", "pytest-dependency"]
+hard-encoding-detection = ["chardet"]
+
+[[package]]
name = "colorama"
+version = "0.4.5"
+description = "Cross-platform colored terminal text."
+category = "dev"
optional = false
-python-versions = "*"
-version = "0.3.9"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
+name = "configupdater"
+version = "3.1"
+description = "Parser like ConfigParser but for updating configuration files"
category = "dev"
-description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
+[package.extras]
+testing = ["flake8", "pytest", "pytest-cov", "pytest-virtualenv", "pytest-xdist", "sphinx"]
+
+[[package]]
name = "coverage"
+version = "6.4.4"
+description = "Code coverage measurement for Python"
+category = "dev"
optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4"
-version = "4.5.4"
+python-versions = ">=3.7"
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
[[package]]
+name = "darglint"
+version = "1.8.1"
+description = "A utility for ensuring Google-style docstrings stay up to date with the source code."
category = "dev"
-description = "Better living through Python with decorators"
-name = "decorator"
optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*"
-version = "4.4.0"
+python-versions = ">=3.6,<4.0"
[[package]]
+name = "decorator"
+version = "5.1.1"
+description = "Decorators for Humans"
category = "dev"
-description = "Dictdiffer is a library that helps you to diff and patch dictionaries."
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
name = "dictdiffer"
+version = "0.9.0"
+description = "Dictdiffer is a library that helps you to diff and patch dictionaries."
+category = "dev"
optional = false
python-versions = "*"
-version = "0.8.0"
+
+[package.extras]
+all = ["Sphinx (>=3)", "check-manifest (>=0.42)", "mock (>=1.3.0)", "numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "numpy (>=1.20.0)", "pytest (==5.4.3)", "pytest (>=6)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "pytest-pycodestyle (>=2)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2)", "pytest-pydocstyle (>=2.2.0)", "sphinx (>=3)", "sphinx-rtd-theme (>=0.2)", "tox (>=3.7.0)"]
+docs = ["Sphinx (>=3)", "sphinx-rtd-theme (>=0.2)"]
+numpy = ["numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "numpy (>=1.20.0)"]
+tests = ["check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest (==5.4.3)", "pytest (>=6)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "pytest-pycodestyle (>=2)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2)", "pytest-pydocstyle (>=2.2.0)", "sphinx (>=3)", "tox (>=3.7.0)"]
[[package]]
-category = "dev"
-description = "Style checker for Sphinx (or other) RST documentation"
name = "doc8"
+version = "1.0.0"
+description = "Style checker for Sphinx (or other) RST documentation"
+category = "dev"
optional = false
-python-versions = "*"
-version = "0.8.0"
+python-versions = ">=3.7"
[package.dependencies]
-chardet = "*"
-docutils = "*"
+docutils = ">=0.19,<0.21"
+Pygments = "*"
restructuredtext-lint = ">=0.7"
-six = "*"
stevedore = "*"
+tomli = {version = "*", markers = "python_version < \"3.11\""}
[[package]]
-category = "dev"
-description = "Docutils -- Python Documentation Utilities"
name = "docutils"
+version = "0.19"
+description = "Docutils -- Python Documentation Utilities"
+category = "dev"
optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
-version = "0.15.2"
+python-versions = ">=3.7"
[[package]]
-category = "dev"
-description = "A parser for Python dependency files"
name = "dparse"
+version = "0.6.2"
+description = "A parser for Python dependency files"
+category = "dev"
optional = false
-python-versions = "*"
-version = "0.4.1"
+python-versions = ">=3.5"
[package.dependencies]
packaging = "*"
-pyyaml = "*"
-six = "*"
+toml = "*"
+
+[package.extras]
+conda = ["pyyaml"]
+pipenv = ["pipenv"]
[[package]]
+name = "dpath"
+version = "2.0.6"
+description = "Filesystem-like pathing and searching for dictionaries"
category = "dev"
-description = "Discover and load entry points from installed packages."
-name = "entrypoints"
optional = false
-python-versions = ">=2.7"
-version = "0.3"
+python-versions = ">=3"
[[package]]
-category = "dev"
-description = "Removes commented-out code."
name = "eradicate"
+version = "2.1.0"
+description = "Removes commented-out code."
+category = "dev"
optional = false
python-versions = "*"
-version = "1.0"
[[package]]
+name = "exceptiongroup"
+version = "1.1.0"
+description = "Backport of PEP 654 (exception groups)"
category = "dev"
-description = "the modular source code checker: pep8, pyflakes and co"
-name = "flake8"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "3.7.8"
+python-versions = ">=3.7"
-[package.dependencies]
-entrypoints = ">=0.3.0,<0.4.0"
-mccabe = ">=0.6.0,<0.7.0"
-pycodestyle = ">=2.5.0,<2.6.0"
-pyflakes = ">=2.1.0,<2.2.0"
+[package.extras]
+test = ["pytest (>=6)"]
[[package]]
+name = "flake8"
+version = "3.9.2"
+description = "the modular source code checker: pep8 pyflakes and co"
category = "dev"
-description = "A flake8 extension that checks for type annotations complexity"
-name = "flake8-annotations-complexity"
optional = false
-python-versions = "*"
-version = "0.0.2"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies]
-setuptools = "*"
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+mccabe = ">=0.6.0,<0.7.0"
+pycodestyle = ">=2.7.0,<2.8.0"
+pyflakes = ">=2.3.0,<2.4.0"
[[package]]
-category = "dev"
-description = "Automated security testing with bandit and flake8."
name = "flake8-bandit"
+version = "3.0.0"
+description = "Automated security testing with bandit and flake8."
+category = "dev"
optional = false
-python-versions = "*"
-version = "2.1.2"
+python-versions = ">=3.6"
[package.dependencies]
-bandit = "*"
+bandit = ">=1.7.3"
flake8 = "*"
flake8-polyfill = "*"
pycodestyle = "*"
[[package]]
-category = "dev"
-description = "Flake8 plugin to forbid backslashes for line breaks"
name = "flake8-broken-line"
+version = "0.5.0"
+description = "Flake8 plugin to forbid backslashes for line breaks"
+category = "dev"
optional = false
python-versions = ">=3.6,<4.0"
-version = "0.1.1"
[package.dependencies]
-flake8 = ">=3.5,<4.0"
+flake8 = ">=3.5,<6"
[[package]]
-category = "dev"
-description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle."
name = "flake8-bugbear"
+version = "22.9.23"
+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
-python-versions = ">=3.5"
-version = "19.8.0"
+python-versions = ">=3.6"
[package.dependencies]
-attrs = "*"
+attrs = ">=19.2.0"
flake8 = ">=3.0.0"
+[package.extras]
+dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit"]
+
[[package]]
+name = "flake8-commas"
+version = "2.1.0"
+description = "Flake8 lint for trailing commas."
category = "dev"
-description = "Check for python builtins being used as variables or parameters."
-name = "flake8-builtins"
optional = false
python-versions = "*"
-version = "1.4.1"
[package.dependencies]
-flake8 = "*"
+flake8 = ">=2"
[[package]]
+name = "flake8-comprehensions"
+version = "3.8.0"
+description = "A flake8 plugin to help you write better list/set/dict comprehensions."
category = "dev"
-description = "Adds coding magic comment checks to flake8"
-name = "flake8-coding"
optional = false
-python-versions = "*"
-version = "1.3.2"
+python-versions = ">=3.7"
[package.dependencies]
-flake8 = "*"
+flake8 = ">=3.0,<3.2.0 || >3.2.0"
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[[package]]
+name = "flake8-debugger"
+version = "4.0.0"
+description = "ipdb/pdb statement checker plugin for flake8"
category = "dev"
-description = "Flake8 lint for trailing commas."
-name = "flake8-commas"
optional = false
-python-versions = "*"
-version = "2.0.0"
+python-versions = ">=3.6"
[package.dependencies]
-flake8 = ">=2,<4.0.0"
+flake8 = ">=3.0"
+pycodestyle = "*"
+six = "*"
[[package]]
+name = "flake8-docstrings"
+version = "1.6.0"
+description = "Extension for flake8 which uses pydocstyle to check docstrings"
category = "dev"
-description = "A flake8 plugin to help you write better list/set/dict comprehensions."
-name = "flake8-comprehensions"
optional = false
-python-versions = ">=3.5"
-version = "2.2.0"
+python-versions = "*"
[package.dependencies]
-flake8 = "!=3.2.0"
+flake8 = ">=3"
+pydocstyle = ">=2.1"
[[package]]
+name = "flake8-eradicate"
+version = "1.2.0"
+description = "Flake8 plugin to find commented out code"
category = "dev"
-description = "ipdb/pdb statement checker plugin for flake8"
-name = "flake8-debugger"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
-version = "3.2.0"
+python-versions = ">=3.6,<4.0"
[package.dependencies]
-flake8 = ">=2.1"
-pycodestyle = "*"
-six = "*"
+attrs = "*"
+eradicate = ">=2.0,<3.0"
+flake8 = ">=3.5,<5"
[[package]]
+name = "flake8-isort"
+version = "4.1.1"
+description = "flake8 plugin that integrates isort ."
category = "dev"
-description = "Extension for flake8 which uses pydocstyle to check docstrings"
-name = "flake8-docstrings"
optional = false
python-versions = "*"
-version = "1.5.0"
[package.dependencies]
-flake8 = ">=3"
-pydocstyle = ">=2.1"
+flake8 = ">=3.2.1,<5"
+isort = ">=4.3.5,<6"
+testfixtures = ">=6.8.0,<7"
+
+[package.extras]
+test = ["pytest-cov"]
[[package]]
+name = "flake8-plugin-utils"
+version = "1.3.2"
+description = "The package provides base classes and utils for flake8 plugin writing"
category = "dev"
-description = "Flake8 plugin to find commented out code"
-name = "flake8-eradicate"
optional = false
python-versions = ">=3.6,<4.0"
-version = "0.2.3"
-
-[package.dependencies]
-attrs = ">=18.2,<20.0"
-eradicate = ">=0.2.1,<1.1.0"
-flake8 = ">=3.5,<4.0"
[[package]]
+name = "flake8-polyfill"
+version = "1.0.2"
+description = "Polyfill package for Flake8 plugins"
category = "dev"
-description = "A Flake8 plugin for checking executable permissions and shebangs."
-name = "flake8-executable"
optional = false
-python-versions = ">=3.6"
-version = "2.0.3"
+python-versions = "*"
[package.dependencies]
-flake8 = ">=3.0.0"
+flake8 = "*"
[[package]]
+name = "flake8-pytest-style"
+version = "1.6.0"
+description = "A flake8 plugin checking common style issues or inconsistencies with pytest-based tests."
category = "dev"
-description = "flake8 plugin that integrates isort ."
-name = "flake8-isort"
optional = false
-python-versions = "*"
-version = "2.7.0"
+python-versions = ">=3.6.2,<4.0.0"
[package.dependencies]
-flake8 = ">=3.2.1"
-isort = ">=4.3.0"
-testfixtures = "*"
+flake8-plugin-utils = ">=1.3.2,<2.0.0"
[[package]]
+name = "flake8-quotes"
+version = "3.3.1"
+description = "Flake8 lint for quotes."
category = "dev"
-description = "Flake8 extension to validate (lack of) logging format strings"
-name = "flake8-logging-format"
optional = false
python-versions = "*"
-version = "0.6.0"
+
+[package.dependencies]
+flake8 = "*"
[[package]]
+name = "flake8-rst-docstrings"
+version = "0.2.5"
+description = "Python docstring reStructuredText (RST) validator"
category = "dev"
-description = "Checks for old string formatting."
-name = "flake8-pep3101"
optional = false
-python-versions = "*"
-version = "1.2.1"
+python-versions = ">=3.6"
[package.dependencies]
-flake8 = ">=3.0"
+flake8 = ">=3.0.0"
+pygments = "*"
+restructuredtext-lint = "*"
[[package]]
+name = "flake8-string-format"
+version = "0.3.0"
+description = "string format checker, plugin for flake8"
category = "dev"
-description = "The package provides base classes and utils for flake8 plugin writing"
-name = "flake8-plugin-utils"
optional = false
-python-versions = ">=3.6,<4.0"
-version = "1.0.0"
+python-versions = "*"
+
+[package.dependencies]
+flake8 = "*"
[[package]]
+name = "flatten-dict"
+version = "0.4.2"
+description = "A flexible utility for flattening and unflattening dict-like objects in Python."
category = "dev"
-description = "Polyfill package for Flake8 plugins"
-name = "flake8-polyfill"
optional = false
-python-versions = "*"
-version = "1.0.2"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
-flake8 = "*"
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+six = ">=1.12,<2.0"
[[package]]
+name = "furl"
+version = "2.1.3"
+description = "URL manipulation made simple."
category = "dev"
-description = "print statement checker plugin for flake8"
-name = "flake8-print"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
-version = "3.1.1"
+python-versions = "*"
[package.dependencies]
-flake8 = ">=2.1"
-pycodestyle = "*"
-six = "*"
+orderedmultidict = ">=1.0.1"
+six = ">=1.8.0"
[[package]]
+name = "gitdb"
+version = "4.0.9"
+description = "Git Object Database"
category = "dev"
-description = "A plugin for flake8 to enable linting .pyi files."
-name = "flake8-pyi"
optional = false
python-versions = ">=3.6"
-version = "19.3.0"
[package.dependencies]
-attrs = "*"
-flake8 = ">=3.2.1"
-pyflakes = ">=2.1.1"
+smmap = ">=3.0.1,<6"
[[package]]
+name = "gitpython"
+version = "3.1.27"
+description = "GitPython is a python library used to interact with Git repositories"
category = "dev"
-description = "pytest assert checker plugin for flake8"
-name = "flake8-pytest"
optional = false
-python-versions = "*"
-version = "1.3"
+python-versions = ">=3.7"
[package.dependencies]
-flake8 = "*"
+gitdb = ">=4.0.1,<5"
+typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""}
[[package]]
+name = "identify"
+version = "2.4.12"
+description = "File identification library for Python"
category = "dev"
-description = "A flake8 plugin checking common style issues or inconsistencies with pytest-based tests."
-name = "flake8-pytest-style"
optional = false
-python-versions = ">=3.6,<4.0"
-version = "0.1.3"
+python-versions = ">=3.7"
-[package.dependencies]
-flake8-plugin-utils = ">=1.0,<2.0"
+[package.extras]
+license = ["ukkonen"]
[[package]]
+name = "idna"
+version = "3.3"
+description = "Internationalized Domain Names in Applications (IDNA)"
category = "dev"
-description = "Flake8 lint for quotes."
-name = "flake8-quotes"
optional = false
-python-versions = "*"
-version = "2.1.0"
-
-[package.dependencies]
-flake8 = "*"
+python-versions = ">=3.5"
[[package]]
+name = "imagesize"
+version = "1.3.0"
+description = "Getting image size from png/jpeg/jpeg2000/gif file"
category = "dev"
-description = "Python docstring reStructuredText (RST) validator"
-name = "flake8-rst-docstrings"
optional = false
-python-versions = "*"
-version = "0.0.11"
-
-[package.dependencies]
-flake8 = ">=3.0.0"
-restructuredtext_lint = "*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
+name = "importlib-metadata"
+version = "4.11.3"
+description = "Read metadata from Python packages"
category = "dev"
-description = "string format checker, plugin for flake8"
-name = "flake8-string-format"
optional = false
-python-versions = "*"
-version = "0.2.3"
+python-versions = ">=3.7"
[package.dependencies]
-flake8 = "*"
-
-[[package]]
-category = "dev"
-description = "Git Object Database"
-name = "gitdb2"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "2.0.6"
+typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
+zipp = ">=0.5"
-[package.dependencies]
-smmap2 = ">=2.0.0"
+[package.extras]
+docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"]
+perf = ["ipython"]
+testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
[[package]]
+name = "importlib-resources"
+version = "5.6.0"
+description = "Read resources from Python packages"
category = "dev"
-description = "Python Git Library"
-name = "gitpython"
optional = false
-python-versions = ">=3.0, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "3.0.4"
+python-versions = ">=3.7"
[package.dependencies]
-gitdb2 = ">=2.0.0"
+zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"]
+testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
-description = "Human friendly output for text interfaces using Python"
-name = "humanfriendly"
optional = false
python-versions = "*"
-version = "4.18"
-
-[package.dependencies]
-pyreadline = "*"
[[package]]
+name = "isort"
+version = "5.10.1"
+description = "A Python utility / library to sort Python imports."
category = "dev"
-description = "Internationalized Domain Names in Applications (IDNA)"
-name = "idna"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "2.8"
+python-versions = ">=3.6.1,<4.0"
-[[package]]
-category = "dev"
-description = "Getting image size from png/jpeg/jpeg2000/gif file"
-name = "imagesize"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "1.1.0"
+[package.extras]
+colors = ["colorama (>=0.4.3,<0.5.0)"]
+pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
+plugins = ["setuptools"]
+requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
+name = "jinja2"
+version = "3.1.1"
+description = "A very fast and expressive template engine."
category = "dev"
-description = "Read metadata from Python packages"
-marker = "python_version < \"3.8\""
-name = "importlib-metadata"
optional = false
-python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3"
-version = "0.23"
+python-versions = ">=3.7"
[package.dependencies]
-zipp = ">=0.5"
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
[[package]]
+name = "jmespath"
+version = "1.0.0"
+description = "JSON Matching Expressions"
category = "dev"
-description = "A Python utility / library to sort Python imports."
-name = "isort"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "4.3.21"
+python-versions = ">=3.7"
[[package]]
+name = "loguru"
+version = "0.6.0"
+description = "Python logging made (stupidly) simple"
category = "dev"
-description = "A very fast and expressive template engine."
-name = "jinja2"
optional = false
-python-versions = "*"
-version = "2.10.3"
+python-versions = ">=3.5"
[package.dependencies]
-MarkupSafe = ">=0.23"
+colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
+win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
-[[package]]
-category = "dev"
-description = "JSON Matching Expressions"
-name = "jmespath"
-optional = false
-python-versions = "*"
-version = "0.9.4"
+[package.extras]
+dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)"]
[[package]]
-category = "dev"
+name = "m2r2"
+version = "0.3.3"
description = "Markdown and reStructuredText in a single file."
-name = "m2r"
-optional = false
-python-versions = "*"
-version = "0.2.1"
-
-[package.dependencies]
-docutils = "*"
-mistune = "*"
-
-[[package]]
category = "dev"
-description = "Create Python CLI apps with little to no effort at all!"
-name = "mando"
optional = false
python-versions = "*"
-version = "0.6.4"
[package.dependencies]
-six = "*"
+docutils = ">=0.19"
+mistune = "0.8.4"
[[package]]
-category = "dev"
-description = "Safely add untrusted strings to HTML/XML markup."
name = "markupsafe"
+version = "2.1.1"
+description = "Safely add untrusted strings to HTML/XML markup."
+category = "dev"
optional = false
-python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
-version = "1.1.1"
+python-versions = ">=3.7"
[[package]]
-category = "dev"
-description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
name = "marshmallow"
+version = "3.15.0"
+description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
+category = "dev"
optional = false
-python-versions = ">=3.5"
-version = "3.2.1"
+python-versions = ">=3.7"
+
+[package.dependencies]
+packaging = "*"
+
+[package.extras]
+dev = ["flake8 (==4.0.1)", "flake8-bugbear (==22.1.11)", "mypy (==0.940)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"]
+docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.7)", "sphinx (==4.4.0)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"]
+lint = ["flake8 (==4.0.1)", "flake8-bugbear (==22.1.11)", "mypy (==0.940)", "pre-commit (>=2.4,<3.0)"]
+tests = ["pytest", "pytz", "simplejson"]
[[package]]
-category = "dev"
-description = "An unofficial extension to Marshmallow to allow for polymorphic fields"
name = "marshmallow-polyfield"
+version = "5.10"
+description = "An unofficial extension to Marshmallow to allow for polymorphic fields"
+category = "dev"
optional = false
-python-versions = "*"
-version = "5.7"
+python-versions = ">=3.5"
[package.dependencies]
marshmallow = ">=3.0.0b10"
-six = "*"
[[package]]
-category = "dev"
-description = "McCabe checker, plugin for flake8"
name = "mccabe"
+version = "0.6.1"
+description = "McCabe checker, plugin for flake8"
+category = "dev"
optional = false
python-versions = "*"
-version = "0.6.1"
[[package]]
-category = "dev"
-description = "The fastest markdown parser in pure Python"
name = "mistune"
+version = "0.8.4"
+description = "The fastest markdown parser in pure Python"
+category = "dev"
optional = false
python-versions = "*"
-version = "0.8.4"
[[package]]
-category = "dev"
-description = "More routines for operating on iterables, beyond itertools"
name = "more-itertools"
+version = "8.12.0"
+description = "More routines for operating on iterables, beyond itertools"
+category = "dev"
optional = false
-python-versions = ">=3.4"
-version = "7.2.0"
+python-versions = ">=3.5"
[[package]]
-category = "dev"
-description = "Optional static typing for Python"
name = "mypy"
+version = "0.942"
+description = "Optional static typing for Python"
+category = "dev"
optional = false
-python-versions = ">=3.5"
-version = "0.740"
+python-versions = ">=3.6"
[package.dependencies]
-mypy-extensions = ">=0.4.0,<0.5.0"
-typed-ast = ">=1.4.0,<1.5.0"
-typing-extensions = ">=3.7.4"
+mypy-extensions = ">=0.4.3"
+tomli = ">=1.1.0"
+typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""}
+typing-extensions = ">=3.10"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+python2 = ["typed-ast (>=1.4.0,<2)"]
+reports = ["lxml"]
[[package]]
-category = "dev"
-description = "Experimental type system extensions for programs checked with the mypy typechecker."
name = "mypy-extensions"
+version = "0.4.3"
+description = "Experimental type system extensions for programs checked with the mypy typechecker."
+category = "dev"
optional = false
python-versions = "*"
-version = "0.4.3"
[[package]]
-category = "dev"
-description = "Flake8 plugin to enforce the same lint configuration (flake8, isort, mypy, pylint) across multiple Python projects"
name = "nitpick"
+version = "0.32.0"
+description = "Enforce the same settings across multiple language-independent projects"
+category = "dev"
optional = false
-python-versions = ">=3.5,<4.0"
-version = "0.21.1"
+python-versions = ">=3.7,<4.0"
[package.dependencies]
-attrs = "*"
+attrs = ">=20.1.0"
+autorepr = "*"
click = "*"
+ConfigUpdater = "*"
dictdiffer = "*"
+dpath = "*"
flake8 = ">=3.0.0"
+flatten-dict = "*"
+furl = "*"
+identify = "*"
+importlib-resources = {version = "*", markers = "python_version >= \"3.7\" and python_version < \"3.9\""}
jmespath = "*"
+loguru = "*"
marshmallow = ">=3.0.0b10"
-marshmallow-polyfield = ">=5.7,<6.0"
+marshmallow-polyfield = ">=5.10,<6.0"
+more-itertools = "*"
+pluggy = "*"
python-slugify = "*"
requests = "*"
+requests-cache = "*"
"ruamel.yaml" = "*"
sortedcontainers = "*"
+StrEnum = "*"
toml = "*"
+tomlkit = ">=0.8.0"
+
+[package.extras]
+doc = ["sphinx", "sphinx-gitref", "sphinx_rtd_theme", "sphobjinv"]
+lint = ["pylint"]
+test = ["freezegun", "pytest", "pytest-cov", "pytest-datadir", "pytest-socket", "pytest-testmon", "pytest-watch", "responses", "testfixtures"]
[[package]]
+name = "numerary"
+version = "0.3.0"
+description = "Python hacks for type-checking numbers"
category = "dev"
-description = "Core utilities for Python packages"
-name = "packaging"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "19.2"
+python-versions = ">=3.7"
[package.dependencies]
-pyparsing = ">=2.0.2"
-six = "*"
+typing-extensions = {version = ">=3.10", markers = "python_version < \"3.9\""}
+
+[package.extras]
+dev = ["beartype (>=0.8,!=0.9.0)", "numpy", "pre-commit", "setuptools-scm", "sympy (>=1.9)", "tox"]
[[package]]
+name = "numerary"
+version = "0.4.2"
+description = "Python hacks for type-checking numbers"
category = "dev"
-description = "Python Build Reasonableness"
-name = "pbr"
optional = false
-python-versions = "*"
-version = "5.4.3"
+python-versions = ">=3.8"
+
+[package.dependencies]
+beartype = ">=0.10.4,<1.0.0"
+
+[package.extras]
+dev = ["numpy", "pre-commit", "setuptools-scm", "sympy (>=1.9)", "tox"]
[[package]]
+name = "orderedmultidict"
+version = "1.0.1"
+description = "Ordered Multivalue Dictionary"
category = "dev"
-description = "Check PEP-8 naming conventions, plugin for flake8"
-name = "pep8-naming"
optional = false
python-versions = "*"
-version = "0.8.2"
[package.dependencies]
-flake8-polyfill = ">=1.0.2,<2"
+six = ">=1.8.0"
[[package]]
+name = "packaging"
+version = "21.3"
+description = "Core utilities for Python packages"
category = "dev"
-description = "plugin and hook calling mechanisms for python"
-name = "pluggy"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "0.13.0"
+python-versions = ">=3.6"
[package.dependencies]
-[package.dependencies.importlib-metadata]
-python = "<3.8"
-version = ">=0.12"
+pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
+name = "pbr"
+version = "5.8.1"
+description = "Python Build Reasonableness"
category = "dev"
-description = "library with cross-python path, ini-parsing, io, code, log facilities"
-name = "py"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "1.8.0"
+python-versions = ">=2.6"
[[package]]
+name = "pep8-naming"
+version = "0.13.2"
+description = "Check PEP-8 naming conventions, plugin for flake8"
category = "dev"
-description = "Python style guide checker"
-name = "pycodestyle"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "2.5.0"
+python-versions = ">=3.7"
+
+[package.dependencies]
+flake8 = ">=3.9.1"
[[package]]
+name = "phantom-types"
+version = "1.0.0"
+description = "Phantom types for Python"
category = "dev"
-description = "Python docstring style checker"
-name = "pydocstyle"
optional = false
-python-versions = ">=3.4"
-version = "4.0.1"
+python-versions = ">=3.7"
[package.dependencies]
-snowballstemmer = "*"
+numerary = [
+ {version = ">=0.3.0,<0.4", markers = "python_version < \"3.8\""},
+ {version = ">=0.4.0", markers = "python_version >= \"3.8\""},
+]
+typeguard = ">=2.10"
+typing-extensions = ">=4.3.0"
+
+[package.extras]
+all = ["phantom-types[dateutil]", "phantom-types[phonenumbers]", "phantom-types[pydantic]"]
+dateutil = ["python-dateutil (>=2.8.2)"]
+phonenumbers = ["phonenumbers (>=8.12.41)"]
+pydantic = ["pydantic (>=1.9.0)"]
+test = ["coverage", "mypy (>=0.930)", "pytest", "pytest-mypy-plugins (>=1.9.3)"]
[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pycodestyle"
+version = "2.7.0"
+description = "Python style guide checker"
category = "dev"
-description = "passive checker of Python programs"
-name = "pyflakes"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "2.1.1"
[[package]]
+name = "pydocstyle"
+version = "6.1.1"
+description = "Python docstring style checker"
category = "dev"
-description = "Pygments is a syntax highlighting package written in Python."
-name = "pygments"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "2.4.2"
+python-versions = ">=3.6"
+
+[package.dependencies]
+snowballstemmer = "*"
+
+[package.extras]
+toml = ["toml"]
[[package]]
+name = "pyflakes"
+version = "2.3.1"
+description = "passive checker of Python programs"
category = "dev"
-description = "Python parsing module"
-name = "pyparsing"
optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
-version = "2.4.2"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
+name = "Pygments"
+version = "2.13.0"
+description = "Pygments is a syntax highlighting package written in Python."
category = "dev"
-description = "A python implmementation of GNU readline."
-marker = "sys_platform == \"win32\""
-name = "pyreadline"
optional = false
-python-versions = "*"
-version = "2.1"
+python-versions = ">=3.6"
+
+[package.extras]
+plugins = ["importlib-metadata"]
[[package]]
+name = "pyparsing"
+version = "3.0.8"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "dev"
-description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.6.8"
+
+[package.extras]
+diagrams = ["jinja2", "railroad-diagrams"]
+
+[[package]]
name = "pytest"
+version = "7.2.0"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
optional = false
-python-versions = ">=3.5"
-version = "5.2.2"
+python-versions = ">=3.7"
[package.dependencies]
-atomicwrites = ">=1.0"
-attrs = ">=17.4.0"
-colorama = "*"
-more-itertools = ">=4.0.0"
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+iniconfig = "*"
packaging = "*"
-pluggy = ">=0.12,<1.0"
-py = ">=1.5.0"
-wcwidth = "*"
+pluggy = ">=0.12,<2.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
-[package.dependencies.importlib-metadata]
-python = "<3.8"
-version = ">=0.12"
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
-category = "dev"
-description = "Pytest plugin for measuring coverage."
name = "pytest-cov"
+version = "4.0.0"
+description = "Pytest plugin for measuring coverage."
+category = "dev"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "2.8.1"
+python-versions = ">=3.6"
[package.dependencies]
-coverage = ">=4.4"
-pytest = ">=3.6"
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
[[package]]
-category = "dev"
-description = "pytest plugin for writing tests for mypy plugins"
name = "pytest-mypy-plugins"
+version = "1.9.3"
+description = "pytest plugin for writing tests for mypy plugins"
+category = "dev"
optional = false
-python-versions = "*"
-version = "1.1.0"
+python-versions = ">=3.6"
[package.dependencies]
-capturer = "*"
+chevron = "*"
decorator = "*"
-mypy = ">=0.730"
-pytest = "*"
+mypy = ">=0.900"
+pytest = ">=6.0.0"
pyyaml = "*"
+regex = "*"
[[package]]
-category = "dev"
-description = "Pytest plugin to randomly order tests and control random.seed."
name = "pytest-randomly"
+version = "3.12.0"
+description = "Pytest plugin to randomly order tests and control random.seed."
+category = "dev"
optional = false
-python-versions = ">=3.5"
-version = "3.1.0"
+python-versions = ">=3.7"
[package.dependencies]
-entrypoints = "*"
+importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""}
pytest = "*"
[[package]]
-category = "dev"
-description = "A Python Slugify application that handles Unicode"
name = "python-slugify"
+version = "6.1.1"
+description = "A Python slugify application that also handles Unicode"
+category = "dev"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "4.0.0"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[package.dependencies]
text-unidecode = ">=1.3"
+[package.extras]
+unidecode = ["Unidecode (>=1.1.1)"]
+
[[package]]
-category = "dev"
-description = "World timezone definitions, modern and historical"
name = "pytz"
+version = "2022.1"
+description = "World timezone definitions, modern and historical"
+category = "dev"
optional = false
python-versions = "*"
-version = "2019.3"
[[package]]
-category = "dev"
-description = "YAML parser and emitter for Python"
name = "pyyaml"
+version = "6.0"
+description = "YAML parser and emitter for Python"
+category = "dev"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "5.1.2"
+python-versions = ">=3.6"
[[package]]
+name = "regex"
+version = "2022.3.15"
+description = "Alternative regular expression module, to replace re."
category = "dev"
-description = "Code Metrics in Python"
-name = "radon"
optional = false
-python-versions = "*"
-version = "2.4.0"
-
-[package.dependencies]
-colorama = ">=0.3,<0.4"
-flake8-polyfill = "*"
-mando = ">=0.6,<0.7"
+python-versions = ">=3.6"
[[package]]
-category = "dev"
-description = "Python HTTP for Humans."
name = "requests"
+version = "2.27.1"
+description = "Python HTTP for Humans."
+category = "dev"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "2.22.0"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[package.dependencies]
certifi = ">=2017.4.17"
-chardet = ">=3.0.2,<3.1.0"
-idna = ">=2.5,<2.9"
-urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
+charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
+idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"]
[[package]]
+name = "requests-cache"
+version = "0.9.3"
+description = "A transparent persistent cache for the requests library"
category = "dev"
-description = "reStructuredText linter"
+optional = false
+python-versions = ">=3.7,<4.0"
+
+[package.dependencies]
+appdirs = ">=1.4.4,<2.0.0"
+attrs = ">=21.2,<22.0"
+cattrs = ">=1.8,<2.0"
+requests = ">=2.22,<3.0"
+url-normalize = ">=1.4,<2.0"
+urllib3 = ">=1.25.5,<2.0.0"
+
+[package.extras]
+all = ["boto3 (>=1.15,<2.0)", "botocore (>=1.18,<2.0)", "itsdangerous (>=2.0,<3.0)", "pymongo (>=3,<5)", "pyyaml (>=5.4)", "redis (>=3,<5)", "ujson (>=4.0)"]
+bson = ["bson (>=0.5)"]
+docs = ["furo (>=2021.9.8)", "linkify-it-py (>=1.0.1,<2.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx (==4.3.0)", "sphinx-autodoc-typehints (>=1.11,<2.0)", "sphinx-automodapi (>=0.13,<0.15)", "sphinx-copybutton (>=0.3,<0.5)", "sphinx-inline-tabs (>=2022.1.2b11)", "sphinx-notfound-page", "sphinx-panels (>=0.6,<0.7)", "sphinxcontrib-apidoc (>=0.3,<0.4)"]
+dynamodb = ["boto3 (>=1.15,<2.0)", "botocore (>=1.18,<2.0)"]
+json = ["ujson (>=4.0)"]
+mongodb = ["pymongo (>=3,<5)"]
+redis = ["redis (>=3,<5)"]
+security = ["itsdangerous (>=2.0,<3.0)"]
+yaml = ["pyyaml (>=5.4)"]
+
+[[package]]
name = "restructuredtext-lint"
+version = "1.4.0"
+description = "reStructuredText linter"
+category = "dev"
optional = false
python-versions = "*"
-version = "1.3.0"
[package.dependencies]
docutils = ">=0.11,<1.0"
[[package]]
-category = "dev"
-description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
name = "ruamel.yaml"
+version = "0.17.21"
+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
-python-versions = "*"
-version = "0.16.5"
+python-versions = ">=3"
[package.dependencies]
-[package.dependencies."ruamel.yaml.clib"]
-python = "<3.8"
-version = ">=0.1.2"
+"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""}
+
+[package.extras]
+docs = ["ryd"]
+jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
[[package]]
-category = "dev"
-description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
-marker = "platform_python_implementation == \"CPython\" and python_version < \"3.8\""
name = "ruamel.yaml.clib"
+version = "0.2.6"
+description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
+category = "dev"
optional = false
-python-versions = "*"
-version = "0.2.0"
+python-versions = ">=3.5"
[[package]]
-category = "dev"
-description = "Safety checks your installed dependencies for known security vulnerabilities."
name = "safety"
+version = "2.3.5"
+description = "Checks installed dependencies for known vulnerabilities and licenses."
+category = "dev"
optional = false
python-versions = "*"
-version = "1.8.5"
[package.dependencies]
-Click = ">=6.0"
-dparse = ">=0.4.1"
-packaging = "*"
+Click = ">=8.0.2"
+dparse = ">=0.6.2"
+packaging = ">=21.0,<22.0"
requests = "*"
-setuptools = "*"
+"ruamel.yaml" = ">=0.17.21"
+setuptools = ">=19.3"
+
+[package.extras]
+github = ["jinja2 (>=3.1.0)", "pygithub (>=1.43.3)"]
+gitlab = ["python-gitlab (>=1.3.0)"]
[[package]]
+name = "setuptools"
+version = "65.5.1"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "dev"
-description = "Python 2 and 3 compatibility utilities"
-name = "six"
optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*"
-version = "1.12.0"
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
category = "dev"
-description = "A pure Python implementation of a sliding window memory map manager"
-name = "smmap2"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "2.0.5"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
+name = "smmap"
+version = "5.0.0"
+description = "A pure Python implementation of a sliding window memory map manager"
category = "dev"
-description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms."
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
name = "snowballstemmer"
+version = "2.2.0"
+description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
+category = "dev"
optional = false
python-versions = "*"
-version = "2.0.0"
[[package]]
-category = "dev"
-description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
name = "sortedcontainers"
+version = "2.4.0"
+description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
+category = "dev"
optional = false
python-versions = "*"
-version = "2.1.0"
[[package]]
-category = "dev"
-description = "Python documentation generator"
name = "sphinx"
+version = "5.3.0"
+description = "Python documentation generator"
+category = "dev"
optional = false
-python-versions = ">=3.5"
-version = "2.2.0"
+python-versions = ">=3.6"
[package.dependencies]
-Jinja2 = ">=2.3"
-Pygments = ">=2.0"
alabaster = ">=0.7,<0.8"
-babel = ">=1.3,<2.0 || >2.0"
-colorama = ">=0.3.5"
-docutils = ">=0.12"
-imagesize = "*"
-packaging = "*"
+babel = ">=2.9"
+colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
+docutils = ">=0.14,<0.20"
+imagesize = ">=1.3"
+importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""}
+Jinja2 = ">=3.0"
+packaging = ">=21.0"
+Pygments = ">=2.12"
requests = ">=2.5.0"
-setuptools = "*"
-snowballstemmer = ">=1.1"
+snowballstemmer = ">=2.0"
sphinxcontrib-applehelp = "*"
sphinxcontrib-devhelp = "*"
-sphinxcontrib-htmlhelp = "*"
+sphinxcontrib-htmlhelp = ">=2.0.0"
sphinxcontrib-jsmath = "*"
sphinxcontrib-qthelp = "*"
-sphinxcontrib-serializinghtml = "*"
+sphinxcontrib-serializinghtml = ">=1.1.5"
+
+[package.extras]
+docs = ["sphinxcontrib-websupport"]
+lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"]
+test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"]
[[package]]
-category = "dev"
-description = "Type hints (PEP 484) support for the Sphinx autodoc extension"
name = "sphinx-autodoc-typehints"
+version = "1.20.0"
+description = "Type hints (PEP 484) support for the Sphinx autodoc extension"
+category = "dev"
optional = false
-python-versions = ">=3.5.2"
-version = "1.8.0"
+python-versions = ">=3.7"
[package.dependencies]
-Sphinx = ">=2.1"
+sphinx = ">=5.3"
+
+[package.extras]
+docs = ["furo (>=2022.9.29)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"]
+testing = ["covdefaults (>=2.2)", "coverage (>=6.5)", "diff-cover (>=7.0.1)", "nptyping (>=2.3.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "sphobjinv (>=2.2.2)", "typing-extensions (>=4.4)"]
+type-comment = ["typed-ast (>=1.5.4)"]
[[package]]
-category = "dev"
-description = "A typlog Sphinx theme"
name = "sphinx-typlog-theme"
+version = "0.8.0"
+description = "A typlog Sphinx theme"
+category = "dev"
optional = false
python-versions = "*"
-version = "0.7.3"
+
+[package.extras]
+dev = ["livereload", "sphinx"]
[[package]]
-category = "dev"
-description = ""
name = "sphinxcontrib-applehelp"
+version = "1.0.2"
+description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
+category = "dev"
optional = false
-python-versions = "*"
-version = "1.0.1"
+python-versions = ">=3.5"
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+test = ["pytest"]
[[package]]
-category = "dev"
-description = ""
name = "sphinxcontrib-devhelp"
+version = "1.0.2"
+description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
+category = "dev"
optional = false
-python-versions = "*"
-version = "1.0.1"
+python-versions = ">=3.5"
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+test = ["pytest"]
[[package]]
-category = "dev"
-description = ""
name = "sphinxcontrib-htmlhelp"
+version = "2.0.0"
+description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
+category = "dev"
optional = false
-python-versions = "*"
-version = "1.0.2"
+python-versions = ">=3.6"
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+test = ["html5lib", "pytest"]
[[package]]
-category = "dev"
-description = "A sphinx extension which renders display math in HTML via JavaScript"
name = "sphinxcontrib-jsmath"
+version = "1.0.1"
+description = "A sphinx extension which renders display math in HTML via JavaScript"
+category = "dev"
optional = false
python-versions = ">=3.5"
-version = "1.0.1"
+
+[package.extras]
+test = ["flake8", "mypy", "pytest"]
[[package]]
-category = "dev"
-description = "Mermaid diagrams in yours Sphinx powered docs"
name = "sphinxcontrib-mermaid"
+version = "0.7.1"
+description = "Mermaid diagrams in yours Sphinx powered docs"
+category = "dev"
optional = false
python-versions = "*"
-version = "0.3.1"
[[package]]
-category = "dev"
-description = ""
name = "sphinxcontrib-qthelp"
+version = "1.0.3"
+description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
+category = "dev"
optional = false
-python-versions = "*"
-version = "1.0.2"
+python-versions = ">=3.5"
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+test = ["pytest"]
[[package]]
-category = "dev"
-description = ""
name = "sphinxcontrib-serializinghtml"
+version = "1.1.5"
+description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
+category = "dev"
optional = false
-python-versions = "*"
-version = "1.1.3"
+python-versions = ">=3.5"
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+test = ["pytest"]
[[package]]
-category = "dev"
-description = "Manage dynamic plugins for Python applications"
name = "stevedore"
+version = "3.5.0"
+description = "Manage dynamic plugins for Python applications"
+category = "dev"
optional = false
-python-versions = "*"
-version = "1.31.0"
+python-versions = ">=3.6"
[package.dependencies]
+importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""}
pbr = ">=2.0.0,<2.1.0 || >2.1.0"
-six = ">=1.10.0"
[[package]]
+name = "strenum"
+version = "0.4.7"
+description = "An Enum that inherits from str."
category = "dev"
-description = "A collection of helpers and mock objects for unit tests and doc tests."
-name = "testfixtures"
optional = false
python-versions = "*"
-version = "6.10.0"
+
+[package.extras]
+docs = ["recommonmark", "sphinx", "sphinx-rtd-theme"]
+release = ["twine"]
+test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"]
[[package]]
+name = "testfixtures"
+version = "6.18.5"
+description = "A collection of helpers and mock objects for unit tests and doc tests."
category = "dev"
-description = "The most basic Text::Unidecode port"
-name = "text-unidecode"
optional = false
python-versions = "*"
-version = "1.3"
+
+[package.extras]
+build = ["setuptools-git", "twine", "wheel"]
+docs = ["django", "django (<2)", "mock", "sphinx", "sybil", "twisted", "zope.component"]
+test = ["django", "django (<2)", "mock", "pytest (>=3.6)", "pytest-cov", "pytest-django", "sybil", "twisted", "zope.component"]
[[package]]
+name = "text-unidecode"
+version = "1.3"
+description = "The most basic Text::Unidecode port"
category = "dev"
-description = "Python Library for Tom's Obvious, Minimal Language"
-name = "toml"
optional = false
python-versions = "*"
-version = "0.10.0"
[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
-description = "Style preserving TOML library"
-name = "tomlkit"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "0.5.8"
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
category = "dev"
-description = "a fork of Python 2 and 3 ast modules with type comment support"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "tomlkit"
+version = "0.11.6"
+description = "Style preserving TOML library"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
name = "typed-ast"
+version = "1.5.2"
+description = "a fork of Python 2 and 3 ast modules with type comment support"
+category = "dev"
optional = false
-python-versions = "*"
-version = "1.4.0"
+python-versions = ">=3.6"
[[package]]
-category = "main"
-description = "Type Hints for Python"
-name = "typing"
+name = "typeguard"
+version = "2.13.3"
+description = "Run-time type checker for Python"
+category = "dev"
optional = false
-python-versions = "*"
-version = "3.7.4.1"
+python-versions = ">=3.5.3"
+
+[package.extras]
+doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
+test = ["mypy", "pytest", "typing-extensions"]
[[package]]
-category = "main"
-description = "Backported and Experimental Type Hints for Python 3.5+"
name = "typing-extensions"
+version = "4.3.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "main"
optional = false
-python-versions = "*"
-version = "3.7.4"
-
-[package.dependencies]
-typing = ">=3.7.4"
+python-versions = ">=3.7"
[[package]]
+name = "url-normalize"
+version = "1.4.3"
+description = "URL normalization for Python"
category = "dev"
-description = "HTTP library with thread-safe connection pooling, file post, and more."
-name = "urllib3"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
-version = "1.25.6"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+
+[package.dependencies]
+six = "*"
[[package]]
+name = "urllib3"
+version = "1.26.9"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "dev"
-description = "Measures number of Terminal column cells of wide-character codes"
-name = "wcwidth"
optional = false
-python-versions = "*"
-version = "0.1.7"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
+secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
-category = "dev"
-description = "The strictest and most opinionated python linter ever"
name = "wemake-python-styleguide"
+version = "0.17.0"
+description = "The strictest and most opinionated python linter ever"
+category = "dev"
optional = false
-python-versions = ">=3.6,<4.0"
-version = "0.12.5"
+python-versions = ">=3.7,<4.0"
[package.dependencies]
astor = ">=0.8,<0.9"
attrs = "*"
-flake8 = ">=3.7,<4.0"
-flake8-annotations-complexity = ">=0.0.2,<0.0.3"
-flake8-bandit = ">=2.1,<3.0"
-flake8-broken-line = ">=0.1,<0.2"
-flake8-bugbear = ">=19.3,<20.0"
-flake8-builtins = ">=1.4,<2.0"
-flake8-coding = ">=1.3,<2.0"
+darglint = ">=1.2,<2.0"
+flake8 = ">=3.7,<5"
+flake8-bandit = ">=2.1,<4"
+flake8-broken-line = ">=0.5,<0.6"
+flake8-bugbear = ">=22.9,<23.0"
flake8-commas = ">=2.0,<3.0"
-flake8-comprehensions = ">=2.1,<3.0"
-flake8-debugger = ">=3.1,<4.0"
-flake8-docstrings = ">=1.3.1,<2.0.0"
-flake8-eradicate = ">=0.2,<0.3"
-flake8-executable = ">=2.0,<3.0"
-flake8-isort = ">=2.6,<3.0"
-flake8-logging-format = ">=0.6,<0.7"
-flake8-pep3101 = ">=1.2,<2.0"
-flake8-print = ">=3.1,<4.0"
-flake8-quotes = ">=2.0.1,<3.0.0"
-flake8-rst-docstrings = ">=0.0.11,<0.0.12"
-flake8-string-format = ">=0.2,<0.3"
-pep8-naming = ">=0.8.2,<0.9.0"
+flake8-comprehensions = ">=3.1,<4.0"
+flake8-debugger = ">=4.0,<5.0"
+flake8-docstrings = ">=1.3,<2.0"
+flake8-eradicate = ">=1.0,<2.0"
+flake8-isort = ">=4.0,<5.0"
+flake8-quotes = ">=3.0,<4.0"
+flake8-rst-docstrings = ">=0.2,<0.3"
+flake8-string-format = ">=0.3,<0.4"
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+pep8-naming = ">=0.13,<0.14"
pygments = ">=2.4,<3.0"
-radon = ">=2.4,<3.0"
-typing_extensions = ">=3.6,<4.0"
+typing_extensions = ">=4.0,<5.0"
[[package]]
+name = "win32-setctime"
+version = "1.1.0"
+description = "A small Python utility to set file creation time on Windows"
category = "dev"
-description = "Backport of pathlib-compatible object wrapper for zip files"
-marker = "python_version < \"3.8\""
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
+
+[[package]]
name = "zipp"
+version = "3.8.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "dev"
optional = false
-python-versions = ">=2.7"
-version = "0.6.0"
+python-versions = ">=3.7"
-[package.dependencies]
-more-itertools = "*"
+[package.extras]
+docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"]
+testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[metadata]
-content-hash = "1a04fa952acb94b3e4daeb86f77c5eeb62f3f8f5af0acd57d2b65756693ed723"
-python-versions = "^3.6"
-
-[metadata.hashes]
-alabaster = ["446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", "a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"]
-astor = ["0e41295809baf43ae8303350e031aff81ae52189b6f881f36d623fa8b2f1960e", "37a6eed8b371f1228db08234ed7f6cfdc7817a3ed3824797e20cbb11dc2a7862"]
-atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"]
-attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"]
-babel = ["af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab", "e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"]
-bandit = ["336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952", "41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"]
-capturer = ["090142a58f3f85def3a7dd55d9024d0d1a86d1a88aaf9317c0f146244994a615", "2c9d507516e5e86c442ff0c45b08cd0810d314e62bbea7e96ba0f473389d17dc"]
-certifi = ["e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", "fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"]
-chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"]
-click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"]
-colorama = ["463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", "48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"]
-coverage = ["08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", "0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", "141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", "19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", "23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", "245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", "331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", "386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", "3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", "60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", "63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", "6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", "6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", "7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", "826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", "93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", "9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", "af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", "bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", "bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", "c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", "dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", "df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", "e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", "e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", "e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", "eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", "eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", "ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", "efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", "fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", "ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"]
-decorator = ["86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", "f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"]
-dictdiffer = ["97cf4ef98ebc1acf737074aed41e379cf48ab5ff528c92109dfb8e2e619e6809", "b3ad476fc9cca60302b52c50e1839342d2092aeaba586d69cbf9249f87f52463"]
-doc8 = ["2df89f9c1a5abfb98ab55d0175fed633cae0cf45025b8b1e0ee5ea772be28543", "d12f08aa77a4a65eb28752f4bc78f41f611f9412c4155e2b03f1f5d4a45efe04"]
-docutils = ["6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", "9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", "a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"]
-dparse = ["00a5fdfa900629e5159bf3600d44905b333f4059a3366f28e0dbd13eeab17b19", "cef95156fa0adedaf042cd42f9990974bec76f25dfeca4dc01f381a243d5aa5b"]
-entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"]
-eradicate = ["4ffda82aae6fd49dfffa777a857cb758d77502a1f2e0f54c9ac5155a39d2d01a"]
-flake8 = ["19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"]
-flake8-annotations-complexity = ["e499d2186efcc5f6f2f1c7eb18568d08f4d9313fad15f614645f3f445f70b45c"]
-flake8-bandit = ["687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"]
-flake8-broken-line = ["30378a3749911e453d0a9e03204156cbbd35bcc03fb89f12e6a5206e5baf3537", "7721725dce3aeee1df371a252822f1fcecfaf2766dcf5bac54ee1b3f779ee9d1"]
-flake8-bugbear = ["d8c466ea79d5020cb20bf9f11cf349026e09517a42264f313d3f6fddb83e0571", "ded4d282778969b5ab5530ceba7aa1a9f1b86fa7618fc96a19a1d512331640f8"]
-flake8-builtins = ["8d806360767947c0035feada4ddef3ede32f0a586ef457e62d811b8456ad9a51", "cd7b1b7fec4905386a3643b59f9ca8e305768da14a49a7efb31fe9364f33cd04"]
-flake8-coding = ["79704112c44d09d4ab6c8965e76a20c3f7073d52146db60303bce777d9612260", "b8f4d5157a8f74670e6cfea732c3d9f4291a4e994c8701d2c55f787c6e6cb741"]
-flake8-commas = ["d3005899466f51380387df7151fb59afec666a0f4f4a2c6a8995b975de0f44b7", "ee2141a3495ef9789a3894ed8802d03eff1eaaf98ce6d8653a7c573ef101935e"]
-flake8-comprehensions = ["7b174ded3d7e73edf587e942458b6c1a7c3456d512d9c435deae367236b9562c", "e36fc12bd3833e0b34fe0639b7a817d32c86238987f532078c57eafdc7a8a219"]
-flake8-debugger = ["08d80b7e90f6c11d72e77d2717b90c29b144c87e5698ffd1b5a1e42acb3ecfd9", "6e662f7e75a3ed729d3be7c92e72bde385ab08ec26e7808bf3dfc63445c87857"]
-flake8-docstrings = ["3d5a31c7ec6b7367ea6506a87ec293b94a0a46c0bce2bb4975b7f1d09b6f3717", "a256ba91bc52307bef1de59e2a009c3cf61c3d0952dbe035d6ff7208940c2edc"]
-flake8-eradicate = ["a42c501d40b2beb6bcbbcb961169b16a7794b179dcc990cb317c2e655c19e379", "dd9baf6428319b946b85964c5ad0fb41d68c8998d40ac4ce73f37b9f41c535be"]
-flake8-executable = ["968618c475a23a538ced9b957a741b818d37610838f99f6abcea249e4de7c9ec", "a636ff78b14b63b1245d1c4d509db2f6ea0f2e27a86ee7eb848f3827bef7e16d"]
-flake8-isort = ["1e67b6b90a9b980ac3ff73782087752d406ce0a729ed928b92797f9fa188917e", "81a8495eefed3f2f63f26cd2d766c7b1191e923a15b9106e6233724056572c68"]
-flake8-logging-format = ["ca5f2b7fc31c3474a0aa77d227e022890f641a025f0ba664418797d979a779f8"]
-flake8-pep3101 = ["493821d6bdd083794eb0691ebe5b68e5c520b622b269d60e54308fb97440e21a", "b661ab718df42b87743dde266ef5de4f9e900b56c67dbccd45d24cf527545553"]
-flake8-plugin-utils = ["1ac5eb19773d5c7fdde60b0d901ae86be9c751bf697c61fdb6609b86872f3c6e", "24b4a3b216ad588951d3d7adef4645dcb3b32a33b878e03baa790b5a66bf3a73"]
-flake8-polyfill = ["12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9", "e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"]
-flake8-print = ["3456804209b0420d443cfd6cfe115dad2b13ec72c9ef1171857b26b7412cc932", "b46a78fd9ef80f85bf421923b6a4ca22f82285f461b8649e196aeca89ed4ff49"]
-flake8-pyi = ["3646134dcc5269d3fd017624961de83cc0a0404f620233885ad86faf64265a11", "a93a47ac78b6d363545885e9254d0eccac8a88c3806ab8af27e30dd8c058654f"]
-flake8-pytest = ["61686128a79e1513db575b2bcac351081d5a293811ddce2d5dfc25e8c762d33e", "b4d6703f7d7b646af1e2660809e795886dd349df11843613dbe6515efa82c0f3"]
-flake8-pytest-style = ["1c2303998c509cd65c3fb047cd536787ddf953e8113bc7f086c0cd7468db4b1f", "820503cb50b7f6aa13a9889f4c47ba35bbd666877a72ed138ae5682a9bccaf9d"]
-flake8-quotes = ["5dbaf668887873f28346fb87943d6da2e4b9f77ce9f2169cff21764a0a4934ed"]
-flake8-rst-docstrings = ["a2fa35c6ef978422234afae8c345f23ff721571d43f2895e29817e94be92dd6c"]
-flake8-string-format = ["68ea72a1a5b75e7018cae44d14f32473c798cf73d75cbaed86c6a9a907b770b2", "774d56103d9242ed968897455ef49b7d6de272000cfa83de5814273a868832f1"]
-gitdb2 = ["1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350", "96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b"]
-gitpython = ["3237caca1139d0a7aa072f6735f5fd2520de52195e0fa1d8b83a9b212a2498b2", "a7d6bef0775f66ba47f25911d285bcd692ce9053837ff48a120c2b8cf3a71389"]
-humanfriendly = ["23057b10ad6f782e7bc3a20e3cb6768ab919f619bbdc0dd75691121bbde5591d", "33ee8ceb63f1db61cce8b5c800c531e1a61023ac5488ccde2ba574a85be00a85"]
-idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"]
-imagesize = ["3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", "f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"]
-importlib-metadata = ["aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", "d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"]
-isort = ["54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"]
-jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"]
-jmespath = ["3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6", "bde2aef6f44302dfb30320115b17d030798de8c4110e28d5cf6cf91a7a31074c"]
-m2r = ["bf90bad66cda1164b17e5ba4a037806d2443f2a4d5ddc9f6a5554a0322aaed99"]
-mando = ["4ce09faec7e5192ffc3c57830e26acba0fd6cd11e1ee81af0d4df0657463bd1c", "79feb19dc0f097daa64a1243db578e7674909b75f88ac2220f1c065c10a0d960"]
-markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"]
-marshmallow = ["077b4612f5d3b9333b736fdc6b963d2b46d409070f44ff3e6c4109645c673e83", "9a2f3e8ea5f530a9664e882d7d04b58650f46190178b2264c72b7d20399d28f0"]
-marshmallow-polyfield = ["963a01e80bca5cb4da42b8d2f7e6e90946257ae22d22ff2ed104a8a863eeb0c6"]
-mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"]
-mistune = ["59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e", "88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"]
-more-itertools = ["409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"]
-mypy = ["1521c186a3d200c399bd5573c828ea2db1362af7209b2adb1bb8532cea2fb36f", "31a046ab040a84a0fc38bc93694876398e62bc9f35eca8ccbf6418b7297f4c00", "3b1a411909c84b2ae9b8283b58b48541654b918e8513c20a400bb946aa9111ae", "48c8bc99380575deb39f5d3400ebb6a8a1cb5cc669bbba4d3bb30f904e0a0e7d", "540c9caa57a22d0d5d3c69047cc9dd0094d49782603eb03069821b41f9e970e9", "672e418425d957e276c291930a3921b4a6413204f53fe7c37cad7bc57b9a3391", "6ed3b9b3fdc7193ea7aca6f3c20549b377a56f28769783a8f27191903a54170f", "9371290aa2cad5ad133e4cdc43892778efd13293406f7340b9ffe99d5ec7c1d9", "ace6ac1d0f87d4072f05b5468a084a45b4eda970e4d26704f201e06d47ab2990", "b428f883d2b3fe1d052c630642cc6afddd07d5cd7873da948644508be3b9d4a7", "d5bf0e6ec8ba346a2cf35cb55bf4adfddbc6b6576fcc9e10863daa523e418dbb", "d7574e283f83c08501607586b3167728c58e8442947e027d2d4c7dcd6d82f453", "dc889c84241a857c263a2b1cd1121507db7d5b5f5e87e77147097230f374d10b", "f4748697b349f373002656bf32fede706a0e713d67bfdcf04edf39b1f61d46eb"]
-mypy-extensions = ["090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", "2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"]
-nitpick = ["fb2f5f6e5c1ae8f351b0f9c01bafe68c35d87480e9bac7064cf7d15491d70db9", "ff451b41fbfe50ecda0522756a5628d6861c8c4f25c183d65bcd4efede0c8c95"]
-packaging = ["28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", "d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"]
-pbr = ["2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8", "b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9"]
-pep8-naming = ["01cb1dab2f3ce9045133d08449f1b6b93531dceacb9ef04f67087c11c723cea9", "0ec891e59eea766efd3059c3d81f1da304d858220678bdc351aab73c533f2fbb"]
-pluggy = ["0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", "fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"]
-py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"]
-pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"]
-pydocstyle = ["04c84e034ebb56eb6396c820442b8c4499ac5eb94a3bda88951ac3dc519b6058", "66aff87ffe34b1e49bff2dd03a88ce6843be2f3346b0c9814410d34987fbab59"]
-pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"]
-pygments = ["71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", "881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"]
-pyparsing = ["6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", "d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"]
-pyreadline = ["4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1", "65540c21bfe14405a3a77e4c085ecfce88724743a4ead47c66b84defcf82c32e", "9ce5fa65b8992dfa373bddc5b6e0864ead8f291c94fbfec05fbd5c836162e67b"]
-pytest = ["27abc3fef618a01bebb1f0d6d303d2816a99aa87a5968ebc32fe971be91eb1e6", "58cee9e09242937e136dbb3dab466116ba20d6b7828c7620f23947f37eb4dae4"]
-pytest-cov = ["cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", "cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"]
-pytest-mypy-plugins = ["2aa73a7081dbb03dadfa3d2e7bc8d06544931799ef0d26d77c76519bcbaabe4b", "fc754203133ab528e66b0feb3466a4a2f6a5ac274e0d9c571d42c1e9c5d24aa4"]
-pytest-randomly = ["5facc2b5ac56e36b9c4bf14f49cc7b4c95427835bbf4a3c739b71a5f5f82d58a", "9256c9ff88466f7bf9794d2eeeea8fbf1cd1bc8df1b3575df59e89b8813bffaa"]
-python-slugify = ["a8fc3433821140e8f409a9831d13ae5deccd0b033d4744d94b31fea141bdd84c"]
-pytz = ["1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", "b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"]
-pyyaml = ["0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", "f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"]
-radon = ["38e495a4aa4c1d7293d3c1733393961fb52209c9bc2d75163c3ba8124d8bbbaa", "f893f2faa632a060f6d0f01843d10a0395515bde865c759c0dd3f15239caf11b"]
-requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"]
-restructuredtext-lint = ["97b3da356d5b3a8514d8f1f9098febd8b41463bed6a1d9f126cf0a048b6fd908"]
-"ruamel.yaml" = ["0db639b1b2742dae666c6fc009b8d1931ef15c9276ef31c0673cc6dcf766cf40", "412a6f5cfdc0525dee6a27c08f5415c7fd832a7afcb7a0ed7319628aed23d408"]
-"ruamel.yaml.clib" = ["1e77424825caba5553bbade750cec2277ef130647d685c2b38f68bc03453bac6", "392b7c371312abf27fb549ec2d5e0092f7ef6e6c9f767bfb13e83cb903aca0fd", "4d55386129291b96483edcb93b381470f7cd69f97585829b048a3d758d31210a", "550168c02d8de52ee58c3d8a8193d5a8a9491a5e7b2462d27ac5bf63717574c9", "57933a6986a3036257ad7bf283529e7c19c2810ff24c86f4a0cfeb49d2099919", "615b0396a7fad02d1f9a0dcf9f01202bf9caefee6265198f252c865f4227fcc6", "77556a7aa190be9a2bd83b7ee075d3df5f3c5016d395613671487e79b082d784", "7aee724e1ff424757b5bd8f6c5bbdb033a570b2b4683b17ace4dbe61a99a657b", "8073c8b92b06b572e4057b583c3d01674ceaf32167801fe545a087d7a1e8bf52", "9c6d040d0396c28d3eaaa6cb20152cb3b2f15adf35a0304f4f40a3cf9f1d2448", "a0ff786d2a7dbe55f9544b3f6ebbcc495d7e730df92a08434604f6f470b899c5", "b1b7fcee6aedcdc7e62c3a73f238b3d080c7ba6650cd808bce8d7761ec484070", "b66832ea8077d9b3f6e311c4a53d06273db5dc2db6e8a908550f3c14d67e718c", "d0d3ac228c9bbab08134b4004d748cf9f8743504875b3603b3afbb97e3472947", "d10e9dd744cf85c219bf747c75194b624cc7a94f0c80ead624b06bfa9f61d3bc", "ea4362548ee0cbc266949d8a441238d9ad3600ca9910c3fe4e82ee3a50706973", "ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad", "f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e"]
-safety = ["0a3a8a178a9c96242b224f033ee8d1d130c0448b0e6622d12deaf37f6c3b4e59", "5059f3ffab3648330548ea9c7403405bbfaf085b11235770825d14c58f24cb78"]
-six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"]
-smmap2 = ["0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde", "29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a"]
-snowballstemmer = ["209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", "df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"]
-sortedcontainers = ["974e9a32f56b17c1bac2aebd9dcf197f3eb9cd30553c5852a3187ad162e1a03a", "d9e96492dd51fae31e60837736b38fe42a187b5404c16606ff7ee7cd582d4c60"]
-sphinx = ["0d586b0f8c2fc3cc6559c5e8fd6124628110514fda0e5d7c82e682d749d2e845", "839a3ed6f6b092bb60f492024489cc9e6991360fb9f52ed6361acd510d261069"]
-sphinx-autodoc-typehints = ["0d968ec3ee4f7fe7695ab6facf5cd2d74d3cea67584277458ad9b2788ebbcc3b", "8edca714fd3de8e43467d7e51dd3812fe999f8874408a639f7c38a9e1a5a4eb3"]
-sphinx-typlog-theme = ["a2a2a3a5f3d1f89b165835762469e386e977251c98b4d76b073d9b04f939571a", "effba0175ca2cadebbb47671ce6471e4095ad563239431e9b6326feed2dec3ee"]
-sphinxcontrib-applehelp = ["edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", "fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"]
-sphinxcontrib-devhelp = ["6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", "9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"]
-sphinxcontrib-htmlhelp = ["4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422", "d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"]
-sphinxcontrib-jsmath = ["2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"]
-sphinxcontrib-mermaid = ["d2e33529c63c12724193b210dcbd4285ca6cff17b8f91f9dbcb8b4b7d07595e7"]
-sphinxcontrib-qthelp = ["513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", "79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"]
-sphinxcontrib-serializinghtml = ["c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", "db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"]
-stevedore = ["01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730", "e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14"]
-testfixtures = ["665a298976c8d77f311b65c46f16b7cda7229a47dff5ad7c822e5b3371a439e2", "9d230c5c80746f9f86a16a1f751a5cf5d8e317d4cc48243a19fb180d22303bce"]
-text-unidecode = ["1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", "bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"]
-toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
-tomlkit = ["32c10cc16ded7e4101c79f269910658cc2a0be5913f1252121c3cd603051c269", "96e6369288571799a3052c1ef93b9de440e1ab751aa045f435b55e9d3bcd0690"]
-typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"]
-typing = ["91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23", "c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36", "f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714"]
-typing-extensions = ["2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95", "b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87", "d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed"]
-urllib3 = ["3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", "9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"]
-wcwidth = ["3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"]
-wemake-python-styleguide = ["6b915c03640aab209970e10ed2b4d66de5978ad533b30b69d7b5e2956de15eaa", "6dd62183cb227b5e5ad96d17b12b08fcebd22802cde6e5fb7e21bd73c3f69a20"]
-zipp = ["3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"]
+lock-version = "1.1"
+python-versions = "^3.7"
+content-hash = "f196640c19ac60c3183bfd7022c3f845ce482e5102051855458ee31bcfc8faef"
+
+[metadata.files]
+alabaster = [
+ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
+ {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
+]
+appdirs = [
+ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
+ {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
+]
+astor = [
+ {file = "astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5"},
+ {file = "astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e"},
+]
+attrs = [
+ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
+ {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
+]
+autorepr = [
+ {file = "autorepr-0.3.0-py2-none-any.whl", hash = "sha256:c34567e4073630feb52d9c788fc198085e9e9de4817e3b93b7c4c534fc689f11"},
+ {file = "autorepr-0.3.0-py2.py3-none-any.whl", hash = "sha256:1d9010d14fb325d3961e3aa73692685563f97d6ba4a2f0f735329fb37422599c"},
+ {file = "autorepr-0.3.0.tar.gz", hash = "sha256:ef770b84793d5433e6bb893054973b8c7ce6b487274f9c3f734f678cae11e85e"},
+]
+babel = [
+ {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"},
+ {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"},
+]
+bandit = [
+ {file = "bandit-1.7.4-py3-none-any.whl", hash = "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a"},
+ {file = "bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2"},
+]
+beartype = [
+ {file = "beartype-0.10.4-py3-none-any.whl", hash = "sha256:1a65453bc25b39979bf5ad65fe5e73350551282956456d828fb5783468649e3e"},
+ {file = "beartype-0.10.4.tar.gz", hash = "sha256:24ec69f6a7f4e6e97af403d08de270def3248518060327095d23b1c4df64bf2a"},
+]
+cattrs = [
+ {file = "cattrs-1.10.0-py3-none-any.whl", hash = "sha256:35dd9063244263e63bd0bd24ea61e3015b00272cead084b2c40d788b0f857c46"},
+ {file = "cattrs-1.10.0.tar.gz", hash = "sha256:211800f725cdecedcbcf4c753bbd22d248312b37d130f06045434acb7d9b34e1"},
+]
+certifi = [
+ {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
+ {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
+]
+charset-normalizer = [
+ {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
+ {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
+]
+chevron = [
+ {file = "chevron-0.14.0-py3-none-any.whl", hash = "sha256:fbf996a709f8da2e745ef763f482ce2d311aa817d287593a5b990d6d6e4f0443"},
+ {file = "chevron-0.14.0.tar.gz", hash = "sha256:87613aafdf6d77b6a90ff073165a61ae5086e21ad49057aa0e53681601800ebf"},
+]
+click = [
+ {file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"},
+ {file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"},
+]
+codespell = [
+ {file = "codespell-2.2.1-py3-none-any.whl", hash = "sha256:0c53a70f466952706407383d87142a78f319a0e18602802c4aadd3d93158bfc6"},
+ {file = "codespell-2.2.1.tar.gz", hash = "sha256:569b67e5e5c3ade02a1e23f6bbc56c64b608a3ab48ddd943ece0a03e6c346ed1"},
+]
+colorama = [
+ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
+ {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
+]
+configupdater = [
+ {file = "ConfigUpdater-3.1-py2.py3-none-any.whl", hash = "sha256:28e266bdb31369b682c15a7af1940b3f8de5ceac34b4715a121914d8ff1722b5"},
+ {file = "ConfigUpdater-3.1.tar.gz", hash = "sha256:ddcc5250f508b9131c45fd1dbceae3f112907ddd7597da1cff30c51bf7c87edb"},
+]
+coverage = [
+ {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"},
+ {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"},
+ {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"},
+ {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"},
+ {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"},
+ {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"},
+ {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"},
+ {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"},
+ {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"},
+ {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"},
+ {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"},
+ {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"},
+ {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"},
+ {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"},
+ {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"},
+ {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"},
+ {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"},
+ {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"},
+ {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"},
+ {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"},
+ {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"},
+ {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"},
+ {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"},
+ {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"},
+ {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"},
+ {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"},
+ {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"},
+ {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"},
+ {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"},
+ {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"},
+ {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"},
+ {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"},
+ {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"},
+ {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"},
+ {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"},
+ {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"},
+ {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"},
+ {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"},
+ {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"},
+ {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"},
+ {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"},
+ {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"},
+ {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"},
+ {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"},
+ {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"},
+ {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"},
+ {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"},
+ {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"},
+ {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"},
+ {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"},
+]
+darglint = [
+ {file = "darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d"},
+ {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"},
+]
+decorator = [
+ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
+ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
+]
+dictdiffer = [
+ {file = "dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595"},
+ {file = "dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578"},
+]
+doc8 = [
+ {file = "doc8-1.0.0-py3-none-any.whl", hash = "sha256:0c6c3104fa7f7bb2103589c0a8e272c105fdff3ddd1ef4808e51b2782185e9ab"},
+ {file = "doc8-1.0.0.tar.gz", hash = "sha256:1e999a14fe415ea96d89d5053c790d01061f19b6737706b817d1579c2a07cc16"},
+]
+docutils = [
+ {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"},
+ {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"},
+]
+dparse = [
+ {file = "dparse-0.6.2-py3-none-any.whl", hash = "sha256:8097076f1dd26c377f30d4745e6ec18fef42f3bf493933b842ac5bafad8c345f"},
+ {file = "dparse-0.6.2.tar.gz", hash = "sha256:d45255bda21f998bc7ddf2afd5e62505ba6134756ba2d42a84c56b0826614dfe"},
+]
+dpath = [
+ {file = "dpath-2.0.6-py3-none-any.whl", hash = "sha256:8c439bb1c3b3222427e9b8812701cd99a0ef3415ddbb7c03a2379f6989a03965"},
+ {file = "dpath-2.0.6.tar.gz", hash = "sha256:5a1ddae52233fbc8ef81b15fb85073a81126bb43698d3f3a1b6aaf561a46cdc0"},
+]
+eradicate = [
+ {file = "eradicate-2.1.0-py3-none-any.whl", hash = "sha256:8bfaca181db9227dc88bdbce4d051a9627604c2243e7d85324f6d6ce0fd08bb2"},
+ {file = "eradicate-2.1.0.tar.gz", hash = "sha256:aac7384ab25b1bf21c4c012de9b4bf8398945a14c98c911545b2ea50ab558014"},
+]
+exceptiongroup = [
+ {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"},
+ {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"},
+]
+flake8 = [
+ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
+ {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
+]
+flake8-bandit = [
+ {file = "flake8_bandit-3.0.0-py2.py3-none-any.whl", hash = "sha256:61b617f4f7cdaa0e2b1e6bf7b68afb2b619a227bb3e3ae00dd36c213bd17900a"},
+ {file = "flake8_bandit-3.0.0.tar.gz", hash = "sha256:54d19427e6a8d50322a7b02e1841c0a7c22d856975f3459803320e0e18e2d6a1"},
+]
+flake8-broken-line = [
+ {file = "flake8-broken-line-0.5.0.tar.gz", hash = "sha256:7c98de9dd1385b71e888709c7f2aee3f0514107ecb5875bc95d0c03392191c97"},
+ {file = "flake8_broken_line-0.5.0-py3-none-any.whl", hash = "sha256:daafb19b67eead0410ce7ba155d51a15b9d020ebe7630d87de9c2b93cedb6703"},
+]
+flake8-bugbear = [
+ {file = "flake8-bugbear-22.9.23.tar.gz", hash = "sha256:17b9623325e6e0dcdcc80ed9e4aa811287fcc81d7e03313b8736ea5733759937"},
+ {file = "flake8_bugbear-22.9.23-py3-none-any.whl", hash = "sha256:cd2779b2b7ada212d7a322814a1e5651f1868ab0d3f24cc9da66169ab8fda474"},
+]
+flake8-commas = [
+ {file = "flake8-commas-2.1.0.tar.gz", hash = "sha256:940441ab8ee544df564ae3b3f49f20462d75d5c7cac2463e0b27436e2050f263"},
+ {file = "flake8_commas-2.1.0-py2.py3-none-any.whl", hash = "sha256:ebb96c31e01d0ef1d0685a21f3f0e2f8153a0381430e748bf0bbbb5d5b453d54"},
+]
+flake8-comprehensions = [
+ {file = "flake8-comprehensions-3.8.0.tar.gz", hash = "sha256:8e108707637b1d13734f38e03435984f6b7854fa6b5a4e34f93e69534be8e521"},
+ {file = "flake8_comprehensions-3.8.0-py3-none-any.whl", hash = "sha256:9406314803abe1193c064544ab14fdc43c58424c0882f6ff8a581eb73fc9bb58"},
+]
+flake8-debugger = [
+ {file = "flake8-debugger-4.0.0.tar.gz", hash = "sha256:e43dc777f7db1481db473210101ec2df2bd39a45b149d7218a618e954177eda6"},
+ {file = "flake8_debugger-4.0.0-py3-none-any.whl", hash = "sha256:82e64faa72e18d1bdd0000407502ebb8ecffa7bc027c62b9d4110ce27c091032"},
+]
+flake8-docstrings = [
+ {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"},
+ {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"},
+]
+flake8-eradicate = [
+ {file = "flake8-eradicate-1.2.0.tar.gz", hash = "sha256:acaa1b6839ff00d284b805c432fdfa6047262bd15a5504ec945797e87b4de1fa"},
+ {file = "flake8_eradicate-1.2.0-py3-none-any.whl", hash = "sha256:51dc660d0c1c1ed93af0f813540bbbf72ab2d3466c14e3f3bac371c618b6042f"},
+]
+flake8-isort = [
+ {file = "flake8-isort-4.1.1.tar.gz", hash = "sha256:d814304ab70e6e58859bc5c3e221e2e6e71c958e7005239202fee19c24f82717"},
+ {file = "flake8_isort-4.1.1-py3-none-any.whl", hash = "sha256:c4e8b6dcb7be9b71a02e6e5d4196cefcef0f3447be51e82730fb336fff164949"},
+]
+flake8-plugin-utils = [
+ {file = "flake8-plugin-utils-1.3.2.tar.gz", hash = "sha256:20fa2a8ca2decac50116edb42e6af0a1253ef639ad79941249b840531889c65a"},
+ {file = "flake8_plugin_utils-1.3.2-py3-none-any.whl", hash = "sha256:1fe43e3e9acf3a7c0f6b88f5338cad37044d2f156c43cb6b080b5f9da8a76f06"},
+]
+flake8-polyfill = [
+ {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"},
+ {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"},
+]
+flake8-pytest-style = [
+ {file = "flake8-pytest-style-1.6.0.tar.gz", hash = "sha256:c1175713e9e11b78cd1a035ed0bca0d1e41d09c4af329a952750b61d4194ddac"},
+ {file = "flake8_pytest_style-1.6.0-py3-none-any.whl", hash = "sha256:5fedb371a950e9fe0e0e6bfc854be7d99151271208f34cd2cc517681ece27780"},
+]
+flake8-quotes = [
+ {file = "flake8-quotes-3.3.1.tar.gz", hash = "sha256:633adca6fb8a08131536af0d750b44d6985b9aba46f498871e21588c3e6f525a"},
+]
+flake8-rst-docstrings = [
+ {file = "flake8-rst-docstrings-0.2.5.tar.gz", hash = "sha256:4fe93f997dea45d9d3c8bd220f12f0b6c359948fb943b5b48021a3f927edd816"},
+ {file = "flake8_rst_docstrings-0.2.5-py3-none-any.whl", hash = "sha256:b99d9041b769b857efe45a448dc8c71b1bb311f9cacbdac5de82f96498105082"},
+]
+flake8-string-format = [
+ {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"},
+ {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"},
+]
+flatten-dict = [
+ {file = "flatten-dict-0.4.2.tar.gz", hash = "sha256:506a96b6e6f805b81ae46a0f9f31290beb5fa79ded9d80dbe1b7fa236ab43076"},
+ {file = "flatten_dict-0.4.2-py2.py3-none-any.whl", hash = "sha256:7e245b20c4c718981212210eec4284a330c9f713e632e98765560e05421e48ad"},
+]
+furl = [
+ {file = "furl-2.1.3-py2.py3-none-any.whl", hash = "sha256:9ab425062c4217f9802508e45feb4a83e54324273ac4b202f1850363309666c0"},
+ {file = "furl-2.1.3.tar.gz", hash = "sha256:5a6188fe2666c484a12159c18be97a1977a71d632ef5bb867ef15f54af39cc4e"},
+]
+gitdb = [
+ {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
+ {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
+]
+gitpython = [
+ {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"},
+ {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"},
+]
+identify = [
+ {file = "identify-2.4.12-py2.py3-none-any.whl", hash = "sha256:5f06b14366bd1facb88b00540a1de05b69b310cbc2654db3c7e07fa3a4339323"},
+ {file = "identify-2.4.12.tar.gz", hash = "sha256:3f3244a559290e7d3deb9e9adc7b33594c1bc85a9dd82e0f1be519bf12a1ec17"},
+]
+idna = [
+ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
+ {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
+]
+imagesize = [
+ {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"},
+ {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"},
+]
+importlib-metadata = [
+ {file = "importlib_metadata-4.11.3-py3-none-any.whl", hash = "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6"},
+ {file = "importlib_metadata-4.11.3.tar.gz", hash = "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539"},
+]
+importlib-resources = [
+ {file = "importlib_resources-5.6.0-py3-none-any.whl", hash = "sha256:a9dd72f6cc106aeb50f6e66b86b69b454766dd6e39b69ac68450253058706bcc"},
+ {file = "importlib_resources-5.6.0.tar.gz", hash = "sha256:1b93238cbf23b4cde34240dd8321d99e9bf2eb4bc91c0c99b2886283e7baad85"},
+]
+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.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
+ {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
+]
+jinja2 = [
+ {file = "Jinja2-3.1.1-py3-none-any.whl", hash = "sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119"},
+ {file = "Jinja2-3.1.1.tar.gz", hash = "sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9"},
+]
+jmespath = [
+ {file = "jmespath-1.0.0-py3-none-any.whl", hash = "sha256:e8dcd576ed616f14ec02eed0005c85973b5890083313860136657e24784e4c04"},
+ {file = "jmespath-1.0.0.tar.gz", hash = "sha256:a490e280edd1f57d6de88636992d05b71e97d69a26a19f058ecf7d304474bf5e"},
+]
+loguru = [
+ {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"},
+ {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"},
+]
+m2r2 = [
+ {file = "m2r2-0.3.3-py3-none-any.whl", hash = "sha256:2ee32a5928c3598b67c70e6d22981ec936c03d5bfd2f64229e77678731952f16"},
+ {file = "m2r2-0.3.3.tar.gz", hash = "sha256:f9b6e9efbc2b6987dbd43d2fd15a6d115ba837d8158ae73295542635b4086e75"},
+]
+markupsafe = [
+ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
+ {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
+ {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
+ {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
+ {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
+ {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
+]
+marshmallow = [
+ {file = "marshmallow-3.15.0-py3-none-any.whl", hash = "sha256:ff79885ed43b579782f48c251d262e062bce49c65c52412458769a4fb57ac30f"},
+ {file = "marshmallow-3.15.0.tar.gz", hash = "sha256:2aaaab4f01ef4f5a011a21319af9fce17ab13bf28a026d1252adab0e035648d5"},
+]
+marshmallow-polyfield = [
+ {file = "marshmallow-polyfield-5.10.tar.gz", hash = "sha256:75d0e31b725650e91428f975a66ed30f703cc6f9fcfe45b8436ee6d676921691"},
+ {file = "marshmallow_polyfield-5.10-py3-none-any.whl", hash = "sha256:a0a91730c6d21bfac1563990c7ba1413928e7499af669619d4fb38d1fb25c4e9"},
+]
+mccabe = [
+ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
+ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
+]
+mistune = [
+ {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"},
+ {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"},
+]
+more-itertools = [
+ {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"},
+ {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"},
+]
+mypy = [
+ {file = "mypy-0.942-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5bf44840fb43ac4074636fd47ee476d73f0039f4f54e86d7265077dc199be24d"},
+ {file = "mypy-0.942-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dcd955f36e0180258a96f880348fbca54ce092b40fbb4b37372ae3b25a0b0a46"},
+ {file = "mypy-0.942-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6776e5fa22381cc761df53e7496a805801c1a751b27b99a9ff2f0ca848c7eca0"},
+ {file = "mypy-0.942-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:edf7237137a1a9330046dbb14796963d734dd740a98d5e144a3eb1d267f5f9ee"},
+ {file = "mypy-0.942-cp310-cp310-win_amd64.whl", hash = "sha256:64235137edc16bee6f095aba73be5334677d6f6bdb7fa03cfab90164fa294a17"},
+ {file = "mypy-0.942-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b840cfe89c4ab6386c40300689cd8645fc8d2d5f20101c7f8bd23d15fca14904"},
+ {file = "mypy-0.942-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2b184db8c618c43c3a31b32ff00cd28195d39e9c24e7c3b401f3db7f6e5767f5"},
+ {file = "mypy-0.942-cp36-cp36m-win_amd64.whl", hash = "sha256:1a0459c333f00e6a11cbf6b468b870c2b99a906cb72d6eadf3d1d95d38c9352c"},
+ {file = "mypy-0.942-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4c3e497588afccfa4334a9986b56f703e75793133c4be3a02d06a3df16b67a58"},
+ {file = "mypy-0.942-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f6ad963172152e112b87cc7ec103ba0f2db2f1cd8997237827c052a3903eaa6"},
+ {file = "mypy-0.942-cp37-cp37m-win_amd64.whl", hash = "sha256:0e2dd88410937423fba18e57147dd07cd8381291b93d5b1984626f173a26543e"},
+ {file = "mypy-0.942-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:246e1aa127d5b78488a4a0594bd95f6d6fb9d63cf08a66dafbff8595d8891f67"},
+ {file = "mypy-0.942-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d8d3ba77e56b84cd47a8ee45b62c84b6d80d32383928fe2548c9a124ea0a725c"},
+ {file = "mypy-0.942-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2bc249409a7168d37c658e062e1ab5173300984a2dada2589638568ddc1db02b"},
+ {file = "mypy-0.942-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9521c1265ccaaa1791d2c13582f06facf815f426cd8b07c3a485f486a8ffc1f3"},
+ {file = "mypy-0.942-cp38-cp38-win_amd64.whl", hash = "sha256:e865fec858d75b78b4d63266c9aff770ecb6a39dfb6d6b56c47f7f8aba6baba8"},
+ {file = "mypy-0.942-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6ce34a118d1a898f47def970a2042b8af6bdcc01546454726c7dd2171aa6dfca"},
+ {file = "mypy-0.942-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:10daab80bc40f84e3f087d896cdb53dc811a9f04eae4b3f95779c26edee89d16"},
+ {file = "mypy-0.942-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3841b5433ff936bff2f4dc8d54cf2cdbfea5d8e88cedfac45c161368e5770ba6"},
+ {file = "mypy-0.942-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f7106cbf9cc2f403693bf50ed7c9fa5bb3dfa9007b240db3c910929abe2a322"},
+ {file = "mypy-0.942-cp39-cp39-win_amd64.whl", hash = "sha256:7742d2c4e46bb5017b51c810283a6a389296cda03df805a4f7869a6f41246534"},
+ {file = "mypy-0.942-py3-none-any.whl", hash = "sha256:a1b383fe99678d7402754fe90448d4037f9512ce70c21f8aee3b8bf48ffc51db"},
+ {file = "mypy-0.942.tar.gz", hash = "sha256:17e44649fec92e9f82102b48a3bf7b4a5510ad0cd22fa21a104826b5db4903e2"},
+]
+mypy-extensions = [
+ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
+ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
+]
+nitpick = [
+ {file = "nitpick-0.32.0-py3-none-any.whl", hash = "sha256:a2befd6371f8726846d4c127b80b4bc2cc510f97ee8d1a698d879e9d20a95a89"},
+ {file = "nitpick-0.32.0.tar.gz", hash = "sha256:9df66107d9dfc5f8631ee5af1c3a1050ef9db0549e04873bef91bf6681a55b61"},
+]
+numerary = [
+ {file = "numerary-0.3.0-py3-none-any.whl", hash = "sha256:0516174ff377bc7114ad383e1355a178410b4f8422ae55dc1d95c5ef23b6a03b"},
+ {file = "numerary-0.4.2-py3-none-any.whl", hash = "sha256:97f21459fa722fb2c27410f85c377143adc43cd94123c9c77c82d04e73ffe935"},
+]
+orderedmultidict = [
+ {file = "orderedmultidict-1.0.1-py2.py3-none-any.whl", hash = "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"},
+ {file = "orderedmultidict-1.0.1.tar.gz", hash = "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad"},
+]
+packaging = [
+ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
+ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
+]
+pbr = [
+ {file = "pbr-5.8.1-py2.py3-none-any.whl", hash = "sha256:27108648368782d07bbf1cb468ad2e2eeef29086affd14087a6d04b7de8af4ec"},
+ {file = "pbr-5.8.1.tar.gz", hash = "sha256:66bc5a34912f408bb3925bf21231cb6f59206267b7f63f3503ef865c1a292e25"},
+]
+pep8-naming = [
+ {file = "pep8-naming-0.13.2.tar.gz", hash = "sha256:93eef62f525fd12a6f8c98f4dcc17fa70baae2f37fa1f73bec00e3e44392fa48"},
+ {file = "pep8_naming-0.13.2-py3-none-any.whl", hash = "sha256:59e29e55c478db69cffbe14ab24b5bd2cd615c0413edf790d47d3fb7ba9a4e23"},
+]
+phantom-types = [
+ {file = "phantom-types-1.0.0.tar.gz", hash = "sha256:c08424050a63584d11f018291092a5aee5b6c4f43952b27eea6b82fc15b81f82"},
+ {file = "phantom_types-1.0.0-py3-none-any.whl", hash = "sha256:080d7e5d9e01d799dd8b160ffad4d36549788b872e10ca3902c3cc518cab5c15"},
+]
+pluggy = [
+ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+pycodestyle = [
+ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
+ {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
+]
+pydocstyle = [
+ {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"},
+ {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"},
+]
+pyflakes = [
+ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
+ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
+]
+Pygments = [
+ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"},
+ {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"},
+]
+pyparsing = [
+ {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"},
+ {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"},
+]
+pytest = [
+ {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
+ {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
+]
+pytest-cov = [
+ {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
+ {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
+]
+pytest-mypy-plugins = [
+ {file = "pytest-mypy-plugins-1.9.3.tar.gz", hash = "sha256:c2c6590ee68fd634013fdd0b47a5c83df300afd8b38d5aabd3b29e21b7907221"},
+ {file = "pytest_mypy_plugins-1.9.3-py3-none-any.whl", hash = "sha256:10e60bd59a2b043ebda70f9a0ad1a8c9324ce6d0b46b93cae3b1172df2d4d81b"},
+]
+pytest-randomly = [
+ {file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"},
+ {file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"},
+]
+python-slugify = [
+ {file = "python-slugify-6.1.1.tar.gz", hash = "sha256:00003397f4e31414e922ce567b3a4da28cf1436a53d332c9aeeb51c7d8c469fd"},
+ {file = "python_slugify-6.1.1-py2.py3-none-any.whl", hash = "sha256:8c0016b2d74503eb64761821612d58fcfc729493634b1eb0575d8f5b4aa1fbcf"},
+]
+pytz = [
+ {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
+ {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
+]
+pyyaml = [
+ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
+ {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
+ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
+ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
+ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
+ {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
+ {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
+ {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
+ {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
+ {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
+ {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
+ {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
+ {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
+ {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
+ {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
+ {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
+ {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
+ {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
+ {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
+ {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
+ {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
+ {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
+ {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
+ {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
+ {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
+ {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
+ {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
+ {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
+ {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
+ {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
+ {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
+ {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
+ {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
+ {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
+ {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
+ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
+ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
+]
+regex = [
+ {file = "regex-2022.3.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:42eb13b93765c6698a5ab3bcd318d8c39bb42e5fa8a7fcf7d8d98923f3babdb1"},
+ {file = "regex-2022.3.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9beb03ff6fe509d6455971c2489dceb31687b38781206bcec8e68bdfcf5f1db2"},
+ {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0a5a1fdc9f148a8827d55b05425801acebeeefc9e86065c7ac8b8cc740a91ff"},
+ {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb374a2a4dba7c4be0b19dc7b1adc50e6c2c26c3369ac629f50f3c198f3743a4"},
+ {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c33ce0c665dd325200209340a88438ba7a470bd5f09f7424e520e1a3ff835b52"},
+ {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04c09b9651fa814eeeb38e029dc1ae83149203e4eeb94e52bb868fadf64852bc"},
+ {file = "regex-2022.3.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab5d89cfaf71807da93c131bb7a19c3e19eaefd613d14f3bce4e97de830b15df"},
+ {file = "regex-2022.3.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e2630ae470d6a9f8e4967388c1eda4762706f5750ecf387785e0df63a4cc5af"},
+ {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:df037c01d68d1958dad3463e2881d3638a0d6693483f58ad41001aa53a83fcea"},
+ {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:940570c1a305bac10e8b2bc934b85a7709c649317dd16520471e85660275083a"},
+ {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7f63877c87552992894ea1444378b9c3a1d80819880ae226bb30b04789c0828c"},
+ {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3e265b388cc80c7c9c01bb4f26c9e536c40b2c05b7231fbb347381a2e1c8bf43"},
+ {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:058054c7a54428d5c3e3739ac1e363dc9347d15e64833817797dc4f01fb94bb8"},
+ {file = "regex-2022.3.15-cp310-cp310-win32.whl", hash = "sha256:76435a92e444e5b8f346aed76801db1c1e5176c4c7e17daba074fbb46cb8d783"},
+ {file = "regex-2022.3.15-cp310-cp310-win_amd64.whl", hash = "sha256:174d964bc683b1e8b0970e1325f75e6242786a92a22cedb2a6ec3e4ae25358bd"},
+ {file = "regex-2022.3.15-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6e1d8ed9e61f37881c8db383a124829a6e8114a69bd3377a25aecaeb9b3538f8"},
+ {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b52771f05cff7517f7067fef19ffe545b1f05959e440d42247a17cd9bddae11b"},
+ {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:673f5a393d603c34477dbad70db30025ccd23996a2d0916e942aac91cc42b31a"},
+ {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8923e1c5231549fee78ff9b2914fad25f2e3517572bb34bfaa3aea682a758683"},
+ {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764e66a0e382829f6ad3bbce0987153080a511c19eb3d2f8ead3f766d14433ac"},
+ {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd00859291658fe1fda48a99559fb34da891c50385b0bfb35b808f98956ef1e7"},
+ {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aa2ce79f3889720b46e0aaba338148a1069aea55fda2c29e0626b4db20d9fcb7"},
+ {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:34bb30c095342797608727baf5c8aa122406aa5edfa12107b8e08eb432d4c5d7"},
+ {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:25ecb1dffc5e409ca42f01a2b2437f93024ff1612c1e7983bad9ee191a5e8828"},
+ {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:aa5eedfc2461c16a092a2fabc5895f159915f25731740c9152a1b00f4bcf629a"},
+ {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:7d1a6e403ac8f1d91d8f51c441c3f99367488ed822bda2b40836690d5d0059f5"},
+ {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3e4d710ff6539026e49f15a3797c6b1053573c2b65210373ef0eec24480b900b"},
+ {file = "regex-2022.3.15-cp36-cp36m-win32.whl", hash = "sha256:0100f0ded953b6b17f18207907159ba9be3159649ad2d9b15535a74de70359d3"},
+ {file = "regex-2022.3.15-cp36-cp36m-win_amd64.whl", hash = "sha256:f320c070dea3f20c11213e56dbbd7294c05743417cde01392148964b7bc2d31a"},
+ {file = "regex-2022.3.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fc8c7958d14e8270171b3d72792b609c057ec0fa17d507729835b5cff6b7f69a"},
+ {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ca6dcd17f537e9f3793cdde20ac6076af51b2bd8ad5fe69fa54373b17b48d3c"},
+ {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0214ff6dff1b5a4b4740cfe6e47f2c4c92ba2938fca7abbea1359036305c132f"},
+ {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a98ae493e4e80b3ded6503ff087a8492db058e9c68de371ac3df78e88360b374"},
+ {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b1cc70e31aacc152a12b39245974c8fccf313187eead559ee5966d50e1b5817"},
+ {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4829db3737480a9d5bfb1c0320c4ee13736f555f53a056aacc874f140e98f64"},
+ {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:303b15a3d32bf5fe5a73288c316bac5807587f193ceee4eb6d96ee38663789fa"},
+ {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:dc7b7c16a519d924c50876fb152af661a20749dcbf653c8759e715c1a7a95b18"},
+ {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ce3057777a14a9a1399b81eca6a6bfc9612047811234398b84c54aeff6d536ea"},
+ {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:48081b6bff550fe10bcc20c01cf6c83dbca2ccf74eeacbfac240264775fd7ecf"},
+ {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dcbb7665a9db9f8d7642171152c45da60e16c4f706191d66a1dc47ec9f820aed"},
+ {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c155a1a80c5e7a8fa1d9bb1bf3c8a953532b53ab1196092749bafb9d3a7cbb60"},
+ {file = "regex-2022.3.15-cp37-cp37m-win32.whl", hash = "sha256:04b5ee2b6d29b4a99d38a6469aa1db65bb79d283186e8460542c517da195a8f6"},
+ {file = "regex-2022.3.15-cp37-cp37m-win_amd64.whl", hash = "sha256:797437e6024dc1589163675ae82f303103063a0a580c6fd8d0b9a0a6708da29e"},
+ {file = "regex-2022.3.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8afcd1c2297bc989dceaa0379ba15a6df16da69493635e53431d2d0c30356086"},
+ {file = "regex-2022.3.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0066a6631c92774391f2ea0f90268f0d82fffe39cb946f0f9c6b382a1c61a5e5"},
+ {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8248f19a878c72d8c0a785a2cd45d69432e443c9f10ab924c29adda77b324ae"},
+ {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d1f3ea0d1924feb4cf6afb2699259f658a08ac6f8f3a4a806661c2dfcd66db1"},
+ {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:794a6bc66c43db8ed06698fc32aaeaac5c4812d9f825e9589e56f311da7becd9"},
+ {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d1445824944e642ffa54c4f512da17a953699c563a356d8b8cbdad26d3b7598"},
+ {file = "regex-2022.3.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f553a1190ae6cd26e553a79f6b6cfba7b8f304da2071052fa33469da075ea625"},
+ {file = "regex-2022.3.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:75a5e6ce18982f0713c4bac0704bf3f65eed9b277edd3fb9d2b0ff1815943327"},
+ {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f16cf7e4e1bf88fecf7f41da4061f181a6170e179d956420f84e700fb8a3fd6b"},
+ {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dad3991f0678facca1a0831ec1ddece2eb4d1dd0f5150acb9440f73a3b863907"},
+ {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:491fc754428514750ab21c2d294486223ce7385446f2c2f5df87ddbed32979ae"},
+ {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:6504c22c173bb74075d7479852356bb7ca80e28c8e548d4d630a104f231e04fb"},
+ {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01c913cf573d1da0b34c9001a94977273b5ee2fe4cb222a5d5b320f3a9d1a835"},
+ {file = "regex-2022.3.15-cp38-cp38-win32.whl", hash = "sha256:029e9e7e0d4d7c3446aa92474cbb07dafb0b2ef1d5ca8365f059998c010600e6"},
+ {file = "regex-2022.3.15-cp38-cp38-win_amd64.whl", hash = "sha256:947a8525c0a95ba8dc873191f9017d1b1e3024d4dc757f694e0af3026e34044a"},
+ {file = "regex-2022.3.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:591d4fba554f24bfa0421ba040cd199210a24301f923ed4b628e1e15a1001ff4"},
+ {file = "regex-2022.3.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9809404528a999cf02a400ee5677c81959bc5cb938fdc696b62eb40214e3632"},
+ {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f08a7e4d62ea2a45557f561eea87c907222575ca2134180b6974f8ac81e24f06"},
+ {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a86cac984da35377ca9ac5e2e0589bd11b3aebb61801204bd99c41fac516f0d"},
+ {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:286908cbe86b1a0240a867aecfe26a439b16a1f585d2de133540549831f8e774"},
+ {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b7494df3fdcc95a1f76cf134d00b54962dd83189520fd35b8fcd474c0aa616d"},
+ {file = "regex-2022.3.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b1ceede92400b3acfebc1425937454aaf2c62cd5261a3fabd560c61e74f6da3"},
+ {file = "regex-2022.3.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0317eb6331146c524751354ebef76a7a531853d7207a4d760dfb5f553137a2a4"},
+ {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c144405220c5ad3f5deab4c77f3e80d52e83804a6b48b6bed3d81a9a0238e4c"},
+ {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5b2e24f3ae03af3d8e8e6d824c891fea0ca9035c5d06ac194a2700373861a15c"},
+ {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f2c53f3af011393ab5ed9ab640fa0876757498aac188f782a0c620e33faa2a3d"},
+ {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:060f9066d2177905203516c62c8ea0066c16c7342971d54204d4e51b13dfbe2e"},
+ {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:530a3a16e57bd3ea0dff5ec2695c09632c9d6c549f5869d6cf639f5f7153fb9c"},
+ {file = "regex-2022.3.15-cp39-cp39-win32.whl", hash = "sha256:78ce90c50d0ec970bd0002462430e00d1ecfd1255218d52d08b3a143fe4bde18"},
+ {file = "regex-2022.3.15-cp39-cp39-win_amd64.whl", hash = "sha256:c5adc854764732dbd95a713f2e6c3e914e17f2ccdc331b9ecb777484c31f73b6"},
+ {file = "regex-2022.3.15.tar.gz", hash = "sha256:0a7b75cc7bb4cc0334380053e4671c560e31272c9d2d5a6c4b8e9ae2c9bd0f82"},
+]
+requests = [
+ {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
+ {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
+]
+requests-cache = [
+ {file = "requests-cache-0.9.3.tar.gz", hash = "sha256:b32f8afba2439e1b3e12cba511c8f579271eff827f063210d62f9efa5bed6564"},
+ {file = "requests_cache-0.9.3-py3-none-any.whl", hash = "sha256:d8b32405b2725906aa09810f4796e54cc03029de269381b404c426bae927bada"},
+]
+restructuredtext-lint = [
+ {file = "restructuredtext_lint-1.4.0.tar.gz", hash = "sha256:1b235c0c922341ab6c530390892eb9e92f90b9b75046063e047cacfb0f050c45"},
+]
+"ruamel.yaml" = [
+ {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"},
+ {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"},
+]
+"ruamel.yaml.clib" = [
+ {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0"},
+ {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:066f886bc90cc2ce44df8b5f7acfc6a7e2b2e672713f027136464492b0c34d7c"},
+ {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7"},
+ {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win32.whl", hash = "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee"},
+ {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de"},
+ {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-manylinux2014_aarch64.whl", hash = "sha256:d3c620a54748a3d4cf0bcfe623e388407c8e85a4b06b8188e126302bcab93ea8"},
+ {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-manylinux2014_aarch64.whl", hash = "sha256:210c8fcfeff90514b7133010bf14e3bad652c8efde6b20e00c43854bf94fa5a6"},
+ {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-manylinux2014_aarch64.whl", hash = "sha256:61bc5e5ca632d95925907c569daa559ea194a4d16084ba86084be98ab1cec1c6"},
+ {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-manylinux2014_aarch64.whl", hash = "sha256:1b4139a6ffbca8ef60fdaf9b33dec05143ba746a6f0ae0f9d11d38239211d335"},
+ {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-2.3.5-py3-none-any.whl", hash = "sha256:2227fcac1b22b53c1615af78872b48348661691450aa25d6704a5504dbd1f7e2"},
+ {file = "safety-2.3.5.tar.gz", hash = "sha256:a60c11f8952f412cbb165d70cb1f673a3b43a2ba9a93ce11f97e6a4de834aa3a"},
+]
+setuptools = [
+ {file = "setuptools-65.5.1-py3-none-any.whl", hash = "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31"},
+ {file = "setuptools-65.5.1.tar.gz", hash = "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f"},
+]
+six = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+smmap = [
+ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
+ {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
+]
+snowballstemmer = [
+ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
+ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
+]
+sortedcontainers = [
+ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
+ {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
+]
+sphinx = [
+ {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"},
+ {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"},
+]
+sphinx-autodoc-typehints = [
+ {file = "sphinx_autodoc_typehints-1.20.0-py3-none-any.whl", hash = "sha256:2fb4f460b0dcd9c7e83f48e72d5f741441ddb858b73eb9159ef3bc1e7a22ec78"},
+ {file = "sphinx_autodoc_typehints-1.20.0.tar.gz", hash = "sha256:776a6439f1f602d4685f5046d00a90b38d8c075884ee81fe075260e92d152d9b"},
+]
+sphinx-typlog-theme = [
+ {file = "sphinx_typlog_theme-0.8.0-py2.py3-none-any.whl", hash = "sha256:b0ab728ab31d071523af0229bcb6427a13493958b3fc2bb7db381520fab77de4"},
+ {file = "sphinx_typlog_theme-0.8.0.tar.gz", hash = "sha256:61dbf97b1fde441bd03a5409874571e229898b67fb3080400837b8f4cee46659"},
+]
+sphinxcontrib-applehelp = [
+ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
+ {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
+]
+sphinxcontrib-devhelp = [
+ {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
+ {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
+]
+sphinxcontrib-htmlhelp = [
+ {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"},
+ {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"},
+]
+sphinxcontrib-jsmath = [
+ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
+ {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
+]
+sphinxcontrib-mermaid = [
+ {file = "sphinxcontrib-mermaid-0.7.1.tar.gz", hash = "sha256:aa8a40b50ec86ad12824b62180240ca52a9bda8424455d7eb252eae9aa5d293c"},
+ {file = "sphinxcontrib_mermaid-0.7.1-py2.py3-none-any.whl", hash = "sha256:3e20de1937c30dfa807e446bf99983d73d0dd3dc5c6524addda59800fe928762"},
+]
+sphinxcontrib-qthelp = [
+ {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
+ {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
+]
+sphinxcontrib-serializinghtml = [
+ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"},
+ {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"},
+]
+stevedore = [
+ {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"},
+ {file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"},
+]
+strenum = [
+ {file = "StrEnum-0.4.7-py3-none-any.whl", hash = "sha256:28ede0075ea0d990144d334c03509f14ee33e2e732289cb6e496ed7bae73fd1f"},
+ {file = "StrEnum-0.4.7.tar.gz", hash = "sha256:6019e9cc1738af6a236c022b82184372178ecadf12438f11f5b680a02a462377"},
+]
+testfixtures = [
+ {file = "testfixtures-6.18.5-py2.py3-none-any.whl", hash = "sha256:7de200e24f50a4a5d6da7019fb1197aaf5abd475efb2ec2422fdcf2f2eb98c1d"},
+ {file = "testfixtures-6.18.5.tar.gz", hash = "sha256:02dae883f567f5b70fd3ad3c9eefb95912e78ac90be6c7444b5e2f46bf572c84"},
+]
+text-unidecode = [
+ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
+ {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
+]
+toml = [
+ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+tomli = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+tomlkit = [
+ {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"},
+ {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"},
+]
+typed-ast = [
+ {file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"},
+ {file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"},
+ {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"},
+ {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"},
+ {file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"},
+ {file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"},
+ {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"},
+ {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"},
+ {file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"},
+ {file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"},
+ {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"},
+ {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"},
+ {file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"},
+ {file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"},
+ {file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"},
+ {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"},
+ {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"},
+ {file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"},
+ {file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"},
+ {file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"},
+ {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"},
+ {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"},
+ {file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"},
+ {file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"},
+]
+typeguard = [
+ {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"},
+ {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"},
+]
+typing-extensions = [
+ {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
+ {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
+]
+url-normalize = [
+ {file = "url-normalize-1.4.3.tar.gz", hash = "sha256:d23d3a070ac52a67b83a1c59a0e68f8608d1cd538783b401bc9de2c0fac999b2"},
+ {file = "url_normalize-1.4.3-py2.py3-none-any.whl", hash = "sha256:ec3c301f04e5bb676d333a7fa162fa977ad2ca04b7e652bfc9fac4e405728eed"},
+]
+urllib3 = [
+ {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
+ {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
+]
+wemake-python-styleguide = [
+ {file = "wemake-python-styleguide-0.17.0.tar.gz", hash = "sha256:c8869fac392019c2bb3eae4287399245d10d2726b23f1b3c68d1564909c3a71a"},
+ {file = "wemake_python_styleguide-0.17.0-py3-none-any.whl", hash = "sha256:d10b953bbe4fba83a34f4c224a0e1849ede89e486eacfc760690e6c87a28eaae"},
+]
+win32-setctime = [
+ {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},
+ {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"},
+]
+zipp = [
+ {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"},
+ {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"},
+]
diff --git a/pyproject.toml b/pyproject.toml
index d7bdda3..7c3db97 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,15 +1,15 @@
[build-system]
-requires = ["poetry>=0.12"]
-build-backend = "poetry.masonry.api"
+requires = ["poetry-core>=1.9.0"]
+build-backend = "poetry.core.masonry.api"
[tool.nitpick]
-style = "https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/master/styles/nitpick-style-wemake.toml"
+style = "https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/0.19.2/styles/nitpick-style-wemake.toml"
[tool.poetry]
name = "classes"
-version = "0.1.0"
+version = "0.4.1"
description = "Smart, pythonic, ad-hoc, typed polymorphism for Python"
license = "BSD-2-Clause"
@@ -44,28 +44,30 @@ classifiers = [
]
[tool.poetry.dependencies]
-python = "^3.6"
-typing_extensions = "^3.7"
+python = "^3.7"
+typing_extensions = ">=3.10,<5.0"
[tool.poetry.dev-dependencies]
-mypy = "^0.740"
-wemake-python-styleguide = "^0.12.4"
-flake8-pytest = "^1.3"
-flake8-pytest-style = "^0.1.3"
-flake8-pyi = "^19.3"
-nitpick = "^0.21.0"
-
-safety = "^1.8"
-
-pytest = "^5.1"
-pytest-cov = "^2.7"
-pytest-randomly = "^3.1"
-pytest-mypy-plugins = "^1.0"
-
-sphinx = "^2.2"
-sphinx-autodoc-typehints = "^1.7"
-sphinxcontrib-mermaid = "^0.3.1"
-sphinx-typlog-theme = "^0.7.1"
-doc8 = "^0.8.0"
-m2r = "^0.2.1"
-tomlkit = "^0.5.5"
+mypy = "^0.942"
+
+wemake-python-styleguide = "^0.17"
+flake8-pytest-style = "^1.6"
+nitpick = "^0.32"
+
+safety = "^2.3"
+
+pytest = "^7.2"
+pytest-cov = "^4.0"
+pytest-randomly = "^3.12"
+pytest-mypy-plugins = "^1.9"
+
+sphinx = "^5.2"
+sphinx-autodoc-typehints = "^1.20"
+sphinxcontrib-mermaid = "^0.7"
+sphinx-typlog-theme = "^0.8"
+doc8 = "^1.0"
+m2r2 = "^0.3"
+tomlkit = "^0.11"
+codespell = "^2.2"
+
+phantom-types = "^1.0"
diff --git a/setup.cfg b/setup.cfg
index 5d8b467..5ba29d1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -3,31 +3,24 @@
# https://docs.python.org/3/distutils/configfile.html
-[coverage:run]
-omit =
- # We test mypy plugins with `pytest-mypy-plugins`,
- # which does not work with coverage:
- classes/contrib/mypy/*
-
-
[flake8]
format = wemake
-show-source = True
-doctests = False
-enable-extensions = G
-statistics = False
+show-source = true
+doctests = false
+statistics = false
+
+# darglint configuration:
+# https://github.com/terrencepreilly/darglint
+strictness = long
+docstring-style = numpy
# Plugins:
-accept-encodings = utf-8
max-complexity = 6
max-line-length = 80
-radon-max-cc = 10
-radon-no-assert = True
-radon-show-closures = True
# wemake-python-styleguide
max-line-complexity = 16
-i-control-code = False
+i-control-code = false
exclude =
# Trash and cache:
@@ -38,13 +31,21 @@ exclude =
*.egg
temp
-ignore = D100, D104, D401, W504, X100, WPS121, RST299, RST303, RST304
+ignore = D100, D104, D401, W504, X100, WPS121, RST299, RST303, RST304, DAR103, DAR203
per-file-ignores =
- classes/__init__.py: F401, WPS113
- classes/typeclass.py: F811, WPS320, WPS428
+ classes/__init__.py: F401, WPS113, WPS436
+ 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, WPS432, WPS436
+ tests/*.py: S101, WPS202, WPS226, WPS431, WPS432, WPS436
+
+
+[isort]
+# isort configuration:
+# # https://pycqa.github.io/isort/docs/configuration/profiles.html
+profile = wemake
[tool:pytest]
@@ -55,24 +56,32 @@ norecursedirs = temp *.egg .eggs dist build docs .tox .git __pycache__
# so you can see whether it gives you any performance gain, or just gives
# you an overhead. See `docs/template/development-process.rst`.
addopts =
+ --strict-config
+ --strict-markers
--doctest-modules
--doctest-glob='*.md'
--doctest-glob='*.rst'
--cov=classes
--cov-report=term:skip-covered
--cov-report=html
+ --cov-report=xml
--cov-branch
--cov-fail-under=100
--mypy-ini-file=setup.cfg
-[isort]
-# See https://github.com/timothycrosley/isort#multi-line-output-modes
-multi_line_output = 3
-include_trailing_comma = true
-default_section = FIRSTPARTY
-# Should be: 80 - 1
-line_length = 79
+[coverage:run]
+omit =
+ # We test mypy plugins with `pytest-mypy-plugins`,
+ # which does not work with coverage:
+ classes/contrib/mypy/*
+
+[coverage:report]
+exclude_lines =
+ # a more strict default pragma
+ \# pragma: no cover\b
+
+ ^if TYPE_CHECKING:
[mypy]
@@ -80,27 +89,34 @@ line_length = 79
# Plugins, includes custom:
plugins =
- classes.contrib.mypy.typeclass_plugin
-
-allow_redefinition = False
-check_untyped_defs = True
-disallow_any_explicit = True
-# disallow_any_generics = True
-# disallow_untyped_calls = True
-ignore_errors = False
-ignore_missing_imports = True
-implicit_reexport = False
-strict_optional = True
-strict_equality = True
-no_implicit_optional = True
-warn_no_return = True
-warn_unused_ignores = True
-warn_redundant_casts = True
-warn_unused_configs = True
-warn_unreachable = True
+ classes.contrib.mypy.classes_plugin
+
+allow_redefinition = false
+check_untyped_defs = true
+disallow_any_explicit = true
+# disallow_any_generics = true
+disallow_untyped_calls = true
+ignore_errors = false
+ignore_missing_imports = true
+implicit_reexport = false
+local_partial_types = true
+strict_optional = true
+strict_equality = true
+no_implicit_optional = true
+warn_no_return = true
+warn_unused_ignores = true
+warn_redundant_casts = true
+warn_unused_configs = true
+warn_unreachable = true
[doc8]
+# doc8 configuration: https://pypi.org/project/doc8/
ignore-path = docs/_build
max-line-length = 80
-sphinx = True
+sphinx = true
+
+
+[codespell]
+# codespell configuration: https://pypi.org/project/codespell
+skip = __pycache__,_build,.mypy_cache,docs/htmlcov
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..4275ffe
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,17 @@
+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() -> Callable[[_TypeClass], ContextManager]:
+ """Fixture to clear typeclass'es cache before and after."""
+ @contextmanager
+ def factory(typeclass: _TypeClass) -> Iterator[None]:
+ typeclass._dispatch_cache.clear() # noqa: WPS437
+ yield
+ typeclass._dispatch_cache.clear() # noqa: WPS437
+ return factory
diff --git a/tests/test_associated_type/test_variadic_generic.py b/tests/test_associated_type/test_variadic_generic.py
new file mode 100644
index 0000000..e884d90
--- /dev/null
+++ b/tests/test_associated_type/test_variadic_generic.py
@@ -0,0 +1,36 @@
+import sys
+from typing import TypeVar
+
+import pytest
+
+from classes import AssociatedType
+
+_FirstType = TypeVar('_FirstType')
+
+
+@pytest.mark.skipif(sys.version_info[:2] >= (3, 10), reason='Does not work')
+def test_type_validation():
+ """Ensures that type validation still works."""
+ with pytest.raises(TypeError):
+ class Example(AssociatedType[1]): # type: ignore
+ """Should fail, because of ``1``."""
+
+
+def test_type_resets():
+ """Ensures that params are reset correctly."""
+ class Example(AssociatedType[_FirstType]):
+ """Correct type."""
+
+ old_args = Example.__parameters__ # type: ignore # noqa: WPS609
+ assert Example[int, int, int] # type: ignore
+ assert Example.__parameters__ == old_args # type: ignore # noqa: WPS609
+
+
+def test_subtype_is_variadic():
+ """Ensures that subtypes are variadic."""
+ class Example(AssociatedType[_FirstType]):
+ """Correct type."""
+
+ assert Example[int]
+ assert Example[int, int] # type: ignore
+ assert Example[int, int, str] # type: ignore
diff --git a/tests/test_supports.py b/tests/test_supports.py
new file mode 100644
index 0000000..3c95fb6
--- /dev/null
+++ b/tests/test_supports.py
@@ -0,0 +1,73 @@
+from typing import List, Sized
+
+import pytest
+
+from classes import typeclass
+
+
+class _ListOfStrMeta(type):
+ def __instancecheck__(cls, other) -> bool:
+ return (
+ isinstance(other, list) and
+ bool(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."""
+
+
+@my_len.instance(protocol=Sized)
+def _my_len_sized(instance: Sized) -> int:
+ return 0
+
+
+@my_len.instance(list)
+def _my_len_list(instance: list) -> int:
+ return 1
+
+
+@my_len.instance(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
+ (1, False), # default impl
+ (_MyList(), True), # mro fallback
+ (_ListOfStr(), True), # mro fallback
+ (_ListOfStr(['a']), True), # mro fallback
+])
+def test_supports(data_type, expected: bool, clear_cache) -> None:
+ """Ensures that ``.supports`` works correctly."""
+ 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_arguments.py b/tests/test_typeclass/test_arguments.py
new file mode 100644
index 0000000..6b0f354
--- /dev/null
+++ b/tests/test_typeclass/test_arguments.py
@@ -0,0 +1,36 @@
+import pytest
+
+from classes import typeclass
+
+
+@typeclass
+def example(instance) -> int:
+ """Example typeclass."""
+
+
+def _example_str(instance: str) -> int:
+ return len(instance)
+
+
+def test_invalid_arguments_empty() -> None:
+ """Tests that invalid arguments do raise."""
+ with pytest.raises(ValueError, match='At least one argument to .*'):
+ example.instance()
+
+
+def test_invalid_arguments_delegate() -> None:
+ """Tests that invalid arguments do raise."""
+ with pytest.raises(ValueError, match='Only a single argument can .*'):
+ example.instance(str, delegate=str)
+
+
+def test_invalid_arguments_protocol() -> None:
+ """Tests that invalid arguments do raise."""
+ with pytest.raises(ValueError, match='Only a single argument can .*'):
+ example.instance(str, protocol=str)
+
+
+def test_invalid_arguments_all() -> None:
+ """Tests that invalid arguments do raise."""
+ with pytest.raises(ValueError, match='Only a single argument can .*'):
+ example.instance(str, protocol=True, delegate=str) # type: ignore
diff --git a/tests/test_typeclass/test_cache.py b/tests/test_typeclass/test_cache.py
new file mode 100644
index 0000000..3b02b45
--- /dev/null
+++ b/tests/test_typeclass/test_cache.py
@@ -0,0 +1,60 @@
+from abc import ABCMeta, abstractmethod
+
+from classes import typeclass
+
+
+@typeclass
+def my_typeclass(instance) -> int:
+ """Example typeclass."""
+
+
+class _MyABC(object, metaclass=ABCMeta):
+ @abstractmethod
+ def get_number(self) -> int:
+ """Example abstract method."""
+
+
+class _MyConcrete(_MyABC):
+ def get_number(self) -> int:
+ """Concrete method."""
+ return 1
+
+
+class _MyRegistered(object):
+ def get_number(self) -> int:
+ """Would be registered in ``_MyABC`` later."""
+ return 2
+
+
+def _my_int(instance: int) -> int:
+ return instance
+
+
+def _my_abc(instance: _MyABC) -> int:
+ return instance.get_number()
+
+
+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
+
+ my_typeclass.instance(_MyABC)(_my_abc)
+ assert not my_typeclass._dispatch_cache # noqa: WPS437
+
+ assert my_typeclass(_MyConcrete()) == 1
+ assert _MyConcrete in my_typeclass._dispatch_cache # noqa: WPS437
+
+ _MyABC.register(_MyRegistered)
+ assert my_typeclass(_MyRegistered()) == 2 # type: ignore
+ assert _MyRegistered in my_typeclass._dispatch_cache # noqa: WPS437
+
+
+def test_cached_calls(clear_cache) -> None:
+ """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
new file mode 100644
index 0000000..76190ba
--- /dev/null
+++ b/tests/test_typeclass/test_call.py
@@ -0,0 +1,79 @@
+from typing import List, Sized
+
+import pytest
+
+from classes import typeclass
+
+
+class _ListOfStrMeta(type):
+ def __instancecheck__(cls, other) -> bool:
+ return (
+ isinstance(other, list) and
+ bool(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(delegate=_ListOfStr)
+def _my_len_list_str(instance: List[str]) -> int:
+ return 0
+
+
+@my_len.instance(list)
+def _my_len_list(instance: list) -> int:
+ return 1
+
+
+@my_len.instance(protocol=Sized)
+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), # concrete type
+ ([], 1), # direct list call
+ ([1, 2, 3], 1), # direct list call
+ ('', 2), # sized protocol
+ (1, 3), # object fallback
+])
+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
diff --git a/tests/test_typeclass/test_callback.py b/tests/test_typeclass/test_callback.py
index acb2008..692ab06 100644
--- a/tests/test_typeclass/test_callback.py
+++ b/tests/test_typeclass/test_callback.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
from typing import Callable
from classes import typeclass
@@ -22,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 e05ec06..b07059c 100644
--- a/tests/test_typeclass/test_protocols.py
+++ b/tests/test_typeclass/test_protocols.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
from typing import Sized
from classes import typeclass
@@ -10,7 +8,7 @@ def protocols(instance, other: str) -> str:
"""Example typeclass for protocols."""
-@protocols.instance(Sized, is_protocol=True)
+@protocols.instance(protocol=Sized)
def _sized_protocols(instance: Sized, other: str) -> str:
return str(len(instance)) + other
@@ -25,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 6832fdd..9e9e0d6 100644
--- a/tests/test_typeclass/test_regular_types.py
+++ b/tests/test_typeclass/test_regular_types.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
from typing import List
from classes import typeclass
@@ -20,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
new file mode 100644
index 0000000..40df2bb
--- /dev/null
+++ b/tests/test_typeclass/test_repr.py
@@ -0,0 +1,23 @@
+from classes import AssociatedType, typeclass
+
+
+class MyType(AssociatedType):
+ """Docs for type."""
+
+
+@typeclass(MyType)
+def my_typeclass_with_type(instance) -> str:
+ """Docs."""
+
+
+@typeclass
+def my_typeclass(instance) -> str:
+ """Docs."""
+
+
+def test_str() -> None:
+ """Ensures that ``str`` is correct."""
+ assert str(my_typeclass) == ''
+ assert str(
+ my_typeclass_with_type,
+ ) == ''
diff --git a/tests/test_typeclass/test_typed_dict.py b/tests/test_typeclass/test_typed_dict.py
new file mode 100644
index 0000000..de776c3
--- /dev/null
+++ b/tests/test_typeclass/test_typed_dict.py
@@ -0,0 +1,51 @@
+import sys
+
+import pytest
+from typing_extensions import TypedDict
+
+from classes import typeclass
+
+if sys.version_info[:2] >= (3, 9): # noqa: C901
+ pytestmark = pytest.mark.skip('Only python3.7 and python3.8 are supported')
+else:
+ class _User(TypedDict):
+ name: str
+ registered: bool
+
+ class _UserDictMeta(type):
+ def __instancecheck__(cls, arg: object) -> bool:
+ return (
+ isinstance(arg, dict) and
+ isinstance(arg.get('name'), str) and
+ isinstance(arg.get('registered'), bool)
+ )
+
+ _Meta = type('_Meta', (_UserDictMeta, type(TypedDict)), {})
+
+ class UserDict(_User, metaclass=_Meta):
+ """We use this class to represent a typed dict with instance check."""
+
+ @typeclass
+ def get_name(instance) -> str:
+ """Example typeclass."""
+
+ @get_name.instance(delegate=UserDict)
+ def _get_name_user_dict(instance: UserDict) -> str:
+ return instance['name']
+
+ def test_correct_typed_dict():
+ """Ensures that typed dict dispatch works."""
+ user: UserDict = {'name': 'sobolevn', 'registered': True}
+ assert get_name(user) == 'sobolevn'
+
+ @pytest.mark.parametrize('test_value', [
+ [],
+ {},
+ {'name': 'sobolevn', 'registered': None},
+ {'name': 'sobolevn'},
+ {'registered': True},
+ ])
+ def test_wrong_typed_dict(test_value):
+ """Ensures that typed dict dispatch works."""
+ with pytest.raises(NotImplementedError):
+ get_name(test_value)
diff --git a/tests/test_typeclass/test_typing_type.py b/tests/test_typeclass/test_typing_type.py
new file mode 100644
index 0000000..a46cbc9
--- /dev/null
+++ b/tests/test_typeclass/test_typing_type.py
@@ -0,0 +1,48 @@
+from typing import Type
+
+import pytest
+
+from classes import typeclass
+
+
+class _MyClass(object):
+ """We use this class to test `Type[MyClass]`."""
+
+
+class _MyClassTypeMeta(type):
+ def __instancecheck__(cls, typ) -> bool:
+ return typ is _MyClass
+
+
+class _MyClassType(Type[_MyClass], metaclass=_MyClassTypeMeta): # type: ignore
+ """Delegate class."""
+
+
+@typeclass
+def class_type(typ) -> Type:
+ """Returns the type representation."""
+
+
+@class_type.instance(delegate=_MyClassType)
+def _my_class_type(typ: Type[_MyClass]) -> Type:
+ return typ
+
+
+def test_correct_class_type():
+ """Ensures `Type[T]` works correctly with delegate."""
+ assert class_type(_MyClass) is _MyClass
+
+
+@pytest.mark.parametrize('typ', [
+ int, float, type, dict, list,
+])
+def test_wrong_class_type(typ):
+ """Ensures other types doesn't works with our delegate."""
+ with pytest.raises(NotImplementedError):
+ class_type(typ)
+
+
+def test_passing_class_type_instance():
+ """Ensures passing a instance of the expected type doesn't work."""
+ with pytest.raises(NotImplementedError):
+ class_type(_MyClass()) # type: ignore[arg-type]
diff --git a/typesafety/test_associated_type/test_validation/test_base_classes.yml b/typesafety/test_associated_type/test_validation/test_base_classes.yml
new file mode 100644
index 0000000..46f88d0
--- /dev/null
+++ b/typesafety/test_associated_type/test_validation/test_base_classes.yml
@@ -0,0 +1,31 @@
+- case: typeclass_definied_by_wrong_type
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ class ToJson(object):
+ ...
+
+ @typeclass(ToJson)
+ def to_json(instance, verbose: bool = False) -> str:
+ ...
+ out: |
+ main:6: error: Single direct subclass of "classes._typeclass.AssociatedType" required; got "main.ToJson"
+
+
+- case: typeclass_definied_by_multiple_parents
+ disable_cache: false
+ main: |
+ from classes import typeclass, AssociatedType
+
+ class A(object):
+ ...
+
+ class ToJson(AssociatedType, A):
+ ...
+
+ @typeclass(ToJson)
+ def to_json(instance, verbose: bool = False) -> str:
+ ...
+ out: |
+ main:9: error: Single direct subclass of "classes._typeclass.AssociatedType" required; got "main.ToJson"
diff --git a/typesafety/test_associated_type/test_validation/test_bodies.yml b/typesafety/test_associated_type/test_validation/test_bodies.yml
new file mode 100644
index 0000000..efe71dc
--- /dev/null
+++ b/typesafety/test_associated_type/test_validation/test_bodies.yml
@@ -0,0 +1,42 @@
+- case: associated_type_with_method
+ disable_cache: false
+ main: |
+ from classes import AssociatedType, typeclass
+
+ class Compare(AssociatedType):
+ def some(self):
+ ...
+
+ @typeclass(Compare)
+ def compare(instance) -> int:
+ ...
+ out: |
+ main:7: error: Associated types must not have bodies
+
+
+- case: associated_type_with_attr
+ disable_cache: false
+ main: |
+ from classes import AssociatedType, typeclass
+
+ class Compare(AssociatedType):
+ x = 1
+
+ @typeclass(Compare)
+ def compare(instance) -> int:
+ ...
+ out: |
+ main:6: error: Associated types must not have bodies
+
+
+- case: associated_type_with_pass
+ disable_cache: false
+ main: |
+ from classes import AssociatedType, typeclass
+
+ class MyType(AssociatedType):
+ pass
+
+ @typeclass(MyType)
+ def sum_all(instance) -> int:
+ pass
diff --git a/typesafety/test_associated_type/test_validation/test_generic_missmatch.yml b/typesafety/test_associated_type/test_validation/test_generic_missmatch.yml
new file mode 100644
index 0000000..14fc15a
--- /dev/null
+++ b/typesafety/test_associated_type/test_validation/test_generic_missmatch.yml
@@ -0,0 +1,17 @@
+- case: typeclass_definied_by_wrong_type
+ disable_cache: false
+ main: |
+ from typing import List, TypeVar
+
+ from classes import AssociatedType, Supports, typeclass
+
+ X = TypeVar('X')
+
+ class Compare(AssociatedType):
+ ...
+
+ @typeclass(Compare)
+ def compare(instance: List[X]) -> X:
+ ...
+ out: |
+ main:10: error: Generic type "main.Compare" with "0" type arguments does not match generic instance declaration "builtins.list[X`-1]" with "1" type arguments
diff --git a/typesafety/test_associated_type/test_validation/test_reuse.yml b/typesafety/test_associated_type/test_validation/test_reuse.yml
new file mode 100644
index 0000000..3a699b8
--- /dev/null
+++ b/typesafety/test_associated_type/test_validation/test_reuse.yml
@@ -0,0 +1,17 @@
+- case: associated_type_reuse
+ disable_cache: false
+ main: |
+ from classes import typeclass, AssociatedType
+
+ class ToJson(AssociatedType):
+ ...
+
+ @typeclass(ToJson)
+ def to_json(instance) -> str:
+ ...
+
+ @typeclass(ToJson)
+ def from_json(instance) -> str:
+ ...
+ out: |
+ main:10: error: AssociatedType "main.ToJson" must not be reused, originally associated with "main.from_json"
diff --git a/typesafety/test_associated_type/test_variadic.yml b/typesafety/test_associated_type/test_variadic.yml
new file mode 100644
index 0000000..011a270
--- /dev/null
+++ b/typesafety/test_associated_type/test_variadic.yml
@@ -0,0 +1,25 @@
+- case: associated_type_variadic
+ disable_cache: false
+ main: |
+ from classes import AssociatedType
+ from typing import TypeVar
+
+ _A = TypeVar('_A')
+ _B = TypeVar('_B')
+
+ class One(AssociatedType[_A]):
+ ...
+
+ class Two(AssociatedType[_A, _B]):
+ ...
+
+ # Correct:
+ One[int]
+ Two[int, str]
+
+ # Wrong:
+ One[int, str]
+ Two[int]
+ out: |
+ main:18: error: Type application has too many types (1 expected)
+ main:19: error: Type application has too few types (2 expected)
diff --git a/typesafety/test_supports_type/test_generic.yml b/typesafety/test_supports_type/test_generic.yml
new file mode 100644
index 0000000..f86bd71
--- /dev/null
+++ b/typesafety/test_supports_type/test_generic.yml
@@ -0,0 +1,181 @@
+- case: supports_generic_correct1
+ disable_cache: false
+ main: |
+ from typing import Iterable, List, Set, TypeVar, Union
+ from classes import AssociatedType, Supports, typeclass
+
+ X = TypeVar('X')
+
+ class Some(AssociatedType[X]):
+ ...
+
+ @typeclass(Some)
+ def some(instance: Iterable[X]) -> X:
+ ...
+
+ @some.instance(list)
+ @some.instance(set)
+ def _some_ex(instance: Union[List[X], Set[X]]) -> X:
+ ...
+
+ x: Supports[Some[int]] = [1, 2, 3]
+ y = {1, 2, 3}
+ reveal_type(some(x)) # N: Revealed type is "builtins.int*"
+ reveal_type(some(y)) # N: Revealed type is "builtins.int*"
+
+
+- case: supports_generic_correct2
+ disable_cache: False
+ skip: sys.version_info[:2] < (3, 8)
+ main: |
+ from typing import TypeVar, Union, Mapping, Dict
+ from classes import AssociatedType, Supports, typeclass
+
+ X = TypeVar('X')
+ Y = TypeVar('Y')
+
+ class Some(AssociatedType[X, Y]):
+ ...
+
+ @typeclass(Some)
+ def some(instance: Mapping[X, Y], a: X, b: Y) -> Y:
+ ...
+
+ @some.instance(dict)
+ def _some_ex(instance: Dict[X, Y], a: X, b: Y) -> Y:
+ ...
+
+ x: Supports[Some[int, str]] = {1: 'a'}
+
+ reveal_type(some(x, 1, 'a')) # N: Revealed type is "builtins.str*"
+ reveal_type(some({'a': 1}, 'a', 1)) # N: Revealed type is "builtins.int*"
+
+
+- case: supports_generic_wrong1
+ disable_cache: false
+ main: |
+ from typing import Iterable, List, TypeVar
+ from classes import AssociatedType, Supports, typeclass
+
+ X = TypeVar('X')
+
+ class Some(AssociatedType[X]):
+ ...
+
+ @typeclass(Some)
+ def some(instance: Iterable[X]) -> X:
+ ...
+
+ @some.instance(list)
+ def _some_ex(instance: List[X]) -> X:
+ ...
+
+ x: Supports[Some[int]] = {1, 2, 3}
+ some({1, 2, 3})
+ out: |
+ main:17: error: Incompatible types in assignment (expression has type "Set[int]", variable has type "Supports[Some[int]]")
+ main:18: error: Argument 1 to "some" has incompatible type "Set[int]"; expected "Supports[Some[]]"
+
+
+- case: supports_generic_wrong2
+ disable_cache: false
+ main: |
+ from typing import Iterable, List, Set, TypeVar, Union
+ from classes import AssociatedType, Supports, typeclass
+
+ X = TypeVar('X')
+
+ class Some(AssociatedType[X]):
+ ...
+
+ @typeclass(Some)
+ def some(instance: Iterable[X]) -> X:
+ ...
+
+ @typeclass
+ def other(instance: Iterable[X]) -> X:
+ ...
+
+ @other.instance(list)
+ @some.instance(set)
+ def _some_ex(instance: Union[List[X], Set[X]]) -> X:
+ ...
+
+ x: Supports[Some[int]] = [1, 2, 3]
+ y: Supports[Some[int]] = {1, 2, 3}
+ z: Supports[Some[int]] = 1
+ out: |
+ main:17: error: Found different typeclass ".instance" calls, use only "main.some"
+ main:17: error: Instance "Union[builtins.list[X`-1], builtins.set[X`-1]]" does not match inferred type "builtins.set[_T`1]"
+ main:22: error: Incompatible types in assignment (expression has type "List[int]", variable has type "Supports[Some[int]]")
+ main:23: error: Incompatible types in assignment (expression has type "Set[int]", variable has type "Supports[Some[int]]")
+ main:24: error: Incompatible types in assignment (expression has type "int", variable has type "Supports[Some[int]]")
+
+
+- case: supports_multiple_one_generic_one_regular
+ disable_cache: false
+ main: |
+ from classes import AssociatedType, Supports, typeclass
+ from typing import TypeVar, Iterable, List
+
+ T = TypeVar('T')
+
+ class Some(AssociatedType[T]):
+ ...
+
+ class FromJson(AssociatedType):
+ ...
+
+ @typeclass(Some)
+ def some(instance: Iterable[T]) -> T:
+ ...
+
+ @typeclass(FromJson)
+ def from_json(instance) -> str:
+ ...
+
+ @some.instance(list)
+ def _some_str(instance: List[T]) -> T:
+ ...
+
+ @from_json.instance(str)
+ def _from_json_str(instance: str) -> str:
+ ...
+
+ a: Supports[Some[str], FromJson]
+ reveal_type(some(a)) # N: Revealed type is "builtins.str*"
+
+
+ - case: supports_multiple_two_generics
+ disable_cache: false
+ main: |
+ from classes import AssociatedType, Supports, typeclass
+ from typing import TypeVar, Iterable, List
+
+ T = TypeVar('T')
+
+ class Some(AssociatedType[T]):
+ ...
+
+ class Other(AssociatedType[T]):
+ ...
+
+ @typeclass(Some)
+ def some(instance: Iterable[T]) -> T:
+ ...
+
+ @typeclass(Other)
+ def other(instance: Iterable[T]) -> T:
+ ...
+
+ @some.instance(list)
+ def _some_list(instance: List[T]) -> T:
+ ...
+
+ @other.instance(list)
+ def _other_list(instance: List[T]) -> T:
+ ...
+
+ a: Supports[Some[str], Other[int]]
+ reveal_type(some(a)) # N: Revealed type is "builtins.str*"
+ reveal_type(other(a)) # N: Revealed type is "builtins.int*"
diff --git a/typesafety/test_supports_type/test_regular.yml b/typesafety/test_supports_type/test_regular.yml
new file mode 100644
index 0000000..f307d98
--- /dev/null
+++ b/typesafety/test_supports_type/test_regular.yml
@@ -0,0 +1,265 @@
+- case: typeclass_supports_type
+ disable_cache: false
+ main: |
+ from classes import typeclass, Supports, AssociatedType
+
+ class ToJson(AssociatedType):
+ ...
+
+ @typeclass(ToJson)
+ def to_json(instance) -> str:
+ ...
+
+ @to_json.instance(int)
+ def _to_json_int(instance: int) -> str:
+ return str(instance)
+
+ @to_json.instance(str)
+ def _to_json_str(instance: str) -> str:
+ return instance
+
+ def convert_to_json(instance: Supports[ToJson]) -> str:
+ return to_json(instance)
+
+ convert_to_json(1)
+ convert_to_json('a')
+ convert_to_json(None) # E: Argument 1 to "convert_to_json" has incompatible type "None"; expected "Supports[ToJson]"
+
+
+- case: typeclass_supports_type_restriction
+ disable_cache: false
+ main: |
+ from classes import typeclass, Supports, AssociatedType
+
+ class ToJson(AssociatedType):
+ ...
+
+ @typeclass(ToJson)
+ def to_json(instance: Supports[ToJson]) -> str:
+ ...
+
+ @to_json.instance(int)
+ def _to_json_int(instance: int) -> str:
+ return str(instance)
+
+ to_json(1)
+
+
+- case: typeclass_supports_callback
+ disable_cache: false
+ main: |
+ from classes import typeclass, Supports, AssociatedType
+ from typing import Callable
+
+ class ToJson(AssociatedType):
+ ...
+
+ @typeclass(ToJson)
+ def to_json(instance) -> str:
+ ...
+
+ @to_json.instance(int)
+ def _to_json_int(instance: int) -> str:
+ return str(instance)
+
+ @to_json.instance(str)
+ def _to_json_str(instance: str) -> str:
+ return instance
+
+ def convert_to_json(
+ callback: Callable[[Supports[ToJson]], str],
+ instance: Supports[ToJson],
+ ) -> str:
+ return callback(instance)
+
+ convert_to_json(to_json, 1)
+ convert_to_json(to_json, 'a')
+ convert_to_json(to_json, None) # E: Argument 2 to "convert_to_json" has incompatible type "None"; expected "Supports[ToJson]"
+
+
+- case: typeclass_supports_with_function
+ disable_cache: false
+ main: |
+ from classes import typeclass, Supports
+
+ @typeclass
+ def to_json(instance) -> str:
+ ...
+
+ @to_json.instance(int)
+ def _to_json_int(instance: int) -> str:
+ return str(instance)
+
+ @to_json.instance(str)
+ def _to_json_str(instance: str) -> str:
+ return instance
+
+ def convert_to_json(instance: Supports[to_json]) -> str:
+ ...
+ out: |
+ main:15: error: Function "main.to_json" is not valid as a type
+ main:15: note: Perhaps you need "Callable[...]" or a callback protocol?
+
+
+- case: typeclass_supports_other
+ disable_cache: false
+ main: |
+ from classes import typeclass, Supports, AssociatedType
+
+ class ToJson(AssociatedType):
+ ...
+
+ @typeclass(ToJson)
+ def to_json(instance) -> str:
+ ...
+
+ class Other(AssociatedType):
+ ...
+
+ def convert_to_json(instance: Supports[Other]) -> str:
+ return to_json(instance)
+ out: |
+ main:14: error: Argument 1 to "to_json" has incompatible type "Supports[Other]"; expected "Supports[ToJson]"
+
+
+- case: supports_annotation
+ disable_cache: false
+ main: |
+ from classes import typeclass, Supports, AssociatedType
+
+ class ToJson(AssociatedType):
+ ...
+
+ @typeclass(ToJson)
+ def to_json(instance) -> str:
+ ...
+
+ @to_json.instance(int)
+ def _to_json_int(instance: int) -> str:
+ return str(instance)
+
+ a: Supports[ToJson] = 1
+ b: Supports[ToJson] = 'a' # E: Incompatible types in assignment (expression has type "str", variable has type "Supports[ToJson]")
+
+
+- case: supports_type_bound
+ disable_cache: false
+ main: |
+ from classes import Supports, AssociatedType
+
+ a: Supports[int] # E: Type argument "builtins.int" of "Supports" must be a subtype of "classes._typeclass.AssociatedType"
+
+ class A(AssociatedType):
+ ...
+
+ b: Supports[A, int] # E: Type argument "builtins.int" of "Supports" must be a subtype of "classes._typeclass.AssociatedType"
+
+
+- case: supports_multiple_types
+ disable_cache: false
+ main: |
+ from classes import AssociatedType, Supports, typeclass
+
+ class A(AssociatedType):
+ ...
+
+ class B(AssociatedType):
+ ...
+
+ @typeclass(A)
+ def one(instance) -> bool:
+ ...
+
+ @one.instance(int)
+ def _one_int(instance: int) -> bool:
+ ...
+
+ @typeclass(B)
+ def two(instance) -> bool:
+ ...
+
+ @two.instance(int)
+ def _two_int(instance: int) -> bool:
+ ...
+
+ a: Supports[A] = 1
+ b: Supports[B] = 1
+ ab: Supports[A, B] = 1
+
+
+- case: supports_multiple_types_callback
+ disable_cache: false
+ main: |
+ from classes import AssociatedType, Supports, typeclass
+
+ class ToJson(AssociatedType):
+ ...
+
+ class FromJson(AssociatedType):
+ ...
+
+ class Other(AssociatedType):
+ ...
+
+ @typeclass(ToJson)
+ def to_json(instance) -> str:
+ ...
+
+ @typeclass(FromJson)
+ def from_json(instance) -> str:
+ ...
+
+ @typeclass(Other)
+ def other(instance) -> str:
+ ...
+
+ @to_json.instance(str)
+ def _to_json_str(instance: str) -> str:
+ ...
+
+ @from_json.instance(str)
+ def _from_json_str(instance: str) -> str:
+ ...
+
+ @other.instance(str)
+ def _other_json_str(instance: str) -> str:
+ ...
+
+ def func(instance: Supports[ToJson, FromJson]) -> Supports[ToJson, FromJson]:
+ return from_json(to_json(instance))
+
+ func('abc')
+
+
+- case: supports_multiple_error_handling_in_mro
+ disable_cache: false
+ main: |
+ from classes import AssociatedType, Supports, typeclass
+
+ class ToJson(AssociatedType):
+ ...
+
+ class FromJson(AssociatedType):
+ ...
+
+ @typeclass(ToJson)
+ def to_json(instance) -> str:
+ ...
+
+ @typeclass(FromJson)
+ def from_json(instance) -> str:
+ ...
+
+ @to_json.instance(str)
+ def _to_json_str(instance: str) -> str:
+ ...
+
+ @from_json.instance(str)
+ def _from_json_str(instance: str, other) -> str: # error
+ ...
+
+ a: Supports[ToJson] = 'a'
+ b: Supports[FromJson] = 'a' # error
+ out: |
+ main:21: error: Instance callback is incompatible "def (instance: builtins.str, other: Any) -> builtins.str"; expected "def (instance: builtins.str) -> builtins.str"
+ main:26: error: Incompatible types in assignment (expression has type "str", variable has type "Supports[FromJson]")
diff --git a/typesafety/test_typeclass/test__call__.yml b/typesafety/test_typeclass/test__call__.yml
new file mode 100644
index 0000000..e46c134
--- /dev/null
+++ b/typesafety/test_typeclass/test__call__.yml
@@ -0,0 +1,241 @@
+- case: typeclass_call_all_arg_types
+ disable_cache: false
+ skip: sys.version_info[:2] < (3, 8)
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def args(
+ instance, /, regular, default=1, *args, kw, kw_default=2, **kwargs,
+ ) -> str:
+ ...
+
+ @args.instance(int)
+ def _args_int(
+ instance: int, /, regular, default=1, *args, kw, kw_default=2, **kwargs,
+ ) -> str:
+ ...
+
+ args(1, 2, 3, 4, 5, kw=6, kw_default=7, other=8)
+ args(1, 2, kw=6)
+ args(1, regular=2, kw=6)
+
+
+- case: typeclass_call_instance_named
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def args(instance) -> str:
+ ...
+
+ @args.instance(int)
+ def _args_int(instance: int) -> str:
+ ...
+
+ args(instance=1)
+
+
+- case: typeclass_call_instance_variance
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ class A:
+ ...
+
+ class B(A):
+ ...
+
+ class C(B):
+ ...
+
+ @typeclass
+ def some(instance) -> str:
+ ...
+
+ @some.instance(B)
+ def _some_b(instance: B) -> str:
+ ...
+
+ some(A())
+ some(B()) # ok
+ some(C()) # ok
+ out: |
+ main:20: error: Argument 1 to "some" has incompatible type "A"; expected "B"
+
+
+- case: typeclass_call_generic_instance_variance
+ disable_cache: false
+ main: |
+ from classes import typeclass
+ from typing import TypeVar, Generic
+
+ X = TypeVar('X')
+
+ class A(Generic[X]):
+ ...
+
+ class B(A[X]):
+ ...
+
+ class C(B[X]):
+ ...
+
+ @typeclass
+ def some(instance: B[X]) -> X:
+ ...
+
+ @some.instance(B)
+ def _some_b(instance: B[X]) -> X:
+ ...
+
+ a: A[int]
+ b: B[int]
+ c: C[int]
+ some(a)
+ some(b) # ok
+ some(c) # ok
+ out: |
+ main:26: error: Argument 1 to "some" has incompatible type "A[int]"; expected "B[]"
+
+
+- case: typeclass_call_variance_union1
+ disable_cache: false
+ main: |
+ from classes import typeclass
+ from typing import Union
+
+ class A:
+ ...
+
+ class B(A):
+ ...
+
+ class C(B):
+ ...
+
+ @typeclass
+ def some(instance) -> str:
+ ...
+
+ @some.instance(B)
+ @some.instance(C)
+ def _some_b(instance: Union[B, C]) -> str:
+ ...
+
+ some(A())
+ some(B()) # ok
+ some(C()) # ok
+ out: |
+ main:22: error: Argument 1 to "some" has incompatible type "A"; expected "B"
+
+
+- case: typeclass_call_variance_union2
+ disable_cache: false
+ main: |
+ from classes import typeclass
+ from typing import Union
+
+ class A:
+ ...
+
+ class B(A):
+ ...
+
+ class C(B):
+ ...
+
+ @typeclass
+ def some(instance) -> str:
+ ...
+
+ @some.instance(A)
+ @some.instance(B)
+ def _some_b(instance: Union[A, B]) -> str:
+ ...
+
+ some(A()) # ok
+ some(B()) # ok
+ some(C()) # ok
+
+
+- case: typeclass_call_variance_union3
+ disable_cache: false
+ main: |
+ from classes import typeclass
+ from typing import Union
+
+ class A:
+ ...
+
+ class B(A):
+ ...
+
+ class C(B):
+ ...
+
+ @typeclass
+ def some(instance) -> str:
+ ...
+
+ @some.instance(A)
+ @some.instance(C)
+ def _some_b(instance: Union[A, C]) -> str:
+ ...
+
+ some(A()) # ok
+ some(B()) # ok
+ some(C()) # ok
+
+
+- case: typeclass_call_variance_union4
+ disable_cache: false
+ main: |
+ from classes import typeclass
+ from typing import Union
+
+ class A:
+ ...
+
+ class B(A):
+ ...
+
+ class C(B):
+ ...
+
+ @typeclass
+ def some(instance) -> str:
+ ...
+
+ @some.instance(A)
+ @some.instance(B)
+ @some.instance(C)
+ def _some_b(instance: Union[A, B, C]) -> str:
+ ...
+
+ some(A()) # ok
+ some(B()) # ok
+ some(C()) # ok
+
+ ab: Union[A, B]
+ ac: Union[A, C]
+ bc: Union[B, C]
+ abc: Union[A, B, C]
+ some(ab) # ok
+ some(ac) # ok
+ some(bc) # ok
+ some(abc) # ok
+
+
+- case: typeclass_call_zero_args_regression270
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def some(instance) -> int:
+ ...
+
+ some() # E: Missing positional argument "instance" in call to "__call__" of "_TypeClass"
diff --git a/typesafety/test_typeclass/test_callback.yml b/typesafety/test_typeclass/test_callback.yml
index 41c28a7..6fb9eab 100644
--- a/typesafety/test_typeclass/test_callback.yml
+++ b/typesafety/test_typeclass/test_callback.yml
@@ -1,7 +1,7 @@
- case: typeclass_callback_correct
- disable_cache: true
+ disable_cache: false
main: |
- from typing import Callable
+ from typing import Callable, Union
from classes import typeclass
@typeclass
@@ -10,20 +10,19 @@
@example.instance(int)
@example.instance(float)
- def _example_int_float(intance, attr: bool) -> bool:
+ def _example_int_float(instance: Union[int, float], attr: bool) -> bool:
...
-
def accepts_typeclass(callback: Callable[[int, bool], bool]) -> bool:
return callback(1, True)
- reveal_type(accepts_typeclass(example)) # N: Revealed type is 'builtins.bool'
+ reveal_type(accepts_typeclass(example)) # N: Revealed type is "builtins.bool"
- case: typeclass_callback_wrong
- disable_cache: true
+ disable_cache: false
main: |
- from typing import Callable
+ from typing import Callable, Union
from classes import typeclass
@typeclass
@@ -32,15 +31,13 @@
@example.instance(int)
@example.instance(float)
- def _example_int_float(intance, attr: bool) -> bool:
+ def _example_int_float(instance: Union[int, float], attr: bool) -> bool:
...
-
def accepts_typeclass(callback: Callable[[str, bool], bool]) -> bool:
return callback('a', True)
- reveal_type(accepts_typeclass(example))
+ accepts_typeclass(example)
out: |
- main:17: error: Argument 1 to "accepts_typeclass" has incompatible type "_TypeClass[Union[int, float], bool, Callable[[int, bool], bool]]"; expected "Callable[[str, bool], bool]"
- main:17: note: "_TypeClass[Union[int, float], bool, Callable[[int, bool], bool]].__call__" has type "Callable[[Arg(Union[int, float], 'instance'), VarArg(Any), KwArg(Any)], bool]"
- main:17: note: Revealed type is 'builtins.bool'
+ main:16: error: Argument 1 to "accepts_typeclass" has incompatible type "_TypeClass[float, Callable[[Any, bool], bool], , Literal['main.example']]"; expected "Callable[[str, bool], bool]"
+ main:16: note: "_TypeClass[float, Callable[[Any, bool], bool], , Literal['main.example']].__call__" has type "Callable[[Arg(Union[float, Supports[]], 'instance'), VarArg(Any), KwArg(Any)], _ReturnType]"
diff --git a/typesafety/test_typeclass/test_generics/test_generics_concrete.yml b/typesafety/test_typeclass/test_generics/test_generics_concrete.yml
new file mode 100644
index 0000000..d7f1911
--- /dev/null
+++ b/typesafety/test_typeclass/test_generics/test_generics_concrete.yml
@@ -0,0 +1,276 @@
+- case: typeclass_concrete_generic
+ disable_cache: false
+ main: |
+ from typing import List
+ from classes import typeclass
+
+ class SomeDelegate(List[int]):
+ ...
+
+ @typeclass
+ def some(instance) -> int:
+ ...
+
+ @some.instance(delegate=SomeDelegate)
+ def _some_list_int(instance: List[int]) -> int:
+ ...
+
+ some([1, 2, 3])
+ some([])
+ some(['a']) # E: List item 0 has incompatible type "str"; expected "int"
+
+
+- case: typeclass_concrete_generic_annotated_as_delegate
+ disable_cache: false
+ main: |
+ from typing import List
+ from classes import typeclass
+
+ class SomeDelegate(List[int]):
+ ...
+
+ @typeclass
+ def some(instance) -> int:
+ ...
+
+ @some.instance(delegate=SomeDelegate)
+ def _some_list_int(instance: SomeDelegate) -> int:
+ ...
+
+ a: SomeDelegate
+ some(a)
+ some([1, 2, 3])
+ some([])
+ some(['a'])
+ out: |
+ main:17: error: Argument 1 to "some" has incompatible type "List[int]"; expected "SomeDelegate"
+ main:18: error: Argument 1 to "some" has incompatible type "List[]"; expected "SomeDelegate"
+ main:19: error: Argument 1 to "some" has incompatible type "List[str]"; expected "SomeDelegate"
+
+
+- case: typeclass_delegate_not_subtype_correct1
+ disable_cache: false
+ main: |
+ from typing import List
+ from classes import typeclass
+
+ class SomeDelegate(object):
+ ...
+
+ @typeclass
+ def some(instance) -> int:
+ ...
+
+ @some.instance(delegate=SomeDelegate)
+ def _some_list_int(instance: List[int]) -> int:
+ ...
+ out: |
+ main:11: error: Instance "builtins.list[builtins.int]" does not match inferred type "main.SomeDelegate"
+
+
+- case: typeclass_delegate_not_subtype_correct2
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ class SomeDelegate(object):
+ ...
+
+ @typeclass
+ def some(instance) -> int:
+ ...
+
+ @some.instance(delegate=SomeDelegate)
+ def _some_int(instance: int) -> int:
+ ...
+ out: |
+ main:10: error: Instance "builtins.int" does not match inferred type "main.SomeDelegate"
+
+
+- case: typeclass_concrete_generic_supports_delegate
+ disable_cache: false
+ main: |
+ from classes import typeclass, Supports, AssociatedType
+ from typing import List
+
+ class ListOfIntMeta(type):
+ def __instancecheck__(cls, arg) -> bool:
+ return (
+ isinstance(arg, list) and
+ bool(arg) and
+ all(isinstance(list_item, int) for list_item in arg)
+ )
+
+ class ListOfInt(List[int], metaclass=ListOfIntMeta):
+ ...
+
+ class A(AssociatedType):
+ ...
+
+ @typeclass(A)
+ def sum_all(instance) -> int:
+ ...
+
+ @sum_all.instance(delegate=ListOfInt)
+ def _sum_all_list_int(instance: ListOfInt) -> int:
+ return sum(instance)
+
+ def test(a: Supports[A]):
+ ...
+
+ a: ListOfInt
+ b: List[int]
+ c: List[str]
+ test(a)
+ test(b)
+ test(c)
+ out: |
+ main:33: error: Argument 1 to "test" has incompatible type "List[int]"; expected "Supports[A]"
+ main:34: error: Argument 1 to "test" has incompatible type "List[str]"; expected "Supports[A]"
+
+
+- case: typeclass_concrete_generic_supports_instance
+ disable_cache: false
+ main: |
+ from classes import typeclass, Supports, AssociatedType
+ from typing import List
+
+ class ListOfIntMeta(type):
+ def __instancecheck__(cls, arg) -> bool:
+ return (
+ isinstance(arg, list) and
+ bool(arg) and
+ all(isinstance(list_item, int) for list_item in arg)
+ )
+
+ class ListOfInt(List[int], metaclass=ListOfIntMeta):
+ ...
+
+ class A(AssociatedType):
+ ...
+
+ @typeclass(A)
+ def sum_all(instance) -> int:
+ ...
+
+ @sum_all.instance(delegate=ListOfInt)
+ def _sum_all_list_int(instance: List[int]) -> int:
+ return sum(instance)
+
+ def test(a: Supports[A]):
+ ...
+
+ a: ListOfInt
+ b: List[int]
+ c: List[str]
+ test(a)
+ test(b)
+ test(c)
+ out: |
+ main:33: error: Argument 1 to "test" has incompatible type "List[int]"; expected "Supports[A]"
+ main:34: error: Argument 1 to "test" has incompatible type "List[str]"; expected "Supports[A]"
+
+
+- case: typeclass_concrete_generic_delegate_and_protocol
+ disable_cache: false
+ main: |
+ from typing import List
+ from classes import typeclass
+
+ class SomeDelegate(object):
+ ...
+
+ @typeclass
+ def some(instance) -> int:
+ ...
+
+ @some.instance(List[int], delegate=SomeDelegate, protocol=str)
+ def _some_list_int(instance: List[int]) -> int:
+ ...
+ out: |
+ main:11: error: Instance "builtins.list[builtins.int]" does not match inferred type "main.SomeDelegate"
+ main:11: error: Regular type "builtins.str*" passed as a protocol
+ main:11: error: Only a single argument can be applied to `.instance`
+
+
+- case: typeclass_concrete_generic_delegate_and_tuple1
+ disable_cache: false
+ main: |
+ from typing import Tuple
+ from classes import typeclass
+
+ class UserTupleMeta(type):
+ def __instancecheck__(cls, arg: object) -> bool:
+ try:
+ return (
+ isinstance(arg, tuple) and
+ isinstance(arg[0], str) and
+ isinstance(arg[1], bool)
+ )
+ except IndexError:
+ return False
+
+ class UserTuple(Tuple[str, bool], metaclass=UserTupleMeta):
+ ...
+
+ @typeclass
+ def get_name(instance) -> str:
+ ...
+
+ @get_name.instance(delegate=UserTuple)
+ def _get_name_user_dict(instance: Tuple[str, bool]) -> str:
+ return instance[0]
+
+
+ get_name(('a', True)) # ok
+
+ get_name(())
+ get_name(('a', 'b', 'c'))
+ get_name(('a', 1))
+ out: |
+ main:29: error: Argument 1 to "get_name" has incompatible type "Tuple[]"; expected "Tuple[str, bool]"
+ main:30: error: Argument 1 to "get_name" has incompatible type "Tuple[str, str, str]"; expected "Tuple[str, bool]"
+ main:31: error: Argument 1 to "get_name" has incompatible type "Tuple[str, int]"; expected "Tuple[str, bool]"
+
+
+- case: typeclass_concrete_generic_delegate_and_tuple2
+ disable_cache: false
+ main: |
+ from typing import Tuple
+ from classes import typeclass
+
+ class UserTupleMeta(type):
+ def __instancecheck__(cls, arg: object) -> bool:
+ try:
+ return (
+ isinstance(arg, tuple) and
+ isinstance(arg[0], str) and
+ isinstance(arg[1], bool)
+ )
+ except IndexError:
+ return False
+
+ class UserTuple(Tuple[str, bool], metaclass=UserTupleMeta):
+ ...
+
+ @typeclass
+ def get_name(instance) -> str:
+ ...
+
+ @get_name.instance(delegate=UserTuple)
+ def _get_name_user_dict(instance: UserTuple) -> str:
+ return instance[0]
+
+
+ a = UserTuple(('a', True))
+ get_name(a)
+
+ get_name(())
+ get_name(('a', 'b'))
+ get_name(('a', True))
+ get_name(('a', 'b', 'c'))
+ out: |
+ main:30: error: Argument 1 to "get_name" has incompatible type "Tuple[]"; expected "UserTuple"
+ main:31: error: Argument 1 to "get_name" has incompatible type "Tuple[str, str]"; expected "UserTuple"
+ main:32: error: Argument 1 to "get_name" has incompatible type "Tuple[str, bool]"; expected "UserTuple"
+ main:33: error: Argument 1 to "get_name" has incompatible type "Tuple[str, str, str]"; expected "UserTuple"
diff --git a/typesafety/test_typeclass/test_generics/test_generics_regular.yml b/typesafety/test_typeclass/test_generics/test_generics_regular.yml
new file mode 100644
index 0000000..6127579
--- /dev/null
+++ b/typesafety/test_typeclass/test_generics/test_generics_regular.yml
@@ -0,0 +1,301 @@
+- case: typeclass_generic_definition_free_typevar
+ disable_cache: false
+ main: |
+ from typing import List, TypeVar
+ from classes import typeclass
+
+ X = TypeVar('X')
+
+ @typeclass
+ def some(instance, b: int) -> X:
+ ...
+
+ @some.instance(list)
+ def _some_ex(instance: List[X], b: int) -> X:
+ return instance[b] # We need this line to test inner inference
+
+ reveal_type(some(['a', 'b'], 0)) # N: Revealed type is "builtins.str*"
+ reveal_type(some([1, 2, 3], 0)) # N: Revealed type is "builtins.int*"
+
+
+- case: typeclass_generic_definition_restriction_correct
+ disable_cache: false
+ main: |
+ from typing import Iterable, List, TypeVar, Tuple
+ from classes import typeclass
+
+ X = TypeVar('X')
+
+ @typeclass
+ def some(instance: Iterable[X], b: int) -> X:
+ ...
+
+ @some.instance(list)
+ def _some_list(instance: List[X], b: int) -> X:
+ return instance[b] # We need this line to test inner inference
+
+ @some.instance(tuple)
+ def _some_tuple(instance: Tuple[X, ...], b: int) -> X:
+ return instance[b] # We need this line to test inner inference
+
+ reveal_type(some(['a', 'b'], 0)) # N: Revealed type is "builtins.str*"
+ reveal_type(some([1, 2, 3], 0)) # N: Revealed type is "builtins.int*"
+
+ reveal_type(some((1, 2, 3), 0)) # N: Revealed type is "builtins.int*"
+ reveal_type(some((1, 2), 0)) # N: Revealed type is "builtins.int*"
+
+
+- case: typeclass_generic_definition_restriction_wrong
+ disable_cache: false
+ main: |
+ from typing import Generic, List, TypeVar
+ from classes import typeclass
+
+ X = TypeVar('X')
+
+ class Some(Generic[X]):
+ ...
+
+ @typeclass
+ def some(instance: Some[X], b: int) -> X:
+ ...
+
+ @some.instance(list)
+ def _some_ex(instance: List[X], b: int) -> X:
+ return instance[b] # We need this line to test inner inference
+ out: |
+ main:13: error: Instance "builtins.list[X`-1]" does not match original type "main.Some[X`-1]"
+
+
+- case: typeclass_generic_definition_any1
+ disable_cache: false
+ mypy_config: |
+ disallow_any_explicit = false
+ disallow_any_generics = false
+ main: |
+ from typing import Iterable, List, TypeVar, Any
+ from classes import typeclass
+
+ X = TypeVar('X')
+
+ @typeclass
+ def some(instance, b: int) -> int:
+ ...
+
+ @some.instance(list)
+ def _some_ex(instance: List[Any], b: int) -> int:
+ ...
+
+
+- case: typeclass_generic_definition_any2
+ disable_cache: false
+ mypy_config: |
+ disallow_any_explicit = false
+ disallow_any_generics = false
+ main: |
+ from typing import Iterable, List, TypeVar, Any
+ from classes import typeclass
+
+ X = TypeVar('X')
+
+ @typeclass
+ def some(instance, b: int) -> int:
+ ...
+
+ @some.instance(List[Any])
+ def _some_ex(instance: list, b: int) -> int:
+ ...
+ out: |
+ main:10: error: Instance "builtins.list[Any]" has concrete generic type, it is not supported during runtime
+
+
+- case: typeclass_generic_definition_unbound
+ disable_cache: false
+ main: |
+ from typing import Iterable, List, TypeVar
+ from classes import typeclass
+
+ X = TypeVar('X')
+
+ @typeclass
+ def some(instance, b: int) -> X:
+ ...
+
+ @some.instance(List[X])
+ def _some_ex(instance: List[X], b: int) -> X:
+ return instance[b] # We need this line to test inner inference
+ out: |
+ main:10: error: Runtime type "builtins.list[X?]" has unbound type, use implicit any
+
+
+- case: typeclass_generic_definition_concrete1
+ disable_cache: false
+ main: |
+ from typing import Iterable, List, TypeVar
+ from classes import typeclass
+
+ X = TypeVar('X')
+
+ @typeclass
+ def some(instance, b: int) -> X:
+ ...
+
+ @some.instance(list)
+ def _some_ex(instance: List[int], b: int) -> X:
+ return instance[b] # We need this line to test inner inference
+ out: |
+ main:10: error: Instance "builtins.list[builtins.int]" has concrete generic type, it is not supported during runtime
+ main:12: error: Incompatible return value type (got "int", expected "X")
+
+
+- case: typeclass_generic_definition_concrete2
+ disable_cache: false
+ main: |
+ from typing import Iterable, List, TypeVar
+ from classes import typeclass
+
+ X = TypeVar('X')
+
+ @typeclass
+ def some(instance, b: int) -> X:
+ ...
+
+ @some.instance(List[int])
+ def _some_ex(instance: List[X], b: int) -> X:
+ return instance[b] # We need this line to test inner inference
+ out: |
+ main:10: error: Instance "builtins.list[builtins.int*]" has concrete generic type, it is not supported during runtime
+
+
+- case: typeclass_generic_call_union1
+ disable_cache: false
+ main: |
+ from typing import Union, Iterable, List, Set, TypeVar
+ from classes import typeclass
+
+ X = TypeVar('X')
+
+ @typeclass
+ def copy(instance: Iterable[X]) -> X:
+ ...
+
+ @copy.instance(list)
+ def _copy_int(instance: List[X]) -> X:
+ ...
+
+ @copy.instance(set)
+ def _copy_str(instance: Set[X]) -> X:
+ ...
+
+ a: Union[Set[str], List[str]]
+
+ reveal_type(copy(a))
+ out: |
+ main:20: note: Revealed type is "builtins.str*"
+
+
+- case: typeclass_generic_call_union2
+ disable_cache: false
+ main: |
+ from typing import TypeVar, Union, Iterable, List, Set
+ from classes import typeclass, AssociatedType, Supports
+
+ X = TypeVar('X')
+
+ class Copy(AssociatedType[X]):
+ ...
+
+ @typeclass(Copy)
+ def copy(instance: Iterable[X]) -> X:
+ ...
+
+ @copy.instance(list)
+ def _copy_int(instance: List[X]) -> X:
+ ...
+
+ @copy.instance(set)
+ def _copy_str(instance: Set[X]) -> X:
+ ...
+
+ a: Union[Set[str], List[str]]
+
+ reveal_type(copy(a))
+ out: |
+ main:23: note: Revealed type is "builtins.str*"
+
+
+- case: typeclass_generic_call_union3
+ disable_cache: false
+ main: |
+ from typing import TypeVar, Union, Iterable, List, Set
+ from classes import typeclass, AssociatedType, Supports
+
+ X = TypeVar('X')
+
+ class Copy(AssociatedType): # NOTE: no typevar
+ ...
+
+ @typeclass(Copy)
+ def copy(instance: Iterable[X]) -> X:
+ ...
+
+ @copy.instance(list)
+ def _copy_int(instance: List[X]) -> X:
+ ...
+
+ @copy.instance(set)
+ def _copy_str(instance: Set[X]) -> X:
+ ...
+
+ a: Union[Set[str], List[str]]
+
+ reveal_type(copy(a))
+ out: |
+ main:9: error: Generic type "main.Copy" with "0" type arguments does not match generic instance declaration "typing.Iterable[X`-1]" with "1" type arguments
+ main:23: note: Revealed type is ""
+
+
+- case: typeclass_generic_tuple
+ disable_cache: false
+ main: |
+ from typing import Iterable, Tuple, TypeVar
+ from classes import typeclass
+
+ X = TypeVar('X')
+
+ @typeclass
+ def some(instance: Iterable[X]) -> X:
+ ...
+
+ @some.instance(tuple)
+ def _some_tuple(instance: Tuple[X, X]) -> X:
+ ...
+ out: |
+ main:10: error: Instance "Tuple[X`-1, X`-1]" has concrete generic type, it is not supported during runtime
+
+
+- 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:
+ ...
diff --git a/typesafety/test_typeclass/test_generics/test_generics_typevar.yml b/typesafety/test_typeclass/test_generics/test_generics_typevar.yml
new file mode 100644
index 0000000..520fe67
--- /dev/null
+++ b/typesafety/test_typeclass/test_generics/test_generics_typevar.yml
@@ -0,0 +1,208 @@
+- case: typeclass_generic_definition1
+ disable_cache: false
+ main: |
+ from typing import TypeVar
+ from classes import typeclass
+
+ X = TypeVar('X')
+
+ @typeclass
+ def copy(instance: X) -> X:
+ ...
+
+ @copy.instance(int)
+ def _copy_ex(instance: int) -> int:
+ return instance + 0
+
+ reveal_type(copy(0))
+ reveal_type(copy(1.5))
+ out: |
+ main:14: note: Revealed type is "builtins.int*"
+ main:15: error: Argument 1 to "copy" has incompatible type "builtins.float"; expected "builtins.int"
+ main:15: note: Revealed type is "builtins.float*"
+
+
+- case: typeclass_generic_definition2
+ disable_cache: false
+ main: |
+ from typing import TypeVar
+ from classes import typeclass, AssociatedType, Supports
+
+ X = TypeVar('X')
+
+ class Copy(AssociatedType):
+ ...
+
+ @typeclass(Copy)
+ def copy(instance: X) -> X:
+ ...
+
+ @copy.instance(int)
+ def _copy_int(instance: int) -> int:
+ return instance + 0
+
+ @copy.instance(str)
+ def _copy_str(instance: str) -> str:
+ return instance + ''
+
+ a: Supports[Copy]
+ reveal_type(copy(0))
+ reveal_type(copy(a))
+ reveal_type(copy('a'))
+ out: |
+ main:22: note: Revealed type is "builtins.int*"
+ main:23: note: Revealed type is "classes._typeclass.Supports*[main.Copy]"
+ main:24: note: Revealed type is "builtins.str*"
+
+
+- case: typeclass_generic_definition3
+ disable_cache: false
+ main: |
+ from typing import TypeVar, Union
+ from classes import typeclass
+
+ X = TypeVar('X')
+ Y = TypeVar('Y')
+
+ @typeclass
+ def compare(instance: X, other: Y) -> Union[X, Y]:
+ ...
+
+ @compare.instance(int)
+ def _compare_ex(instance: int, other: Y) -> Union[int, Y]:
+ ...
+
+ reveal_type(compare(0, 'a'))
+ reveal_type(compare('a', 1))
+ out: |
+ main:15: note: Revealed type is "Union[builtins.int*, builtins.str*]"
+ main:16: error: Argument 1 to "compare" has incompatible type "Literal['a']?"; expected "builtins.int"
+ main:16: note: Revealed type is "Union[builtins.str*, builtins.int*]"
+
+
+- case: typeclass_generic_definition4
+ disable_cache: false
+ main: |
+ from typing import TypeVar
+ from classes import typeclass
+
+ X = TypeVar('X')
+
+ @typeclass
+ def some(instance: X) -> X:
+ ...
+
+ @some.instance(int)
+ def _some_ex(instance: X) -> X:
+ ...
+ out: |
+ main:10: error: Instance "X`-1" does not match inferred type "builtins.int*"
+
+
+- case: typeclass_generic_definition_bound
+ disable_cache: false
+ main: |
+ from typing import TypeVar
+ from classes import typeclass
+
+ X = TypeVar('X', bound='A')
+
+ class A(object):
+ ...
+
+ class B(A):
+ ...
+
+ @typeclass
+ def some(instance: X) -> X:
+ ...
+
+ @some.instance(B)
+ def _some_ex(instance: B) -> B:
+ ...
+
+ reveal_type(some(B()))
+ some(A())
+ out: |
+ main:20: note: Revealed type is "main.B*"
+ main:21: error: Argument 1 to "some" has incompatible type "main.A"; expected "main.B"
+
+
+- case: typeclass_generic_definition_values
+ disable_cache: false
+ main: |
+ from typing import TypeVar
+ from classes import typeclass
+
+ X = TypeVar('X', 'A', 'B')
+
+ class A(object):
+ ...
+
+ class B(A):
+ ...
+
+ @typeclass
+ def some(instance: X) -> X:
+ ...
+
+ @some.instance(B)
+ def _some_ex(instance: B) -> B:
+ ...
+
+ reveal_type(some(B()))
+ some(A())
+ out: |
+ main:20: note: Revealed type is "main.B*"
+ main:21: error: Argument 1 to "some" has incompatible type "main.A"; expected "main.B"
+
+
+- case: typeclass_generic_definition_two_args
+ disable_cache: false
+ main: |
+ from typing import TypeVar
+ from classes import typeclass
+
+ X = TypeVar('X')
+
+ @typeclass
+ def compare(instance: X, other: X) -> X:
+ ...
+
+ @compare.instance(int)
+ def _compare_ex(instance: int, other: int) -> int:
+ ...
+
+ reveal_type(compare(0, 1))
+ compare(1.5, 1)
+ compare(1.5, 1.6)
+ out: |
+ main:14: note: Revealed type is "builtins.int*"
+ main:15: error: Argument 1 to "compare" has incompatible type "builtins.float"; expected "builtins.int"
+ main:16: error: Argument 1 to "compare" has incompatible type "builtins.float"; expected "builtins.int"
+
+
+- case: typeclass_generic_definition_union_call
+ disable_cache: false
+ main: |
+ from typing import TypeVar, Union
+ from classes import typeclass
+
+ X = TypeVar('X')
+
+ @typeclass
+ def copy(instance: X) -> X:
+ ...
+
+ @copy.instance(int)
+ def _copy_int(instance: int) -> int:
+ return instance + 0
+
+ @copy.instance(str)
+ def _copy_str(instance: str) -> str:
+ return instance + ''
+
+ a: Union[str, int]
+ reveal_type(copy(a))
+ out: |
+ main:19: note: Revealed type is "Union[builtins.str, builtins.int]"
diff --git a/typesafety/test_typeclass/test_generics/test_typed_dict.yml b/typesafety/test_typeclass/test_generics/test_typed_dict.yml
new file mode 100644
index 0000000..663c3b6
--- /dev/null
+++ b/typesafety/test_typeclass/test_generics/test_typed_dict.yml
@@ -0,0 +1,121 @@
+- case: typeclass_typed_dict1
+ disable_cache: false
+ main: |
+ from classes import typeclass, AssociatedType, Supports
+ from typing_extensions import TypedDict
+
+ class User(TypedDict):
+ name: str
+ registered: bool
+
+ class UserDictMeta(type):
+ def __instancecheck__(cls, arg: object) -> bool:
+ return (
+ isinstance(arg, dict) and
+ isinstance(arg.get('name'), str) and
+ isinstance(arg.get('registered'), bool)
+ )
+
+ UserMeta = type('UserMeta', (UserDictMeta, type(TypedDict)), {})
+
+ class UserDict(User, metaclass=UserMeta):
+ ...
+
+ class GetName(AssociatedType):
+ ...
+
+ @typeclass(GetName)
+ def get_name(instance) -> str:
+ ...
+
+ @get_name.instance(delegate=UserDict)
+ def _get_name_user_dict(instance: UserDict) -> str:
+ return instance['name']
+
+ def callback(instance: Supports[GetName]) -> str:
+ return get_name(instance)
+
+ a: UserDict = {'name': 'sobolevn', 'registered': True}
+ b: User = {'name': 'sobolevn', 'registered': True}
+ c = {'name': 'sobolevn', 'registered': True}
+
+ callback(a) # ok
+ callback(b)
+ callback(c)
+ callback({})
+ out: |
+ main:40: error: Argument 1 to "callback" has incompatible type "User"; expected "Supports[GetName]"
+ main:41: error: Argument 1 to "callback" has incompatible type "Dict[str, object]"; expected "Supports[GetName]"
+ main:42: error: Argument 1 to "callback" has incompatible type "Dict[, ]"; expected "Supports[GetName]"
+
+
+- case: typeclass_typed_dict2
+ disable_cache: false
+ main: |
+ from classes import typeclass
+ from typing_extensions import TypedDict
+
+ class User(TypedDict):
+ name: str
+ registered: bool
+
+ class UserDictMeta(type):
+ def __instancecheck__(cls, arg: object) -> bool:
+ return (
+ isinstance(arg, dict) and
+ isinstance(arg.get('name'), str) and
+ isinstance(arg.get('registered'), bool)
+ )
+
+ UserMeta = type('UserMeta', (UserDictMeta, type(TypedDict)), {})
+
+ class UserDict(User, metaclass=UserMeta):
+ ...
+
+ @typeclass
+ def get_name(instance) -> str:
+ ...
+
+ @get_name.instance(delegate=UserDict)
+ def _get_name_user_dict(instance: User) -> str:
+ return instance['name']
+ out: |
+ main:25: error: Instance "TypedDict('main.User', {'name': builtins.str, 'registered': builtins.bool})" does not match inferred type "main.UserDict"
+
+
+- case: typeclass_typed_dict3
+ disable_cache: false
+ main: |
+ from classes import typeclass
+ from typing_extensions import TypedDict
+
+ class User(TypedDict):
+ name: str
+ registered: bool
+
+ class Other(TypedDict): # even has the same structure
+ name: str
+ registered: bool
+
+ class UserDictMeta(type):
+ def __instancecheck__(cls, arg: object) -> bool:
+ return (
+ isinstance(arg, dict) and
+ isinstance(arg.get('name'), str) and
+ isinstance(arg.get('registered'), bool)
+ )
+
+ UserMeta = type('UserMeta', (UserDictMeta, type(TypedDict)), {})
+
+ class UserDict(User, metaclass=UserMeta):
+ ...
+
+ @typeclass
+ def get_name(instance) -> str:
+ ...
+
+ @get_name.instance(delegate=UserDict)
+ def _get_name_user_dict(instance: Other) -> str:
+ return instance['name']
+ out: |
+ main:29: error: Instance "TypedDict('main.Other', {'name': builtins.str, 'registered': builtins.bool})" does not match inferred type "main.UserDict"
diff --git a/typesafety/test_typeclass/test_instance.yml b/typesafety/test_typeclass/test_instance.yml
deleted file mode 100644
index d15173f..0000000
--- a/typesafety/test_typeclass/test_instance.yml
+++ /dev/null
@@ -1,81 +0,0 @@
-- case: typeclass_instances_union
- disable_cache: true
- main: |
- from typing import Union
- from classes import typeclass
-
- @typeclass
- def a(instance) -> str:
- ...
-
- @a.instance(str)
- @a.instance(int)
- def _a_int_str(instance: Union[str, int]) -> str:
- return str(instance)
-
- reveal_type(a) # N: Revealed type is 'classes.typeclass._TypeClass[Union[builtins.str*, builtins.int*], builtins.str, def (builtins.str*) -> builtins.str]'
-
-
-- case: typeclass_instance_any
- disable_cache: true
- main: |
- from classes import typeclass
-
- @typeclass
- def a(instance):
- ...
-
- @a.instance(str)
- def _a_int_str(instance: str) -> str:
- return str(instance)
-
- reveal_type(a) # N: Revealed type is 'classes.typeclass._TypeClass[builtins.str*, Any, def (builtins.str*) -> Any]'
-
-
-- case: typeclass_instance_missing_first_arg
- disable_cache: true
- main: |
- from classes import typeclass
-
- @typeclass
- def a(instance):
- ...
-
- @a.instance
- def some():
- ...
-
- out: |
- main:7: error: No overload variant of "instance" of "_TypeClass" matches argument type "Callable[[], Any]"
- main:7: note: <1 more non-matching overload not shown>
- main:7: note: def [_InstanceType] instance(self, type_argument: Type[_InstanceType], *, is_protocol: Literal[False] = ...) -> Callable[[Callable[[_InstanceType], Any]], NoReturn]
- main:7: note: Possible overload variant:
-
-
-- case: typeclass_wrong_param
- disable_cache: true
- main: |
- from classes import typeclass
-
- typeclass(1)
-
- out: |
- main:3: error: Value of type variable "_CallbackType" of "typeclass" cannot be "int"
-
-
-- case: typeclass_instance_wrong_param
- disable_cache: true
- main: |
- from classes import typeclass
-
- @typeclass
- def a(instance):
- ...
-
- a.instance(1)
-
- out: |
- main:7: error: No overload variant of "instance" of "_TypeClass" matches argument type "int"
- main:7: note: <1 more non-matching overload not shown>
- main:7: note: def [_InstanceType] instance(self, type_argument: Type[_InstanceType], *, is_protocol: Literal[False] = ...) -> Callable[[Callable[[_InstanceType], Any]], NoReturn]
- main:7: note: Possible overload variant:
diff --git a/typesafety/test_typeclass/test_instance/test_instance_method.yml b/typesafety/test_typeclass/test_instance/test_instance_method.yml
new file mode 100644
index 0000000..459602d
--- /dev/null
+++ b/typesafety/test_typeclass/test_instance/test_instance_method.yml
@@ -0,0 +1,269 @@
+- case: typeclass_two_typeclasses_two_instances
+ disable_cache: false
+ main: |
+ from typing import Union
+ from classes import typeclass
+
+ @typeclass
+ def a(instance) -> str:
+ ...
+
+ @typeclass
+ def b(instance) -> str:
+ ...
+
+ @a.instance(str)
+ def _a_str(instance: str) -> str:
+ ...
+
+ @b.instance(int)
+ def _b_int(instance: int) -> str:
+ ...
+
+ a('a')
+ b(2)
+
+ a(1)
+ b('b')
+ out: |
+ main:23: error: Argument 1 to "a" has incompatible type "int"; expected "str"
+ main:24: error: Argument 1 to "b" has incompatible type "str"; expected "int"
+
+
+- case: typeclass_two_typeclasses_one_instance
+ disable_cache: false
+ main: |
+ from typing import Union
+ from classes import typeclass
+
+ @typeclass
+ def a(instance) -> str:
+ ...
+
+ @typeclass
+ def b(instance) -> str:
+ ...
+
+ @a.instance(str)
+ @b.instance(int)
+ def _a_int_str(instance: Union[str, int]) -> str:
+ return str(instance)
+
+ a('a')
+ b(2)
+ out: |
+ main:12: error: Found different typeclass ".instance" calls, use only "main.b"
+ main:12: error: Instance "Union[builtins.str, builtins.int]" does not match inferred type "builtins.int*"
+ main:17: error: Argument 1 to "a" has incompatible type "str"; expected
+ main:18: error: Argument 1 to "b" has incompatible type "int"; expected
+
+
+- case: typeclass_instances_union1
+ disable_cache: false
+ main: |
+ from typing import Union
+ from classes import typeclass
+
+ @typeclass
+ def a(instance) -> str:
+ ...
+
+ @a.instance(str)
+ @a.instance(int)
+ def _a_int_str(instance: Union[str, int]) -> str:
+ return str(instance)
+
+ a(1)
+ a('a')
+ a(None) # E: Argument 1 to "a" has incompatible type "None"; expected "Union[str, int]"
+
+
+- case: typeclass_instances_union2
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def a(instance) -> str:
+ ...
+
+ @a.instance(str)
+ @a.instance(int)
+ def _a_int_str(instance: int) -> str:
+ ...
+ out: |
+ main:7: error: Instance "builtins.int" does not match inferred type "Union[builtins.str*, builtins.int*]"
+
+
+- case: typeclass_instances_union3
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def a(instance) -> str:
+ ...
+
+ @a.instance(str)
+ @a.instance(int)
+ def _a_int_str(instance: str) -> str:
+ ...
+ out: |
+ main:7: error: Instance "builtins.str" does not match inferred type "Union[builtins.str*, builtins.int*]"
+
+
+- case: typeclass_instances_union4
+ disable_cache: false
+ main: |
+ from typing import Union
+ from classes import typeclass
+
+ @typeclass
+ def a(instance) -> str:
+ ...
+
+ @a.instance(str)
+ @a.instance(int)
+ def _a_int_str(instance: Union[str, int, None]) -> str:
+ ...
+ out: |
+ main:8: error: Instance "Union[builtins.str, builtins.int, None]" does not match inferred type "Union[builtins.str*, builtins.int*]"
+
+
+- case: typeclass_instance_mixed_order
+ disable_cache: False
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def some(instance) -> str:
+ ...
+
+ @some.instance(int)
+ def _some_str(instance: str) -> str:
+ ...
+
+ @some.instance(int)
+ def _some_int(instance: str) -> str:
+ ...
+ out: |
+ main:7: error: Instance "builtins.str" does not match inferred type "builtins.int*"
+ main:11: error: Instance "builtins.str" does not match inferred type "builtins.int*"
+
+
+- case: typeclass_instance_any1
+ disable_cache: false
+ mypy_config: |
+ disallow_any_explicit = false
+ disallow_any_generics = false
+ main: |
+ from typing import Any
+ from classes import typeclass
+
+ @typeclass
+ def a(instance) -> str:
+ ...
+
+ @a.instance(int)
+ def _a_int(instance: Any) -> str:
+ ...
+ out: |
+ main:8: error: Instance "Any" does not match inferred type "builtins.int*"
+
+
+- case: typeclass_instance_any2
+ disable_cache: false
+ mypy_config: |
+ disallow_any_explicit = false
+ disallow_any_generics = false
+ main: |
+ from typing import Any
+ from classes import typeclass
+
+ @typeclass
+ def a(instance) -> str:
+ ...
+
+ @a.instance(Any)
+ def _a_any(instance: Any) -> str:
+ ...
+ out: |
+ main:8: error: Instance "Any" does not match inferred type "builtins.object"
+ main:8: error: Argument 1 to "instance" of "_TypeClass" has incompatible type "object"; expected "None"
+
+
+- case: typeclass_instance_wrong_param
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def a(instance) -> str:
+ ...
+
+ a.instance(1) # E: Argument 1 to "instance" of "_TypeClass" has incompatible type "int"; expected "None"
+
+
+- case: typeclass_instance_callback_def
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def some(instance, b: int) -> int:
+ ...
+
+ def _some_str(instance: str, b: int) -> int:
+ ...
+ some.instance(str)(_some_str)
+
+ some('a', 1)
+ some(None, 1) # E: Argument 1 to "some" has incompatible type "None"; expected "str"
+
+
+- case: typeclass_instance_named_alias
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def some(instance, b: int) -> int:
+ ...
+
+ alias = some.instance(str) # E: Need type annotation for "alias"
+
+
+- case: typeclass_none_instance
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def a(instance) -> str:
+ ...
+
+ @a.instance(None)
+ def _a_none(instance: None) -> str:
+ ...
+
+ a(None)
+
+
+- case: typeclass_instance_override
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def example(instance) -> str:
+ ...
+
+ @example.instance(str)
+ def _example_str(instance: str) -> str:
+ return instance.lower()
+
+ @example.instance(str)
+ def _example_str_new(instance: str) -> str:
+ return _example_str(instance) + '!'
+
+ reveal_type(example('a')) # N: Revealed type is "builtins.str"
diff --git a/typesafety/test_typeclass/test_instance/test_instance_protocols.yml b/typesafety/test_typeclass/test_instance/test_instance_protocols.yml
new file mode 100644
index 0000000..e85c97e
--- /dev/null
+++ b/typesafety/test_typeclass/test_instance/test_instance_protocols.yml
@@ -0,0 +1,51 @@
+- case: typeclass_protocol_usage
+ disable_cache: false
+ main: |
+ from typing import Sized
+ from classes import typeclass
+
+ @typeclass
+ def protocols(instance, other: str) -> str:
+ ...
+
+ @protocols.instance(protocol=Sized)
+ def _sized_protocols(instance: Sized, other: str) -> str:
+ ...
+
+ protocols('abc', 'xyz')
+ protocols([1, 2, 3], 'xyz')
+ protocols(None, 'xyz') # E: Argument 1 to "protocols" has incompatible type "None"; expected "Sized"
+
+
+- case: typeclass_protocol_wrong_usage0
+ disable_cache: false
+ main: |
+ from typing import Sized
+ from classes import typeclass
+
+ @typeclass
+ def protocols(instance, other: str) -> str:
+ ...
+
+ @protocols.instance(Sized)
+ def _sized_protocols(instance: Sized, other: str) -> str:
+ ...
+ out: |
+ main:8: error: Protocol type "typing.Sized" is passed as a regular type
+
+
+- case: typeclass_protocol_wrong_usage1
+ disable_cache: false
+ main: |
+ from typing import Sized
+ from classes import typeclass
+
+ @typeclass
+ def protocols(instance, other: str) -> str:
+ ...
+
+ @protocols.instance(protocol=int)
+ def _sized_protocols(instance: int, other: str) -> str:
+ ...
+ out: |
+ main:8: error: Regular type "builtins.int*" passed as a protocol
diff --git a/typesafety/test_typeclass/test_instance/test_instance_variance.yml b/typesafety/test_typeclass/test_instance/test_instance_variance.yml
new file mode 100644
index 0000000..e2afa99
--- /dev/null
+++ b/typesafety/test_typeclass/test_instance/test_instance_variance.yml
@@ -0,0 +1,131 @@
+- case: typeclass_instance_arg_variance
+ disable_cache: False
+ main: |
+ from classes import typeclass
+
+ class A(object):
+ ...
+
+ class B(A):
+ ...
+
+ class C(B):
+ ...
+
+ @typeclass
+ def some(instance, arg: B) -> str:
+ ...
+
+ @some.instance(str)
+ def _some_str(instance: str, arg: A) -> str:
+ ...
+
+ @some.instance(bool)
+ def _some_bool(instance: bool, arg: B) -> str:
+ ...
+
+ @some.instance(int)
+ def _some_int(instance: int, arg: C) -> str:
+ ...
+ out: |
+ main:24: error: Instance callback is incompatible "def (instance: builtins.int, arg: main.C) -> builtins.str"; expected "def (instance: builtins.int, arg: main.B) -> builtins.str"
+
+
+- case: typeclass_instance_ret_type_variance
+ disable_cache: False
+ main: |
+ from classes import typeclass
+
+ class A(object):
+ ...
+
+ class B(A):
+ ...
+
+ class C(B):
+ ...
+
+ @typeclass
+ def some(instance) -> B:
+ ...
+
+ @some.instance(str)
+ def _some_str(instance: str) -> A:
+ ...
+
+ @some.instance(bool)
+ def _some_bool(instance: bool) -> B:
+ ...
+
+ @some.instance(int)
+ def _some_int(instance: int) -> C:
+ ...
+ out: |
+ main:16: error: Instance callback is incompatible "def (instance: builtins.str) -> main.A"; expected "def (instance: builtins.str) -> main.B"
+
+
+- case: typeclass_instance_self_variance
+ disable_cache: False
+ main: |
+ from classes import typeclass
+
+ class A(object):
+ ...
+
+ class B(A):
+ ...
+
+ class C(B):
+ ...
+
+ @typeclass
+ def some(instance: B):
+ ...
+
+ @some.instance(A)
+ def _some_a(instance: A):
+ ...
+
+ @some.instance(B)
+ def _some_b(instance: B):
+ ...
+
+ @some.instance(C)
+ def _some_c(instance: C):
+ ...
+ out: |
+ main:16: error: Instance "main.A" does not match original type "main.B"
+
+
+- case: typeclass_instance_runtime_variance
+ disable_cache: False
+ main: |
+ from classes import typeclass
+
+ class A(object):
+ ...
+
+ class B(A):
+ ...
+
+ class C(B):
+ ...
+
+ @typeclass
+ def some(instance) -> str:
+ ...
+
+ @some.instance(A)
+ def _some_a(instance: B) -> str:
+ ...
+
+ @some.instance(B)
+ def _some_b(instance: B) -> str:
+ ...
+
+ @some.instance(C)
+ def _some_c(instance: B) -> str:
+ ...
+ out: |
+ main:16: error: Instance "main.B" does not match inferred type "main.A"
+ main:24: error: Instance "main.B" does not match inferred type "main.C"
diff --git a/typesafety/test_typeclass/test_supports/test_supports_method.yml b/typesafety/test_typeclass/test_supports/test_supports_method.yml
new file mode 100644
index 0000000..985bcc5
--- /dev/null
+++ b/typesafety/test_typeclass/test_supports/test_supports_method.yml
@@ -0,0 +1,43 @@
+- case: typeclass_object_supports
+ disable_cache: false
+ main: |
+ from classes import typeclass, AssociatedType
+
+ class ToJson(AssociatedType):
+ ...
+
+ @typeclass(ToJson)
+ def to_json(instance) -> str:
+ ...
+
+ @to_json.instance(int)
+ def _to_json_int(instance: int) -> str:
+ ...
+
+ reveal_type(to_json.supports(1))
+ reveal_type(to_json.supports('a'))
+ reveal_type(to_json.supports(int))
+ out: |
+ main:14: note: Revealed type is "builtins.bool"
+ main:15: note: Revealed type is "builtins.bool"
+ main:16: note: Revealed type is "builtins.bool"
+
+
+- case: typeclass_function_supports
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def to_json(instance) -> str:
+ ...
+
+ @to_json.instance(int)
+ def _to_json_int(instance: int) -> str:
+ ...
+
+ reveal_type(to_json.supports(1))
+ reveal_type(to_json.supports('a'))
+ out: |
+ main:11: note: Revealed type is "builtins.bool"
+ main:12: note: Revealed type is "builtins.bool"
diff --git a/typesafety/test_typeclass/test_supports/test_typeguard.yml b/typesafety/test_typeclass/test_supports/test_typeguard.yml
new file mode 100644
index 0000000..d941a29
--- /dev/null
+++ b/typesafety/test_typeclass/test_supports/test_typeguard.yml
@@ -0,0 +1,120 @@
+- case: typeclass_function_supports_typeguard
+ disable_cache: false
+ main: |
+ from classes import typeclass
+ from typing import Union
+
+ @typeclass
+ def to_json(instance) -> str:
+ ...
+
+ @to_json.instance(int)
+ def _to_json_int(instance: int) -> str:
+ ...
+
+ item: Union[int, str]
+ to_json(item) # will fail
+
+ if to_json.supports(item):
+ reveal_type(item)
+ to_json(item) # ok
+ out: |
+ main:13: error: Argument 1 to "to_json" has incompatible type "Union[int, str]"; expected "int"
+ main:16: note: Revealed type is "builtins.int*"
+
+
+- case: typeclass_object_supports_typeguard
+ disable_cache: false
+ main: |
+ from classes import typeclass, AssociatedType
+ from typing import Union
+
+ class ToJson(AssociatedType):
+ ...
+
+ @typeclass(ToJson)
+ def to_json(instance) -> str:
+ ...
+
+ @to_json.instance(int)
+ def _to_json_int(instance: int) -> str:
+ ...
+
+ @to_json.instance(dict)
+ def _to_json_dict(instance: dict) -> str:
+ ...
+
+ item: Union[int, str]
+ to_json(item) # will fail
+
+ if to_json.supports(item):
+ reveal_type(item)
+ to_json(item) # ok
+ out: |
+ main:20: error: Argument 1 to "to_json" has incompatible type "Union[int, str]"; expected "Supports[ToJson]"
+ main:23: note: Revealed type is "Union[builtins.dict[Any, Any], builtins.int]"
+
+
+- case: typeclass_function_supports_typeguard_generic1
+ disable_cache: false
+ main: |
+ from classes import typeclass
+ from typing import Union, TypeVar
+
+ X = TypeVar('X')
+
+ @typeclass
+ def copy(instance: X) -> X:
+ ...
+
+ @copy.instance(int)
+ def _copy_int(instance: int) -> int:
+ ...
+
+ @copy.instance(dict)
+ def _copy_dict(instance: dict) -> dict:
+ ...
+
+ item: Union[int, str]
+ copy(item) # will fail
+
+ if copy.supports(item):
+ reveal_type(item)
+ copy(item) # ok
+ out: |
+ main:19: error: Argument 1 to "copy" has incompatible type "Union[builtins.int, builtins.str]"; expected "Union[builtins.dict[Any, Any], builtins.int]"
+ main:22: note: Revealed type is "Union[builtins.dict[Any, Any], builtins.int]"
+
+
+- case: typeclass_object_supports_typeguard_generic1
+ disable_cache: false
+ main: |
+ from classes import typeclass, AssociatedType
+ from typing import Union, TypeVar
+
+ X = TypeVar('X')
+
+ class Copy(AssociatedType):
+ ...
+
+ @typeclass(Copy)
+ def copy(instance: X) -> X:
+ ...
+
+ @copy.instance(int)
+ def _copy_int(instance: int) -> int:
+ ...
+
+ @copy.instance(dict)
+ def _copy_dict(instance: dict) -> dict:
+ ...
+
+ item: Union[int, str]
+ copy(item) # will fail
+
+ if copy.supports(item):
+ reveal_type(item)
+ copy(item) # ok
+ out: |
+ main:22: error: Argument 1 to "copy" has incompatible type "Union[builtins.int, builtins.str]"; expected "classes._typeclass.Supports[main.Copy]"
+ main:25: note: Revealed type is "Union[builtins.dict[Any, Any], builtins.int]"
diff --git a/typesafety/test_typeclass/test_typeclass.yml b/typesafety/test_typeclass/test_typeclass.yml
index c0cb2d9..f65a4e2 100644
--- a/typesafety/test_typeclass/test_typeclass.yml
+++ b/typesafety/test_typeclass/test_typeclass.yml
@@ -1,104 +1,88 @@
-- case: typeclass_definition
- disable_cache: true
+- case: typeclass_definition_any
+ disable_cache: false
main: |
from classes import typeclass
@typeclass
- def example(instance, arg: str, other: int, *, attr: bool) -> bool:
+ def example(instance):
...
- reveal_type(example) # N: Revealed type is 'classes.typeclass._TypeClass[Any, builtins.bool, def (instance: Any, arg: builtins.str, other: builtins.int, *, attr: builtins.bool) -> builtins.bool]'
-
-- case: typeclass_instance
- disable_cache: true
+- case: typeclass_definied_by_type
+ disable_cache: false
main: |
- from classes import typeclass
+ from classes import typeclass, AssociatedType
- @typeclass
- def example(instance, arg: str) -> bool:
+ class ToJson(AssociatedType):
...
- @example.instance(str)
- def _example_str(instance: str, arg: str) -> bool:
+ @typeclass(ToJson)
+ def to_json(instance, verbose: bool = False) -> str:
...
- reveal_type(example) # N: Revealed type is 'classes.typeclass._TypeClass[builtins.str*, builtins.bool, def (builtins.str*, arg: builtins.str) -> builtins.bool]'
-
-
-- case: typeclass_instance_union
- disable_cache: true
- main: |
- from classes import typeclass
+ @to_json.instance(int)
+ def _to_json_int(instance: int, verbose: bool = False) -> str:
+ return str(instance)
- @typeclass
- def example(instance, arg: str) -> bool:
- ...
-
- @example.instance(int)
- @example.instance(float)
- def _example_int_float(instance, arg: str) -> bool:
- ...
-
- @example.instance(str)
- def _example_str(instance: str, arg: str) -> bool:
- ...
+ @to_json.instance(str)
+ def _to_json_str(instance: str, verbose: bool = False) -> str:
+ return instance
- reveal_type(example) # N: Revealed type is 'classes.typeclass._TypeClass[Union[builtins.str*, builtins.int*, builtins.float*], builtins.bool, def (builtins.str*, arg: builtins.str) -> builtins.bool]'
+ to_json(1, verbose=True)
+ to_json('a')
+ to_json(None)
+ out: |
+ main:20: error: Argument 1 to "to_json" has incompatible type "None"; expected "Supports[ToJson]"
-- case: typeclass_incorrect_instance_callback1
- disable_cache: true
+- case: typeclass_class_wrong_sig
+ disable_cache: false
main: |
- from classes import typeclass
+ from classes import typeclass, AssociatedType
- @typeclass
- def example(instance, arg: str) -> bool:
+ class ToJson(AssociatedType):
...
- @example.instance(int)
- def _example_int(instance: str, arg: str) -> bool:
+ @typeclass(ToJson)
+ def to_json(instance, verbose: bool = False) -> str:
...
- reveal_type(example)
+ @to_json.instance(int)
+ def _to_json_int(instance: str) -> int:
+ ...
out: |
- main:7: error: Argument 1 has incompatible type "Callable[[str, str], bool]"; expected "Callable[[int, str], bool]"
- main:11: note: Revealed type is 'classes.typeclass._TypeClass[builtins.int*, builtins.bool, def (builtins.int*, arg: builtins.str) -> builtins.bool]'
+ main:10: error: Instance callback is incompatible "def (instance: builtins.str) -> builtins.int"; expected "def (instance: builtins.str, verbose: builtins.bool =) -> builtins.str"
+ main:10: error: Instance "builtins.str" does not match inferred type "builtins.int*"
-- case: typeclass_incorrect_instance_callback2
- disable_cache: true
+- case: typeclass_definied_by_literal
+ disable_cache: false
main: |
from classes import typeclass
- @typeclass
- def example(instance, arg: str) -> bool:
- ...
-
- @example.instance(int)
- def _example_int(instance: int) -> bool:
- ...
-
- reveal_type(example)
+ typeclass(1)
out: |
- main:7: error: Argument 1 has incompatible type "Callable[[int], bool]"; expected "Callable[[int, str], bool]"
- main:11: note: Revealed type is 'classes.typeclass._TypeClass[builtins.int*, builtins.bool, def (builtins.int*, arg: builtins.str) -> builtins.bool]'
+ main:3: error: No overload variant of "typeclass" matches argument type "int"
+ main:3: note: Possible overload variants:
+ main:3: note: def [_AssociatedType] typeclass(definition: Type[_AssociatedType]) -> _TypeClassDef[_AssociatedType]
+ main:3: note: def [_SignatureType, _InstanceType, _AssociatedType, _Fullname <: str] typeclass(signature: _SignatureType) -> _TypeClass[_InstanceType, _SignatureType, _AssociatedType, _Fullname]
-- case: typeclass_incorrect_instance_callback3
- disable_cache: true
+- case: typeclass_non_global_declaration
+ disable_cache: false
main: |
from classes import typeclass
- @typeclass
- def example(instance, arg: str) -> bool:
- ...
+ def some():
+ @typeclass
+ def a(instance) -> str:
+ ...
- @example.instance(int)
- def _example_int(instance, arg: int) -> bool:
- ...
+ @a.instance(int)
+ def _a_int(instance: int) -> str:
+ ...
- reveal_type(example)
+ reveal_type(_a_int)
out: |
- main:7: error: Argument 1 has incompatible type "Callable[[Any, int], bool]"; expected "Callable[[int, str], bool]"
- main:11: note: Revealed type is 'classes.typeclass._TypeClass[builtins.int*, builtins.bool, def (builtins.int*, arg: builtins.str) -> builtins.bool]'
+ main:8: error: Typeclass cannot be loaded, it must be a global declaration
+ main:12: note: Revealed type is "Any"
diff --git a/typesafety/test_typeclass/test_validation/test_body.yml b/typesafety/test_typeclass/test_validation/test_body.yml
new file mode 100644
index 0000000..c99db89
--- /dev/null
+++ b/typesafety/test_typeclass/test_validation/test_body.yml
@@ -0,0 +1,69 @@
+- case: typeclass_with_ellipsis
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def args(instance) -> str:
+ ...
+
+
+- case: typeclass_with_docstring
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def args(instance) -> str:
+ """Some."""
+
+
+- case: typeclass_with_body
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def args(instance) -> str:
+ return 'a'
+ out: |
+ main:3: error: Typeclass definitions must not have bodies
+
+
+- case: typeclass_with_pass_body
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def sum_all(instance) -> int:
+ pass
+
+
+- case: typeclass_with_body_and_associated_type
+ disable_cache: false
+ main: |
+ from classes import typeclass, AssociatedType
+
+ class Some(AssociatedType):
+ ...
+
+ @typeclass(Some)
+ def args(instance) -> str:
+ return 'a'
+ out: |
+ main:6: error: Typeclass definitions must not have bodies
+
+
+- case: typeclass_with_two_ellipsises
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def args(instance) -> str:
+ ...
+ ...
+ out: |
+ main:3: error: Typeclass definitions must not have bodies
+ main:4: error: Missing return statement
diff --git a/typesafety/test_typeclass/test_validation/test_first_arg.yml b/typesafety/test_typeclass/test_validation/test_first_arg.yml
new file mode 100644
index 0000000..f7a08d3
--- /dev/null
+++ b/typesafety/test_typeclass/test_validation/test_first_arg.yml
@@ -0,0 +1,94 @@
+- case: typeclass_first_arg_pos
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def args(instance) -> str:
+ ...
+
+
+- case: typeclass_first_arg_pos_only
+ disable_cache: false
+ skip: sys.version_info[:2] < (3, 8)
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def args(instance, /) -> str:
+ ...
+
+
+- case: typeclass_first_arg_opt
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def args(instance: int = 1) -> str:
+ ...
+ out: |
+ main:3: error: First argument in typeclass definition must be positional
+
+
+- case: typeclass_first_arg_opt_with_associated
+ disable_cache: false
+ main: |
+ from classes import typeclass, AssociatedType
+
+ class Some(AssociatedType):
+ ...
+
+ @typeclass(Some)
+ def args(instance: int = 1) -> str:
+ ...
+ out: |
+ main:6: error: First argument in typeclass definition must be positional
+
+
+- case: typeclass_first_arg_star
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def args(*instance: str) -> str:
+ ...
+ out: |
+ main:3: error: First argument in typeclass definition must be positional
+
+
+- case: typeclass_first_arg_star2
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def args(**instance) -> str:
+ ...
+ out: |
+ main:3: error: First argument in typeclass definition must be positional
+
+
+- case: typeclass_first_kw
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def args(*instance) -> str:
+ ...
+ out: |
+ main:3: error: First argument in typeclass definition must be positional
+
+
+- case: typeclass_first_kw_opt
+ disable_cache: false
+ main: |
+ from classes import typeclass
+
+ @typeclass
+ def args(*, instance: int = 1) -> str:
+ ...
+ out: |
+ main:3: error: First argument in typeclass definition must be positional
diff --git a/typesafety/test_typing_type.yml b/typesafety/test_typing_type.yml
new file mode 100644
index 0000000..7fd35c6
--- /dev/null
+++ b/typesafety/test_typing_type.yml
@@ -0,0 +1,26 @@
+- case: typing_type_correct
+ disable_cache: true
+ main: |
+ from typing import Type
+ from classes import typeclass
+
+ class _MyClass(object):
+ ...
+
+ class _MyClassType(Type[_MyClass]):
+ ...
+
+
+ @typeclass
+ def class_type(typ) -> Type:
+ ...
+
+ @class_type.instance(delegate=_MyClassType)
+ def _my_class_type(typ: Type[_MyClass]) -> Type:
+ return typ
+
+ class_type(_MyClass)
+ class_type(int)
+ out: |
+ main:7: error: Invalid base class "Type"
+ main:20: error: Argument 1 to "class_type" has incompatible type "Type[int]"; expected "Type[_MyClass]"