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 @@ ----- [![Build Status](https://travis-ci.org/dry-python/classes.svg?branch=master)](https://travis-ci.org/dry-python/classes) -[![Coverage Status](https://coveralls.io/repos/github/dry-python/classes/badge.svg?branch=master)](https://coveralls.io/github/dry-python/classes?branch=master) +[![codecov](https://codecov.io/gh/dry-python/classes/branch/master/graph/badge.svg)](https://codecov.io/gh/dry-python/classes) [![Documentation Status](https://readthedocs.org/projects/classes/badge/?version=latest)](https://classes.readthedocs.io/en/latest/?badge=latest) [![Python Version](https://img.shields.io/pypi/pyversions/classes.svg)](https://pypi.org/project/classes/) [![wemake-python-styleguide](https://img.shields.io/badge/style-wemake-000000.svg)](https://github.com/wemake-services/wemake-python-styleguide) -[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) +[![Telegram chat](https://img.shields.io/badge/chat-join-blue?logo=telegram)](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]"