├── tests ├── run.sh ├── health.sh ├── conftest.py ├── typing_test.py ├── util_test.py └── interfaces_test.py ├── interfaces ├── typing.py ├── __init__.py ├── compat.py ├── spec.py ├── base.py ├── exceptions.py └── util.py ├── .gitignore ├── Pipfile ├── setup.cfg ├── .pre-commit-config.yaml ├── pyproject.toml ├── LICENSE ├── README.md └── Pipfile.lock /tests/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec pipenv run pytest tests $* 3 | -------------------------------------------------------------------------------- /tests/health.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec pipenv run pytest --mypy --flake8 --isort --black interfaces $* 3 | -------------------------------------------------------------------------------- /interfaces/typing.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | 4 | if typing.TYPE_CHECKING: 5 | import interfaces.base 6 | 7 | InterfaceType = interfaces.base._InterfaceMeta 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore hidden 2 | .* 3 | *~ 4 | \#*\# 5 | 6 | # allow gitignore 7 | !.gitignore 8 | 9 | # allow some dot files 10 | !/.pre-commit-config.yaml 11 | 12 | # python 13 | *.pyc 14 | 15 | # ignore dev and build files 16 | /dist 17 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [requires] 7 | python_version = "3.7" 8 | 9 | [packages] 10 | 11 | [dev-packages] 12 | flit = "*" 13 | pytest = "*" 14 | pytest-black = "*" 15 | pytest-flake8 = "*" 16 | pytest-isort = "*" 17 | pytest-mypy = "*" 18 | rope = "*" 19 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [isort] 2 | force_grid_wrap=0 3 | include_trailing_comma=True 4 | line_length=88 5 | multi_line_output=3 6 | use_parentheses=True 7 | lines_after_imports = 2 8 | 9 | [flake8] 10 | ignore = E203, E266, E501, W503 11 | max-line-length = 88 12 | 13 | [mypy] 14 | ignore_missing_imports = True 15 | disallow_untyped_calls = True 16 | disallow_untyped_defs = True 17 | disallow_incomplete_defs = True 18 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import sys 3 | import typing 4 | 5 | import pytest 6 | 7 | 8 | def pytest_configure(config): 9 | sys.path.insert(0, str(pathlib.Path(__file__).parents[1])) 10 | 11 | 12 | @pytest.fixture(scope='session') 13 | def typeT1(): 14 | return typing.TypeVar('T1') 15 | 16 | 17 | @pytest.fixture(scope='session') 18 | def typeT2(): 19 | return typing.TypeVar('T2') 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: isort 5 | name: isort 6 | entry: pipenv run isort 7 | language: system 8 | types: [python] 9 | - id: mypy 10 | name: mypy 11 | entry: pipenv run mypy 12 | language: system 13 | types: [python] 14 | exclude: > 15 | (?x)^( 16 | tests/ 17 | ) 18 | - repo: https://github.com/ambv/black 19 | rev: 19.3b0 20 | hooks: 21 | - id: black 22 | - repo: https://github.com/pre-commit/pre-commit-hooks 23 | rev: v2.1.0 24 | hooks: 25 | - id: flake8 26 | -------------------------------------------------------------------------------- /tests/typing_test.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | import interfaces 4 | 5 | 6 | def test_typing_self_test() -> None: 7 | subprocess.run(f'mypy {__file__}', check=True, shell=True) 8 | 9 | 10 | class SampleInterface(interfaces.interface): # type: ignore 11 | def method(self) -> None: 12 | pass 13 | 14 | 15 | class SampleImplementation( 16 | interfaces.object, implements=[SampleInterface] # type: ignore 17 | ): 18 | def method(self) -> None: 19 | pass 20 | 21 | 22 | def typed_code_wrapper() -> SampleInterface: 23 | typed_var: SampleInterface 24 | typed_var = SampleImplementation() 25 | return typed_var 26 | -------------------------------------------------------------------------------- /interfaces/__init__.py: -------------------------------------------------------------------------------- 1 | """A new approach to interfaces in Python 2 | """ 3 | import interfaces.base 4 | import interfaces.compat 5 | import interfaces.exceptions 6 | import interfaces.spec 7 | import interfaces.util 8 | 9 | 10 | __version__ = '0.1.2' 11 | 12 | 13 | __all__ = ['Interface', 'Object', 'isimplementation'] 14 | 15 | 16 | interface = Interface = interfaces.base.Interface 17 | object = Object = interfaces.compat.Object 18 | 19 | InterfaceNoInstanceAllowedError = interfaces.exceptions.InterfaceNoInstanceAllowedError 20 | InterfaceNotImplementedError = interfaces.exceptions.InterfaceNotImplementedError 21 | InterfaceOverloadingError = interfaces.exceptions.InterfaceOverloadingError 22 | 23 | interface_spec = interfaces.spec.interface_spec 24 | 25 | isimplementation = interfaces.util.isimplementation 26 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit"] 3 | build-backend = "flit.buildapi" 4 | 5 | [tool.flit.metadata] 6 | module = "interfaces" 7 | author = "Serge Matveenko" 8 | author-email = "lig@countzero.co" 9 | home-page = "https://github.com/lig/python-interfaces" 10 | description-file = "README.md" 11 | classifiers = [ 12 | "Development Status :: 4 - Beta", 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved :: MIT License", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python :: 3.7", 17 | "Programming Language :: Python :: 3.8", 18 | "Programming Language :: Python :: 3 :: Only", 19 | "Topic :: Software Development :: Libraries :: Python Modules", 20 | "Typing :: Typed", 21 | ] 22 | requires-python = ">=3.7" 23 | dist-name = "strict-interfaces" 24 | keywords = "python strict interfaces implemetation pep" 25 | 26 | [tool.black] 27 | target-version = ["py37"] 28 | skip-string-normalization = true 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Serge Matveenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /interfaces/compat.py: -------------------------------------------------------------------------------- 1 | import collections.abc 2 | import typing 3 | 4 | import interfaces.base 5 | import interfaces.util 6 | 7 | 8 | __all__ = ['Object'] 9 | 10 | 11 | class Object: 12 | def __init_subclass__( 13 | cls, 14 | implements: typing.Optional[ 15 | typing.Union[ 16 | typing.Iterable[typing.Type[interfaces.base.Interface]], 17 | typing.Type[interfaces.base.Interface], 18 | ] 19 | ] = None, 20 | ) -> None: 21 | if implements is None: 22 | implements = () 23 | 24 | if not isinstance(implements, collections.abc.Iterable): 25 | implements = (implements,) 26 | 27 | for iface in implements: 28 | if not isinstance(iface, interfaces.base._InterfaceMeta): 29 | raise TypeError( 30 | "Arguments to `implements` must be subclasses of `interface`," 31 | " not `%r`", 32 | iface, 33 | ) 34 | 35 | for iface in implements: 36 | interfaces.util._isimplementation(cls, iface, raise_errors=True) 37 | 38 | super().__init_subclass__() 39 | -------------------------------------------------------------------------------- /interfaces/spec.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import collections.abc 4 | import functools 5 | import types 6 | import typing 7 | 8 | import interfaces.base 9 | import interfaces.typing 10 | 11 | 12 | class InterfaceSpec(collections.abc.Mapping): 13 | slots = ('_iface', '_iface_spec') 14 | 15 | def __init__(self, iface: interfaces.typing.InterfaceType) -> None: 16 | self._iface = iface 17 | self._iface_spec = { 18 | attr_name: getattr(iface, attr_name) 19 | for attr_name in self._get_iface_attrs(iface) 20 | if not (attr_name[:2] == attr_name[-2:] == '__') 21 | } 22 | 23 | def __getitem__(self, key: str) -> types.MethodType: 24 | return self._iface_spec[key] 25 | 26 | def __iter__(self) -> typing.Iterator[str]: 27 | return iter(self._iface_spec) 28 | 29 | def __len__(self) -> int: 30 | return len(self._iface_spec) 31 | 32 | def __repr__(self) -> str: 33 | return f"{self.__class__.__name__!s}({self._iface!r})" 34 | 35 | @staticmethod 36 | def _get_iface_attrs( 37 | iface: interfaces.typing.InterfaceType 38 | ) -> collections.abc.Iterable[str]: 39 | return dir(iface) 40 | 41 | 42 | class _PartialInterfaceSpec(InterfaceSpec): 43 | @staticmethod 44 | def _get_iface_attrs( 45 | iface: interfaces.typing.InterfaceType 46 | ) -> collections.abc.Iterable[str]: 47 | return vars(iface).keys() 48 | 49 | 50 | @functools.lru_cache(maxsize=None) 51 | def interface_spec(iface: interfaces.typing.InterfaceType) -> InterfaceSpec: 52 | return iface.__interface_spec__() 53 | -------------------------------------------------------------------------------- /interfaces/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | import interfaces.exceptions 6 | import interfaces.spec 7 | import interfaces.util 8 | 9 | 10 | __all__ = ['Interface'] 11 | 12 | 13 | class _InterfaceMeta(type): 14 | def __interface_spec__(self) -> interfaces.spec.InterfaceSpec: 15 | return interfaces.spec.InterfaceSpec(iface=self) 16 | 17 | def __subclasscheck__(self, subclass: typing.Type) -> bool: 18 | return interfaces.util.isimplementation(subclass, self) 19 | 20 | 21 | class Interface(metaclass=_InterfaceMeta): 22 | def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> None: 23 | raise interfaces.exceptions.InterfaceNoInstanceAllowedError(iface=cls) 24 | 25 | def __init_subclass__(cls) -> None: 26 | cls_method_names = interfaces.spec._PartialInterfaceSpec(cls).keys() 27 | 28 | for cls_base in cls.__bases__: 29 | 30 | if not isinstance(cls_base, _InterfaceMeta): 31 | raise TypeError( 32 | "Cannot create a consistent method resolution order (MRO) for bases" 33 | f" {', '.join(k.__name__ for k in cls.__bases__)}" 34 | ) 35 | 36 | base_spec = interfaces.spec.interface_spec(cls_base) 37 | if cls_method_names.isdisjoint(base_spec.keys()): 38 | continue 39 | 40 | raise interfaces.exceptions.InterfaceOverloadingError( 41 | method_names=set(base_spec).intersection(cls_method_names), 42 | ancestor_iface=cls_base, 43 | descendant_iface=cls, 44 | ) 45 | 46 | super().__init_subclass__() 47 | -------------------------------------------------------------------------------- /interfaces/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | import interfaces.typing 6 | 7 | 8 | __all__ = [ 9 | 'InterfaceNoInstanceAllowedError', 10 | 'InterfaceNotImplementedError', 11 | 'InterfaceOverloadingError', 12 | ] 13 | 14 | 15 | class InterfaceError(Exception): 16 | pass 17 | 18 | 19 | class InterfaceNoInstanceAllowedError(InterfaceError): 20 | def __init__(self, *, iface: interfaces.typing.InterfaceType) -> None: 21 | self._iface = iface 22 | 23 | def __str__(self) -> str: 24 | return ( 25 | f"Attempted to create an instance of interface `{self._iface!r}` which is" 26 | " not allowed" 27 | ) 28 | 29 | 30 | class InterfaceNotImplementedError(InterfaceError): 31 | def __init__( 32 | self, 33 | *, 34 | klass: typing.Type, 35 | method_name: str, 36 | iface: interfaces.typing.InterfaceType, 37 | ) -> None: 38 | self._klass = klass 39 | self._method_name = method_name 40 | self._iface = iface 41 | 42 | def __str__(self) -> str: 43 | return ( 44 | f"`{self._klass!r}` must fully implement `{self._method_name!s}` method of" 45 | f" `{self._iface}`" 46 | ) 47 | 48 | 49 | class InterfaceOverloadingError(InterfaceError): 50 | def __init__( 51 | self, 52 | *, 53 | method_names: typing.Container[str], 54 | ancestor_iface: interfaces.typing.InterfaceType, 55 | descendant_iface: interfaces.typing.InterfaceType, 56 | ) -> None: 57 | self._method_names = method_names 58 | self._ancestor_iface = ancestor_iface 59 | self._descendant_iface = descendant_iface 60 | 61 | def __str__(self) -> str: 62 | return ( 63 | f"Attempted to overload method(s) `{self._method_names!s}` of" 64 | f" `{self._ancestor_iface!r}` in `{self._descendant_iface!r}`" 65 | ) 66 | -------------------------------------------------------------------------------- /interfaces/util.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import collections.abc 4 | import inspect 5 | import typing 6 | 7 | import interfaces.exceptions 8 | import interfaces.spec 9 | import interfaces.typing 10 | 11 | 12 | __all__ = ['isimplementation'] 13 | 14 | 15 | def isimplementation( 16 | cls: typing.Type, 17 | interface_or_iterable: typing.Union[ 18 | typing.Iterable[interfaces.typing.InterfaceType], 19 | interfaces.typing.InterfaceType, 20 | ], 21 | ) -> bool: 22 | 23 | return all( 24 | _isimplementation(cls, iface) 25 | for iface in ( 26 | interface_or_iterable # type: ignore 27 | if isinstance(interface_or_iterable, collections.abc.Iterable) 28 | else (interface_or_iterable,) 29 | ) 30 | ) 31 | 32 | 33 | def _isimplementation( 34 | cls: type, iface: interfaces.typing.InterfaceType, *, raise_errors: bool = False 35 | ) -> bool: 36 | 37 | for attr_name, iface_attr in interfaces.spec.interface_spec(iface).items(): 38 | 39 | if attr_name[:2] == attr_name[-2:] == '__': 40 | continue 41 | 42 | if not hasattr(cls, attr_name): 43 | return _isimplementation_fail(cls, attr_name, iface, raise_errors) 44 | 45 | cls_attr = inspect.getattr_static(cls, attr_name) 46 | 47 | if ( 48 | inspect.isdatadescriptor(iface_attr) 49 | and inspect.isdatadescriptor(cls_attr) 50 | and ( 51 | inspect.signature(cls_attr.__get__) 52 | == inspect.signature(iface_attr.__get__) 53 | ) 54 | and ( 55 | inspect.signature(cls_attr.__set__) 56 | == inspect.signature(iface_attr.__set__) 57 | ) 58 | and ( 59 | inspect.signature(cls_attr.__delete__) 60 | == inspect.signature(iface_attr.__delete__) 61 | ) 62 | ): 63 | continue 64 | 65 | if ( 66 | inspect.isfunction(iface_attr) 67 | and inspect.isfunction(cls_attr) 68 | and inspect.signature(cls_attr) == inspect.signature(iface_attr) 69 | ): 70 | continue 71 | 72 | return _isimplementation_fail(cls, attr_name, iface, raise_errors) 73 | 74 | return True 75 | 76 | 77 | def _isimplementation_fail( 78 | cls: type, 79 | attr_name: str, 80 | iface: interfaces.typing.InterfaceType, 81 | raise_errors: bool, 82 | ) -> bool: 83 | if raise_errors: 84 | raise interfaces.exceptions.InterfaceNotImplementedError( 85 | klass=cls, method_name=attr_name, iface=iface 86 | ) 87 | else: 88 | return False 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Strict Interfaces 2 | 3 | 4 | ## Installation 5 | 6 | ```shell 7 | pip install strict-interfaces 8 | ``` 9 | 10 | 11 | ## Design Goals 12 | 13 | * Be as strict as possible 14 | * Fail on import time 15 | * Do not mess with `object` and/or `type` inheritance 16 | * Possibility to integrate in CPython Core 17 | * Ability to use "out of the box" regardless support in an interpreter 18 | 19 | 20 | ## Features 21 | 22 | * Special keyword `implements` on the class definition 23 | * Multiple interface implementation 24 | * Implicit interface implementation 25 | * Interface inheritance with overloading being restricted 26 | * Special `isimplementation` function similar to `issubclass` 27 | * Partial `issubclass` support (see below) 28 | * It's restricted to create an interface instance 29 | * It's restricted to inherit from `object` and `interface` at the same time 30 | 31 | 32 | ## Usage 33 | 34 | ### Explicit implementation 35 | 36 | ```python 37 | class TestInterface(interfaces.interface): 38 | def method(self, arg: typeT1) -> typeT2: 39 | pass 40 | 41 | class TestClass(interfaces.object, implements=[TestInterface]): 42 | def method(self, arg: typeT1) -> typeT2: 43 | pass 44 | ``` 45 | 46 | ### Raises when is not implemented 47 | 48 | ```python 49 | class TestInterface(interfaces.interface): 50 | def method(self, arg): 51 | pass 52 | 53 | class TestClass(interfaces.object, implements=[TestInterface]): 54 | pass 55 | ``` 56 | 57 | ### Implicit implementation and run-time check 58 | 59 | ```python 60 | class TestInterfaceA(interfaces.interface): 61 | def method_a(arg: typeT1) -> typeT1: 62 | pass 63 | 64 | class TestInterfaceB(interfaces.interface): 65 | def method_b(arg: typeT2) -> typeT2: 66 | pass 67 | 68 | class TestClass: 69 | def method_a(arg: typeT1) -> typeT1: 70 | pass 71 | 72 | def method_b(arg: typeT2) -> typeT2: 73 | pass 74 | 75 | assert interfaces.isimplementation(TestClass, (TestInterfaceA, TestInterfaceB)) 76 | ``` 77 | 78 | ### `isimplementation` checks whether all interfaces are implemented 79 | 80 | ```python 81 | class TestInterfaceA(interfaces.interface): 82 | def method_a(arg: typeT1) -> typeT1: 83 | pass 84 | 85 | class TestInterfaceB(interfaces.interface): 86 | def method_b(arg: typeT2) -> typeT2: 87 | pass 88 | 89 | class TestClass: 90 | def method_a(arg: typeT1) -> typeT1: 91 | pass 92 | 93 | # NOTE: In this case `isimplementation` behaves different than `issubclass` 94 | assert not interfaces.isimplementation(TestClass, (TestInterfaceA, TestInterfaceB)) 95 | assert issubclass(TestClass, (TestInterfaceA, TestInterfaceB)) 96 | ``` 97 | 98 | 99 | ## Contributing 100 | 101 | Pull requests, feature requests, and bug reports are always welcome! 102 | 103 | [github.com/lig/python-interfaces](https://github.com/lig/python-interfaces) 104 | 105 | 106 | ## Discussions 107 | 108 | There are threads [on Python Ideas mailing list](https://groups.google.com/forum/#!topic/python-ideas/uZqje5A1Mcs) and [on Reddit](https://www.reddit.com/r/Python/comments/bkwtnp/a_new_approach_to_interfaces_in_python/). 109 | -------------------------------------------------------------------------------- /tests/util_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import interfaces 4 | 5 | 6 | @pytest.fixture(scope='session', params=[interfaces.isimplementation, issubclass]) 7 | def isimplementation(request): 8 | return request.param 9 | 10 | 11 | def test_010_isimplementation_single_true_explicit_interface( 12 | typeT1, typeT2, isimplementation 13 | ): 14 | class TestInterface(interfaces.interface): 15 | def method(arg: typeT1) -> typeT2: 16 | pass 17 | 18 | class TestClass(interfaces.object, implements=[TestInterface]): 19 | def method(arg: typeT1) -> typeT2: 20 | pass 21 | 22 | assert isimplementation(TestClass, TestInterface) 23 | 24 | 25 | def test_020_isimplementation_single_true_explicit_object( 26 | typeT1, typeT2, isimplementation 27 | ): 28 | class TestInterface(interfaces.interface): 29 | def method(arg: typeT1) -> typeT2: 30 | pass 31 | 32 | class TestClass(interfaces.object): 33 | def method(arg: typeT1) -> typeT2: 34 | pass 35 | 36 | assert isimplementation(TestClass, TestInterface) 37 | 38 | 39 | def test_030_isimplementation_single_true(typeT1, typeT2, isimplementation): 40 | class TestInterface(interfaces.interface): 41 | def method(arg: typeT1) -> typeT2: 42 | pass 43 | 44 | class TestClass: 45 | def method(arg: typeT1) -> typeT2: 46 | pass 47 | 48 | assert isimplementation(TestClass, TestInterface) 49 | 50 | 51 | def test_040_isimplementation_single_false(typeT1, typeT2, isimplementation): 52 | class TestInterface(interfaces.interface): 53 | def method(arg: typeT1) -> typeT2: 54 | pass 55 | 56 | class TestClass: 57 | pass 58 | 59 | assert not isimplementation(TestClass, TestInterface) 60 | 61 | 62 | def test_050_isimplementation_multi_true_one_explicit(typeT1, typeT2, isimplementation): 63 | class TestInterfaceA(interfaces.interface): 64 | def method_a(arg: typeT1) -> typeT1: 65 | pass 66 | 67 | class TestInterfaceB(interfaces.interface): 68 | def method_b(arg: typeT2) -> typeT2: 69 | pass 70 | 71 | class TestClass(interfaces.object, implements=[TestInterfaceA, TestInterfaceB]): 72 | def method_a(arg: typeT1) -> typeT1: 73 | pass 74 | 75 | def method_b(arg: typeT2) -> typeT2: 76 | pass 77 | 78 | assert isimplementation(TestClass, TestInterfaceA) 79 | 80 | 81 | def test_060_isimplementation_multi_true_one(typeT1, typeT2, isimplementation): 82 | class TestInterfaceA(interfaces.interface): 83 | def method_a(arg: typeT1) -> typeT1: 84 | pass 85 | 86 | class TestInterfaceB(interfaces.interface): 87 | def method_b(arg: typeT2) -> typeT2: 88 | pass 89 | 90 | class TestClass: 91 | def method_a(arg: typeT1) -> typeT1: 92 | pass 93 | 94 | def method_b(arg: typeT2) -> typeT2: 95 | pass 96 | 97 | assert isimplementation(TestClass, TestInterfaceA) 98 | 99 | 100 | def test_070_isimplementation_multi_true_all_explicit(typeT1, typeT2, isimplementation): 101 | class TestInterfaceA(interfaces.interface): 102 | def method_a(arg: typeT1) -> typeT1: 103 | pass 104 | 105 | class TestInterfaceB(interfaces.interface): 106 | def method_b(arg: typeT2) -> typeT2: 107 | pass 108 | 109 | class TestClass(interfaces.object, implements=[TestInterfaceA, TestInterfaceB]): 110 | def method_a(arg: typeT1) -> typeT1: 111 | pass 112 | 113 | def method_b(arg: typeT2) -> typeT2: 114 | pass 115 | 116 | assert isimplementation(TestClass, (TestInterfaceA, TestInterfaceB)) 117 | 118 | 119 | def test_080_isimplementation_multi_true_all(typeT1, typeT2, isimplementation): 120 | class TestInterfaceA(interfaces.interface): 121 | def method_a(arg: typeT1) -> typeT1: 122 | pass 123 | 124 | class TestInterfaceB(interfaces.interface): 125 | def method_b(arg: typeT2) -> typeT2: 126 | pass 127 | 128 | class TestClass: 129 | def method_a(arg: typeT1) -> typeT1: 130 | pass 131 | 132 | def method_b(arg: typeT2) -> typeT2: 133 | pass 134 | 135 | assert isimplementation(TestClass, (TestInterfaceA, TestInterfaceB)) 136 | 137 | 138 | def test_090_isimplementation_multi_false_one(typeT1, typeT2): 139 | class TestInterfaceA(interfaces.interface): 140 | def method_a(arg: typeT1) -> typeT1: 141 | pass 142 | 143 | class TestInterfaceB(interfaces.interface): 144 | def method_b(arg: typeT2) -> typeT2: 145 | pass 146 | 147 | class TestClass: 148 | def method_a(arg: typeT1) -> typeT1: 149 | pass 150 | 151 | # NOTE: In this case `isimplementation` behaves different than `issubclass` 152 | assert not interfaces.isimplementation(TestClass, (TestInterfaceA, TestInterfaceB)) 153 | 154 | 155 | def test_100_isimplementation_multi_false_all(typeT1, typeT2, isimplementation): 156 | class TestInterfaceA(interfaces.interface): 157 | def method_a(arg: typeT1) -> typeT1: 158 | pass 159 | 160 | class TestInterfaceB(interfaces.interface): 161 | def method_b(arg: typeT2) -> typeT2: 162 | pass 163 | 164 | class TestClass: 165 | pass 166 | 167 | assert not isimplementation(TestClass, (TestInterfaceA, TestInterfaceB)) 168 | -------------------------------------------------------------------------------- /tests/interfaces_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import interfaces 4 | 5 | 6 | def test_010_empty_definition(): 7 | class TestInterface(interfaces.interface): 8 | pass 9 | 10 | class TestClass(interfaces.object, implements=[TestInterface]): 11 | pass 12 | 13 | 14 | def test_020_non_iterable_as_implements_value(): 15 | class TestInterface(interfaces.interface): 16 | pass 17 | 18 | class TestClass(interfaces.object, implements=TestInterface): 19 | pass 20 | 21 | 22 | def test_030_no_implements_value(): 23 | class TestInterface(interfaces.interface): 24 | pass 25 | 26 | class TestClass(interfaces.object): 27 | pass 28 | 29 | 30 | def test_040_error_non_interface_as_implements_value(): 31 | class TestInterface(interfaces.interface): 32 | pass 33 | 34 | with pytest.raises(TypeError): 35 | 36 | class TestClass(interfaces.object, implements=[object]): 37 | pass 38 | 39 | 40 | def test_050_method_is_implemented_no_annotations(): 41 | class TestInterface(interfaces.interface): 42 | def method(self, arg): 43 | pass 44 | 45 | class TestClass(interfaces.object, implements=[TestInterface]): 46 | def method(self, arg): 47 | pass 48 | 49 | 50 | def test_060_method_is_implemented_with_annotations(typeT1, typeT2): 51 | class TestInterface(interfaces.interface): 52 | def method(self, arg: typeT1) -> typeT2: 53 | pass 54 | 55 | class TestClass(interfaces.object, implements=[TestInterface]): 56 | def method(self, arg: typeT1) -> typeT2: 57 | pass 58 | 59 | 60 | def test_070_error_method_is_not_implemented(): 61 | class TestInterface(interfaces.interface): 62 | def method(self, arg): 63 | pass 64 | 65 | with pytest.raises(interfaces.InterfaceNotImplementedError): 66 | 67 | class TestClass(interfaces.object, implements=[TestInterface]): 68 | pass 69 | 70 | 71 | def test_080_error_method_params_signature_is_not_implemented(typeT1, typeT2): 72 | class TestInterface(interfaces.interface): 73 | def method(self, arg: typeT1) -> typeT2: 74 | pass 75 | 76 | with pytest.raises(interfaces.InterfaceNotImplementedError): 77 | 78 | class TestClass(interfaces.object, implements=[TestInterface]): 79 | def method(self, arg: typeT2) -> typeT2: 80 | pass 81 | 82 | 83 | def test_090_error_method_return_signature_is_not_implemented(typeT1, typeT2): 84 | class TestInterface(interfaces.interface): 85 | def method(self, arg: typeT1) -> typeT2: 86 | pass 87 | 88 | with pytest.raises(interfaces.InterfaceNotImplementedError): 89 | 90 | class TestClass(interfaces.object, implements=[TestInterface]): 91 | def method(self, arg: typeT1) -> typeT1: 92 | pass 93 | 94 | 95 | def test_100_property_is_implemented_no_annotations(): 96 | class TestInterface(interfaces.interface): 97 | @property 98 | def value(self): 99 | pass 100 | 101 | class TestClass(interfaces.object, implements=[TestInterface]): 102 | @property 103 | def value(self): 104 | pass 105 | 106 | 107 | def test_110_error_property_is_implemented_as_method_no_annotations(): 108 | class TestInterface(interfaces.interface): 109 | @property 110 | def value(self): 111 | pass 112 | 113 | with pytest.raises(interfaces.InterfaceNotImplementedError): 114 | 115 | class TestClass(interfaces.object, implements=[TestInterface]): 116 | def value(self): 117 | pass 118 | 119 | 120 | def test_120_error_interface_instance(): 121 | class TestInterface(interfaces.interface): 122 | pass 123 | 124 | with pytest.raises(interfaces.InterfaceNoInstanceAllowedError): 125 | _ = TestInterface() 126 | 127 | 128 | def test_130_interface_inheritance(typeT1, typeT2): 129 | class TestInterfaceA(interfaces.interface): 130 | def method_a(arg: typeT1) -> typeT1: 131 | pass 132 | 133 | class TestInterfaceB(TestInterfaceA): 134 | def method_b(arg: typeT2) -> typeT2: 135 | pass 136 | 137 | class TestClass(interfaces.object, implements=[TestInterfaceB]): 138 | def method_a(arg: typeT1) -> typeT1: 139 | pass 140 | 141 | def method_b(arg: typeT2) -> typeT2: 142 | pass 143 | 144 | 145 | def test_140_error_not_implemented_with_inheritance(typeT1, typeT2): 146 | class TestInterfaceA(interfaces.interface): 147 | def method_a(arg: typeT1) -> typeT1: 148 | pass 149 | 150 | class TestInterfaceB(TestInterfaceA): 151 | def method_b(arg: typeT2) -> typeT2: 152 | pass 153 | 154 | with pytest.raises(interfaces.InterfaceNotImplementedError): 155 | 156 | class TestClass(interfaces.object, implements=[TestInterfaceB]): 157 | def method_b(arg: typeT2) -> typeT2: 158 | pass 159 | 160 | 161 | def test_150_interface_multiple_inheritance(typeT1, typeT2): 162 | class TestInterfaceA(interfaces.interface): 163 | def method_a(arg: typeT1) -> typeT1: 164 | pass 165 | 166 | class TestInterfaceB(interfaces.interface): 167 | def method_b(arg: typeT2) -> typeT2: 168 | pass 169 | 170 | class TestInterfaceC(TestInterfaceA, TestInterfaceB): 171 | pass 172 | 173 | class TestClass(interfaces.object, implements=[TestInterfaceC]): 174 | def method_a(arg: typeT1) -> typeT1: 175 | pass 176 | 177 | def method_b(arg: typeT2) -> typeT2: 178 | pass 179 | 180 | 181 | def test_160_error_not_implemented_with_multiple_inheritance(typeT1, typeT2): 182 | class TestInterfaceA(interfaces.interface): 183 | def method_a(arg: typeT1) -> typeT1: 184 | pass 185 | 186 | class TestInterfaceB(interfaces.interface): 187 | def method_b(arg: typeT2) -> typeT2: 188 | pass 189 | 190 | class TestInterfaceC(TestInterfaceA, TestInterfaceB): 191 | pass 192 | 193 | with pytest.raises(interfaces.InterfaceNotImplementedError): 194 | 195 | class TestClassA(interfaces.object, implements=[TestInterfaceC]): 196 | def method_a(arg: typeT1) -> typeT1: 197 | pass 198 | 199 | with pytest.raises(interfaces.InterfaceNotImplementedError): 200 | 201 | class TestClassB(interfaces.object, implements=[TestInterfaceC]): 202 | def method_b(arg: typeT2) -> typeT2: 203 | pass 204 | 205 | 206 | def test_170_error_method_overloading(): 207 | class TestInterfaceA(interfaces.interface): 208 | def method(): 209 | pass 210 | 211 | with pytest.raises(interfaces.InterfaceOverloadingError): 212 | 213 | class TestInterfaceB(TestInterfaceA): 214 | def method(): 215 | pass 216 | 217 | 218 | def test_180_error_mixing_bases_interface_object(): 219 | class TestInterface(interfaces.interface): 220 | pass 221 | 222 | class TestClassA: 223 | pass 224 | 225 | with pytest.raises(TypeError): 226 | 227 | class TestClassB(TestInterface, TestClassA): 228 | pass 229 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "af8a649091d2100000af90aa90bf7d24b433383b889daa73a5f7d7a707c3d522" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": {}, 19 | "develop": { 20 | "appdirs": { 21 | "hashes": [ 22 | "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", 23 | "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" 24 | ], 25 | "version": "==1.4.3" 26 | }, 27 | "atomicwrites": { 28 | "hashes": [ 29 | "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", 30 | "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" 31 | ], 32 | "version": "==1.3.0" 33 | }, 34 | "attrs": { 35 | "hashes": [ 36 | "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", 37 | "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" 38 | ], 39 | "version": "==19.1.0" 40 | }, 41 | "black": { 42 | "hashes": [ 43 | "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", 44 | "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" 45 | ], 46 | "version": "==19.3b0" 47 | }, 48 | "certifi": { 49 | "hashes": [ 50 | "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", 51 | "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" 52 | ], 53 | "version": "==2019.3.9" 54 | }, 55 | "chardet": { 56 | "hashes": [ 57 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 58 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 59 | ], 60 | "version": "==3.0.4" 61 | }, 62 | "click": { 63 | "hashes": [ 64 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 65 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 66 | ], 67 | "version": "==7.0" 68 | }, 69 | "docutils": { 70 | "hashes": [ 71 | "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", 72 | "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", 73 | "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" 74 | ], 75 | "version": "==0.14" 76 | }, 77 | "entrypoints": { 78 | "hashes": [ 79 | "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", 80 | "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" 81 | ], 82 | "version": "==0.3" 83 | }, 84 | "flake8": { 85 | "hashes": [ 86 | "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", 87 | "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" 88 | ], 89 | "version": "==3.7.7" 90 | }, 91 | "flit": { 92 | "hashes": [ 93 | "sha256:1d93f7a833ed8a6e120ddc40db5c4763bc39bccc75c05081ec8285ece718aefb", 94 | "sha256:6f6f0fb83c51ffa3a150fa41b5ac118df9ea4a87c2c06dff4ebf9adbe7b52b36" 95 | ], 96 | "index": "pypi", 97 | "version": "==1.3" 98 | }, 99 | "idna": { 100 | "hashes": [ 101 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 102 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 103 | ], 104 | "version": "==2.8" 105 | }, 106 | "isort": { 107 | "hashes": [ 108 | "sha256:01cb7e1ca5e6c5b3f235f0385057f70558b70d2f00320208825fa62887292f43", 109 | "sha256:268067462aed7eb2a1e237fcb287852f22077de3fb07964e87e00f829eea2d1a" 110 | ], 111 | "version": "==4.3.17" 112 | }, 113 | "mccabe": { 114 | "hashes": [ 115 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 116 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 117 | ], 118 | "version": "==0.6.1" 119 | }, 120 | "more-itertools": { 121 | "hashes": [ 122 | "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7", 123 | "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a" 124 | ], 125 | "markers": "python_version > '2.7'", 126 | "version": "==7.0.0" 127 | }, 128 | "mypy": { 129 | "hashes": [ 130 | "sha256:2afe51527b1f6cdc4a5f34fc90473109b22bf7f21086ba3e9451857cf11489e6", 131 | "sha256:56a16df3e0abb145d8accd5dbb70eba6c4bd26e2f89042b491faa78c9635d1e2", 132 | "sha256:5764f10d27b2e93c84f70af5778941b8f4aa1379b2430f85c827e0f5464e8714", 133 | "sha256:5bbc86374f04a3aa817622f98e40375ccb28c4836f36b66706cf3c6ccce86eda", 134 | "sha256:6a9343089f6377e71e20ca734cd8e7ac25d36478a9df580efabfe9059819bf82", 135 | "sha256:6c9851bc4a23dc1d854d3f5dfd5f20a016f8da86bcdbb42687879bb5f86434b0", 136 | "sha256:b8e85956af3fcf043d6f87c91cbe8705073fc67029ba6e22d3468bfee42c4823", 137 | "sha256:b9a0af8fae490306bc112229000aa0c2ccc837b49d29a5c42e088c132a2334dd", 138 | "sha256:bbf643528e2a55df2c1587008d6e3bda5c0445f1240dfa85129af22ae16d7a9a", 139 | "sha256:c46ab3438bd21511db0f2c612d89d8344154c0c9494afc7fbc932de514cf8d15", 140 | "sha256:f7a83d6bd805855ef83ec605eb01ab4fa42bcef254b13631e451cbb44914a9b0" 141 | ], 142 | "version": "==0.701" 143 | }, 144 | "mypy-extensions": { 145 | "hashes": [ 146 | "sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812", 147 | "sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e" 148 | ], 149 | "version": "==0.4.1" 150 | }, 151 | "pluggy": { 152 | "hashes": [ 153 | "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", 154 | "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" 155 | ], 156 | "version": "==0.9.0" 157 | }, 158 | "py": { 159 | "hashes": [ 160 | "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", 161 | "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" 162 | ], 163 | "version": "==1.8.0" 164 | }, 165 | "pycodestyle": { 166 | "hashes": [ 167 | "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", 168 | "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" 169 | ], 170 | "version": "==2.5.0" 171 | }, 172 | "pyflakes": { 173 | "hashes": [ 174 | "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", 175 | "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" 176 | ], 177 | "version": "==2.1.1" 178 | }, 179 | "pytest": { 180 | "hashes": [ 181 | "sha256:3773f4c235918987d51daf1db66d51c99fac654c81d6f2f709a046ab446d5e5d", 182 | "sha256:b7802283b70ca24d7119b32915efa7c409982f59913c1a6c0640aacf118b95f5" 183 | ], 184 | "index": "pypi", 185 | "version": "==4.4.1" 186 | }, 187 | "pytest-black": { 188 | "hashes": [ 189 | "sha256:6849e2cfa9a659377c52e0e698e8d0f8a79ff603cb1c930ad86b512357edba4f" 190 | ], 191 | "index": "pypi", 192 | "version": "==0.3.5" 193 | }, 194 | "pytest-flake8": { 195 | "hashes": [ 196 | "sha256:4d225c13e787471502ff94409dcf6f7927049b2ec251c63b764a4b17447b60c0", 197 | "sha256:d7e2b6b274a255b7ae35e9224c85294b471a83b76ecb6bd53c337ae977a499af" 198 | ], 199 | "index": "pypi", 200 | "version": "==1.0.4" 201 | }, 202 | "pytest-isort": { 203 | "hashes": [ 204 | "sha256:3be60e0de277b420ff89303ca6494320c41f7819ffa898756b90ef976e4c636a", 205 | "sha256:4bfee60dad1870b51700d55a85f5ceda766bd9d3d2878c1bbabee80e61b1be1a" 206 | ], 207 | "index": "pypi", 208 | "version": "==0.3.1" 209 | }, 210 | "pytest-mypy": { 211 | "hashes": [ 212 | "sha256:8f6436eed8118afd6c10a82b3b60fb537336736b0fd7a29262a656ac42ce01ac", 213 | "sha256:acc653210e7d8d5c72845a5248f00fd33f4f3379ca13fe56cfc7b749b5655c3e" 214 | ], 215 | "index": "pypi", 216 | "version": "==0.3.2" 217 | }, 218 | "pytoml": { 219 | "hashes": [ 220 | "sha256:ca2d0cb127c938b8b76a9a0d0f855cf930c1d50cc3a0af6d3595b566519a1013" 221 | ], 222 | "version": "==0.1.20" 223 | }, 224 | "requests": { 225 | "hashes": [ 226 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", 227 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" 228 | ], 229 | "version": "==2.21.0" 230 | }, 231 | "rope": { 232 | "hashes": [ 233 | "sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969", 234 | "sha256:c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf", 235 | "sha256:f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf" 236 | ], 237 | "index": "pypi", 238 | "version": "==0.14.0" 239 | }, 240 | "six": { 241 | "hashes": [ 242 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 243 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 244 | ], 245 | "version": "==1.12.0" 246 | }, 247 | "toml": { 248 | "hashes": [ 249 | "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", 250 | "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" 251 | ], 252 | "version": "==0.10.0" 253 | }, 254 | "typed-ast": { 255 | "hashes": [ 256 | "sha256:132eae51d6ef3ff4a8c47c393a4ef5ebf0d1aecc96880eb5d6c8ceab7017cc9b", 257 | "sha256:18141c1484ab8784006c839be8b985cfc82a2e9725837b0ecfa0203f71c4e39d", 258 | "sha256:2baf617f5bbbfe73fd8846463f5aeafc912b5ee247f410700245d68525ec584a", 259 | "sha256:3d90063f2cbbe39177e9b4d888e45777012652d6110156845b828908c51ae462", 260 | "sha256:4304b2218b842d610aa1a1d87e1dc9559597969acc62ce717ee4dfeaa44d7eee", 261 | "sha256:4983ede548ffc3541bae49a82675996497348e55bafd1554dc4e4a5d6eda541a", 262 | "sha256:5315f4509c1476718a4825f45a203b82d7fdf2a6f5f0c8f166435975b1c9f7d4", 263 | "sha256:6cdfb1b49d5345f7c2b90d638822d16ba62dc82f7616e9b4caa10b72f3f16649", 264 | "sha256:7b325f12635598c604690efd7a0197d0b94b7d7778498e76e0710cd582fd1c7a", 265 | "sha256:8d3b0e3b8626615826f9a626548057c5275a9733512b137984a68ba1598d3d2f", 266 | "sha256:8f8631160c79f53081bd23446525db0bc4c5616f78d04021e6e434b286493fd7", 267 | "sha256:912de10965f3dc89da23936f1cc4ed60764f712e5fa603a09dd904f88c996760", 268 | "sha256:b010c07b975fe853c65d7bbe9d4ac62f1c69086750a574f6292597763781ba18", 269 | "sha256:c908c10505904c48081a5415a1e295d8403e353e0c14c42b6d67f8f97fae6616", 270 | "sha256:c94dd3807c0c0610f7c76f078119f4ea48235a953512752b9175f9f98f5ae2bd", 271 | "sha256:ce65dee7594a84c466e79d7fb7d3303e7295d16a83c22c7c4037071b059e2c21", 272 | "sha256:eaa9cfcb221a8a4c2889be6f93da141ac777eb8819f077e1d09fb12d00a09a93", 273 | "sha256:f3376bc31bad66d46d44b4e6522c5c21976bf9bca4ef5987bb2bf727f4506cbb", 274 | "sha256:f9202fa138544e13a4ec1a6792c35834250a85958fde1251b6a22e07d1260ae7" 275 | ], 276 | "version": "==1.3.5" 277 | }, 278 | "urllib3": { 279 | "hashes": [ 280 | "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", 281 | "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3" 282 | ], 283 | "version": "==1.24.2" 284 | } 285 | } 286 | } 287 | --------------------------------------------------------------------------------