diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..c5a6404 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,26 @@ +name: pre-commit + +on: + push: + branches: + - master + + pull_request: + branches: + - master + +jobs: + pre-commit: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Run pre-commit + uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3b95e58 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,50 @@ +name: Test + +on: + push: + branches: + - master + + pull_request: + branches: + - master + +jobs: + test: + runs-on: "ubuntu-latest" + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + # Install all supported Python versions as tox will handle them from the single command + - name: Setup Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: Setup Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: Setup Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Setup Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Setup Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: python -m pip install .[dev] + + - name: Run tox targets + run: "python -m tox" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..39023a9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-toml + - id: trailing-whitespace + +- repo: https://github.com/psf/black + rev: 24.8.0 + hooks: + - id: black + language_version: python3.10 + +- repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort diff --git a/pure_interface/__init__.py b/pure_interface/__init__.py index 7515c9a..2b9dada 100644 --- a/pure_interface/__init__.py +++ b/pure_interface/__init__.py @@ -1,10 +1,18 @@ -from .errors import PureInterfaceError, InterfaceError, AdaptionError -from .interface import Interface, InterfaceType -from .interface import type_is_interface, get_type_interfaces -from .interface import get_interface_names, get_interface_method_names, get_interface_attribute_names -from .interface import get_is_development, set_is_development, get_missing_method_warnings from ._sub_interface import sub_interface_of -from .adaption import adapts, register_adapter, AdapterTracker, adapt_args +from .adaption import AdapterTracker, adapt_args, adapts, register_adapter from .delegation import Delegate +from .errors import AdaptionError, InterfaceError, PureInterfaceError +from .interface import ( + Interface, + InterfaceType, + get_interface_attribute_names, + get_interface_method_names, + get_interface_names, + get_is_development, + get_missing_method_warnings, + get_type_interfaces, + set_is_development, + type_is_interface, +) -__version__ = '8.0.2' +__version__ = "8.0.2" diff --git a/pure_interface/_sub_interface.py b/pure_interface/_sub_interface.py index 86da644..cd5d274 100644 --- a/pure_interface/_sub_interface.py +++ b/pure_interface/_sub_interface.py @@ -4,11 +4,13 @@ The decorator checks that the sub-interface is infact a subset and registers the larger interface as an implementation of the sub-interface. """ + from inspect import signature -from typing import Callable, TypeVar, Type +from typing import Callable, Type, TypeVar + from . import errors, interface -AnotherInterfaceType = TypeVar('AnotherInterfaceType', bound=Type[interface.Interface]) +AnotherInterfaceType = TypeVar("AnotherInterfaceType", bound=Type[interface.Interface]) def _check_interfaces_match(large_interface, small_interface): @@ -18,38 +20,40 @@ def _check_interfaces_match(large_interface, small_interface): small_methods = interface.get_interface_method_names(small_interface) if len(small_attributes) + len(small_methods) == 0: - raise interface.InterfaceError(f'Sub-interface {small_interface.__name__} is empty') + raise interface.InterfaceError(f"Sub-interface {small_interface.__name__} is empty") if not small_attributes.issubset(large_attributes): new_attrs = sorted(small_attributes.difference(large_attributes)) - attr_str = ', '.join(new_attrs) - msg = f'{small_interface.__name__} has attributes that are not on {large_interface.__name__}: {attr_str}' + attr_str = ", ".join(new_attrs) + msg = f"{small_interface.__name__} has attributes that are not on {large_interface.__name__}: {attr_str}" raise interface.InterfaceError(msg) if not small_methods.issubset(large_methods): new_methods = sorted(small_methods.difference(large_methods)) - method_str = ', '.join(new_methods) - msg = f'{small_interface.__name__} has methods that are not on {large_interface.__name__}: {method_str}' + method_str = ", ".join(new_methods) + msg = f"{small_interface.__name__} has methods that are not on {large_interface.__name__}: {method_str}" raise interface.InterfaceError(msg) for method_name in small_methods: large_method = getattr(large_interface, method_name) small_method = getattr(small_interface, method_name) if signature(large_method) != signature(small_method): - msg = (f'Signature of method {method_name} on {large_interface.__name__} ' - f'and {small_interface.__name__} must match exactly') + msg = ( + f"Signature of method {method_name} on {large_interface.__name__} " + f"and {small_interface.__name__} must match exactly" + ) raise interface.InterfaceError(msg) def sub_interface_of( - large_interface: interface.AnInterfaceType + large_interface: interface.AnInterfaceType, ) -> Callable[[AnotherInterfaceType], AnotherInterfaceType]: if not interface.type_is_interface(large_interface): - raise errors.InterfaceError(f'sub_interface_of argument {large_interface} is not an interface type') + raise errors.InterfaceError(f"sub_interface_of argument {large_interface} is not an interface type") def decorator(small_interface: AnotherInterfaceType) -> AnotherInterfaceType: if not interface.type_is_interface(small_interface): - raise errors.InterfaceError('class decorated by sub_interface_of must be an interface type') + raise errors.InterfaceError("class decorated by sub_interface_of must be an interface type") _check_interfaces_match(large_interface, small_interface) small_interface.register(large_interface) # type: ignore[arg-type] diff --git a/pure_interface/adaption.py b/pure_interface/adaption.py index 78819b3..d0008bd 100644 --- a/pure_interface/adaption.py +++ b/pure_interface/adaption.py @@ -1,15 +1,21 @@ -from __future__ import division, absolute_import, print_function +from __future__ import absolute_import, division, print_function import functools import inspect import types -from typing import Any, Type, TypeVar, Callable, Optional, Union import typing import warnings +from typing import Any, Callable, Optional, Type, TypeVar, Union -from .errors import InterfaceError, AdaptionError -from .interface import AnInterface, Interface, InterfaceType, type_is_interface -from .interface import get_type_interfaces, get_pi_attribute +from .errors import AdaptionError, InterfaceError +from .interface import ( + AnInterface, + Interface, + InterfaceType, + get_pi_attribute, + get_type_interfaces, + type_is_interface, +) def adapts(from_type: Any, to_interface: Optional[Type[Interface]] = None) -> Callable[[Any], Any]: @@ -35,9 +41,9 @@ def decorator(cls): if interfaces: interface = interfaces[0] elif isinstance(cls, type): - raise InterfaceError('Class {} does not provide any interfaces'.format(cls.__name__)) + raise InterfaceError("Class {} does not provide any interfaces".format(cls.__name__)) else: - raise InterfaceError('to_interface must be specified when decorating non-classes') + raise InterfaceError("to_interface must be specified when decorating non-classes") else: interface = to_interface register_adapter(cls, from_type, interface) @@ -46,15 +52,14 @@ def decorator(cls): return decorator -T = TypeVar('T') -U = TypeVar('U') # U can be a structural type so can't expect it to be a subclass of Interface +T = TypeVar("T") +U = TypeVar("U") # U can be a structural type so can't expect it to be a subclass of Interface def register_adapter( - adapter: Union[Callable[[T], U], Type[U]], - from_type: Type[T], - to_interface: Type[Interface]) -> None: - """ Registers adapter to convert instances of from_type to objects that provide to_interface + adapter: Union[Callable[[T], U], Type[U]], from_type: Type[T], to_interface: Type[Interface] +) -> None: + """Registers adapter to convert instances of from_type to objects that provide to_interface for the to_interface.adapt() method. :param adapter: callable that takes an instance of from_type and returns an object providing to_interface. @@ -62,44 +67,45 @@ def register_adapter( :param to_interface: an Interface class to adapt to. """ if not callable(adapter): - raise AdaptionError('adapter must be callable') + raise AdaptionError("adapter must be callable") if not isinstance(from_type, type): - raise AdaptionError('{} must be a type'.format(from_type)) - if not (isinstance(to_interface, type) and get_pi_attribute(to_interface, 'type_is_interface', False)): - raise AdaptionError('{} is not an interface'.format(to_interface)) - adapters = get_pi_attribute(to_interface, 'adapters') + raise AdaptionError("{} must be a type".format(from_type)) + if not (isinstance(to_interface, type) and get_pi_attribute(to_interface, "type_is_interface", False)): + raise AdaptionError("{} is not an interface".format(to_interface)) + adapters = get_pi_attribute(to_interface, "adapters") if from_type in adapters: - raise AdaptionError('{} already has an adapter to {}'.format(from_type, to_interface)) + raise AdaptionError("{} already has an adapter to {}".format(from_type, to_interface)) adapters[from_type] = adapter class AdapterTracker(object): - """ The idiom of checking if `x is b` is broken for adapted objects because a new adapter is potentially + """The idiom of checking if `x is b` is broken for adapted objects because a new adapter is potentially instantiated each time x or b is adapted. Also in some context we adapt the same objects many times and don't want the overhead of lots of copies. This class provides adapt() and adapt_or_none() methods that track adaptions. Thus if `x is b` is `True` then `adapter.adapt(x, I) is adapter.adapt(b, I)` is `True`. """ + def __init__(self, mapping_factory=dict): self._factory = mapping_factory self._adapters = mapping_factory() def adapt(self, obj: Any, interface: Type[AnInterface]) -> AnInterface: - """ Adapts `obj` to `interface`""" + """Adapts `obj` to `interface`""" try: return self._adapters[interface][obj] except KeyError: return self._adapt(obj, interface) def adapt_or_none(self, obj: Any, interface: Type[AnInterface]) -> Optional[AnInterface]: - """ Adapt obj to interface returning None on failure.""" + """Adapt obj to interface returning None on failure.""" try: return self.adapt(obj, interface) except ValueError: return None def clear(self) -> None: - """ Clears the cached adapters.""" + """Clears the cached adapters.""" self._adapters = self._factory() def _adapt(self, obj: Any, interface: Type[AnInterface]) -> AnInterface: @@ -113,7 +119,7 @@ def _adapt(self, obj: Any, interface: Type[AnInterface]) -> AnInterface: def _interface_from_anno(annotation: Any) -> Optional[InterfaceType]: - """ Typically the annotation is the interface, but if a default value of None is given the annotation is + """Typically the annotation is the interface, but if a default value of None is given the annotation is a Union[interface, None] a.k.a. Optional[interface]. Lets be nice and support those too. """ try: @@ -121,7 +127,7 @@ def _interface_from_anno(annotation: Any) -> Optional[InterfaceType]: return annotation except TypeError: pass - if hasattr(annotation, '__origin__') and hasattr(annotation, '__args__'): + if hasattr(annotation, "__origin__") and hasattr(annotation, "__args__"): # could be a Union if annotation.__origin__ is not Union: return None @@ -133,34 +139,35 @@ def _interface_from_anno(annotation: Any) -> Optional[InterfaceType]: def adapt_args(*func_arg, **kwarg_types): - """ adapts arguments to the decorated function to the types given. For example: + """adapts arguments to the decorated function to the types given. For example: - @adapt_args(foo=IFoo, bar=IBar) - def my_func(foo, bar): - pass + @adapt_args(foo=IFoo, bar=IBar) + def my_func(foo, bar): + pass - This would adapt the foo parameter to IFoo (with IFoo.optional_adapt(foo)) and bar to IBar (using IBar.adapt(bar)) - before passing them to my_func. `None` values are never adapted, so my_func(foo, None) will work, otherwise - AdaptionError is raised if the parameter is not adaptable. - All arguments must be specified as keyword arguments + This would adapt the foo parameter to IFoo (with IFoo.optional_adapt(foo)) and bar to IBar (using IBar.adapt(bar)) + before passing them to my_func. `None` values are never adapted, so my_func(foo, None) will work, otherwise + AdaptionError is raised if the parameter is not adaptable. + All arguments must be specified as keyword arguments - @adapt_args(IFoo, IBar) # NOT ALLOWED - def other_func(foo, bar): - pass + @adapt_args(IFoo, IBar) # NOT ALLOWED + def other_func(foo, bar): + pass - Parameters are only adapted if not None. This is useful for optional args: + Parameters are only adapted if not None. This is useful for optional args: - @adapt_args(foo=IFoo) - def optional_func(foo=None): - pass + @adapt_args(foo=IFoo) + def optional_func(foo=None): + pass - In Python 3 the types can be taken from the annotations. Optional[interface] is supported too. + In Python 3 the types can be taken from the annotations. Optional[interface] is supported too. - @adapt_args - def my_func(foo: IFoo, bar: Optional[IBar] = None): - pass + @adapt_args + def my_func(foo: IFoo, bar: Optional[IBar] = None): + pass """ + def decorator(func): @functools.wraps(func) def wrapped(*args, **kwargs): @@ -170,21 +177,24 @@ def wrapped(*args, **kwargs): adapted_kwargs[name] = InterfaceType.optional_adapt(interface, kwarg) return func(**adapted_kwargs) + return wrapped if func_arg: if len(func_arg) != 1: - raise AdaptionError('Only one posititional argument permitted') + raise AdaptionError("Only one posititional argument permitted") if not isinstance(func_arg[0], types.FunctionType): - raise AdaptionError('Positional argument must be a function (to decorate)') + raise AdaptionError("Positional argument must be a function (to decorate)") if kwarg_types: - raise AdaptionError('keyword parameters not permitted with positional argument') + raise AdaptionError("keyword parameters not permitted with positional argument") funcn = func_arg[0] annotations = typing.get_type_hints(funcn) if not annotations: - warnings.warn('No annotations for {}. ' - 'Add annotations or pass explicit argument types to adapt_args'.format(funcn.__name__), - stacklevel=2) + warnings.warn( + "No annotations for {}. " + "Add annotations or pass explicit argument types to adapt_args".format(funcn.__name__), + stacklevel=2, + ) for key, anno in annotations.items(): i_face = _interface_from_anno(anno) if i_face is not None: @@ -195,5 +205,5 @@ def wrapped(*args, **kwargs): i_face = typing.cast(InterfaceType, i_face) # keep mypy happy can_adapt = type_is_interface(i_face) if not can_adapt: - raise AdaptionError('adapt_args parameter values must be subtypes of Interface') + raise AdaptionError("adapt_args parameter values must be subtypes of Interface") return decorator diff --git a/pure_interface/delegation.py b/pure_interface/delegation.py index 965e058..c0ec3be 100644 --- a/pure_interface/delegation.py +++ b/pure_interface/delegation.py @@ -1,19 +1,25 @@ -from __future__ import division, absolute_import, print_function +from __future__ import absolute_import, division, print_function import operator -from typing import Dict, Union, Sequence, Type, Set, Tuple, Any, Optional, List +from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union from .errors import InterfaceError -from .interface import get_interface_names, type_is_interface, get_type_interfaces, AnInterfaceType, AnInterface +from .interface import ( + AnInterface, + AnInterfaceType, + get_interface_names, + get_type_interfaces, + type_is_interface, +) _composed_types_map: Dict[Tuple[Type, ...], Type] = {} -_letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' +_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" class _Delegated: def __init__(self, dotted_name: str): self._getter = operator.attrgetter(dotted_name) - self._impl_name, self._attr_name = dotted_name.rsplit('.', 1) + self._impl_name, self._attr_name = dotted_name.rsplit(".", 1) def __get__(self, obj, cls): if obj is None: @@ -26,7 +32,7 @@ def __set__(self, obj, value): class Delegate: - """ Mapping based delegate class + """Mapping based delegate class The class attribute pi_attr_delegates is a mapping of implmentation-name -> attr-name-list where implementation-name is the name of the attribute containing the implementation. @@ -110,6 +116,7 @@ def __init__(self, impl): self.foo = 3 """ + pi_attr_fallback: Optional[str] = None pi_attr_delegates: Dict[str, Union[List[str], type]] = {} pi_attr_mapping: Dict[str, Sequence[str]] = {} @@ -124,34 +131,34 @@ def i_have_attribute(attrib: str) -> bool: return True return False - for delegate, attr_list in cls.__dict__.get('pi_attr_delegates', {}).items(): + for delegate, attr_list in cls.__dict__.get("pi_attr_delegates", {}).items(): if isinstance(attr_list, type): attr_list = list(get_interface_names(attr_list)) if delegate in cls.pi_attr_mapping: - raise ValueError(f'Delegate {delegate} is in pi_attr_map') + raise ValueError(f"Delegate {delegate} is in pi_attr_map") for attr in attr_list: if attr in cls.pi_attr_mapping: - raise ValueError(f'{attr} in pi_attr_map and handled by delegate {delegate}') + raise ValueError(f"{attr} in pi_attr_map and handled by delegate {delegate}") if i_have_attribute(attr): continue - dotted_name = f'{delegate}.{attr}' + dotted_name = f"{delegate}.{attr}" setattr(cls, attr, _Delegated(dotted_name)) - for attr, dotted_name in cls.__dict__.get('pi_attr_mapping', {}).items(): + for attr, dotted_name in cls.__dict__.get("pi_attr_mapping", {}).items(): if not i_have_attribute(attr): setattr(cls, attr, _Delegated(dotted_name)) - fallback = cls.__dict__.get('pi_attr_fallback', None) + fallback = cls.__dict__.get("pi_attr_fallback", None) if fallback is not None: for interface in get_type_interfaces(cls): interface_names = get_interface_names(interface) for attr in interface_names: if not i_have_attribute(attr): - dotted_name = f'{fallback}.{attr}' + dotted_name = f"{fallback}.{attr}" setattr(cls, attr, _Delegated(dotted_name)) @classmethod def provided_by(cls, obj: Any): - if not hasattr(cls, 'pi_composed_interfaces'): - raise InterfaceError('provided_by() can only be called on composed types') + if not hasattr(cls, "pi_composed_interfaces"): + raise InterfaceError("provided_by() can only be called on composed types") if isinstance(obj, cls): return True other_mro = [c for c in type(obj).mro() if type_is_interface(c)] @@ -163,9 +170,9 @@ def provided_by(cls, obj: Any): def __composed_init__(self, *args): for i, impl in enumerate(args): - attr = '_' + _letters[i] + attr = "_" + _letters[i] if not isinstance(impl, type(self).pi_composed_interfaces[i]): - raise ValueError(f'Expected {type(self).pi_composed_interfaces[i]} got {type(impl)} instead') + raise ValueError(f"Expected {type(self).pi_composed_interfaces[i]} got {type(impl)} instead") setattr(self, attr, impl) @@ -192,9 +199,9 @@ class B(IB): t.bar -> 1 """ if len(interface_types) < 2: - raise ValueError('2 or more interfaces required') + raise ValueError("2 or more interfaces required") if len(interface_types) > len(_letters): - raise ValueError(f'Too many interfaces. Use {len(_letters)} or fewer.') + raise ValueError(f"Too many interfaces. Use {len(_letters)} or fewer.") interface_types = tuple(interface_types) c_type = _composed_types_map.get(interface_types) if c_type is not None: @@ -203,20 +210,21 @@ class B(IB): all_names: Set[str] = set() for i, interface in enumerate(interface_types): if not type_is_interface(interface): - raise ValueError('all arguments to composed_type must be Interface classes') - attr = '_' + _letters[i] + raise ValueError("all arguments to composed_type must be Interface classes") + attr = "_" + _letters[i] int_names = get_interface_names(interface) delegates[attr] = [name for name in int_names if name not in all_names] all_names.update(int_names) - name = ''.join((cls.__name__ for cls in interface_types)) - arg_names = ', '.join((cls.__name__.lower() for cls in interface_types)) + name = "".join((cls.__name__ for cls in interface_types)) + arg_names = ", ".join((cls.__name__.lower() for cls in interface_types)) bases = (Delegate,) + interface_types - cls_attrs = {'__init__': __composed_init__, - '__doc__': f'{name}({arg_names})', - 'pi_attr_delegates': delegates, - 'pi_composed_interfaces': interface_types, - } + cls_attrs = { + "__init__": __composed_init__, + "__doc__": f"{name}({arg_names})", + "pi_attr_delegates": delegates, + "pi_composed_interfaces": interface_types, + } c_type = type(name, bases, cls_attrs) _composed_types_map[interface_types] = c_type return c_type diff --git a/pure_interface/errors.py b/pure_interface/errors.py index 5f624dd..e737a85 100644 --- a/pure_interface/errors.py +++ b/pure_interface/errors.py @@ -1,14 +1,16 @@ - class PureInterfaceError(Exception): - """ All exceptions raised by this module are subclasses of this exception """ + """All exceptions raised by this module are subclasses of this exception""" + pass class InterfaceError(PureInterfaceError, TypeError): - """ An error with an interface class definition or implementation""" + """An error with an interface class definition or implementation""" + pass class AdaptionError(PureInterfaceError, ValueError): - """ An adaption error """ + """An adaption error""" + pass diff --git a/pure_interface/interface.py b/pure_interface/interface.py index 1e46e73..a21b188 100644 --- a/pure_interface/interface.py +++ b/pure_interface/interface.py @@ -1,27 +1,41 @@ """ pure_interface enforces empty functions and properties on interfaces and provides adaption and structural type checking. """ + from __future__ import absolute_import, division, print_function import abc -from abc import abstractclassmethod, abstractmethod, abstractstaticmethod import collections import dis import functools import inspect -from inspect import Parameter, signature, Signature import sys import types -from typing import Any, Callable, Dict, FrozenSet, Generic, Iterable, List, Optional, Set, Tuple, Type, TypeVar import warnings import weakref +from abc import abstractclassmethod, abstractmethod, abstractstaticmethod +from inspect import Parameter, Signature, signature +from typing import ( + Any, + Callable, + Dict, + FrozenSet, + Generic, + Iterable, + List, + Optional, + Set, + Tuple, + Type, + TypeVar, +) from .errors import AdaptionError, InterfaceError -is_development = not hasattr(sys, 'frozen') +is_development = not hasattr(sys, "frozen") missing_method_warnings: List[str] = [] -_T = TypeVar('_T') +_T = TypeVar("_T") def set_is_development(is_dev: bool) -> None: @@ -41,17 +55,20 @@ def no_adaption(obj: _T) -> _T: return obj -AnInterface = TypeVar('AnInterface', bound='Interface') -AnInterfaceType = TypeVar('AnInterfaceType', bound='InterfaceType') +AnInterface = TypeVar("AnInterface", bound="Interface") +AnInterfaceType = TypeVar("AnInterfaceType", bound="InterfaceType") class _PIAttributes: - """ rather than clutter the class namespace with lots of _pi_XXX attributes, collect them all here""" - - def __init__(self, this_type_is_an_interface: bool, - abstract_properties: Set[str], - interface_method_signatures: Dict[str, Signature], - interface_attribute_names: List[str]): + """rather than clutter the class namespace with lots of _pi_XXX attributes, collect them all here""" + + def __init__( + self, + this_type_is_an_interface: bool, + abstract_properties: Set[str], + interface_method_signatures: Dict[str, Signature], + interface_attribute_names: List[str], + ): self.type_is_interface: bool = this_type_is_an_interface # abstractproperties are checked for at instantiation. # When concrete classes use a @property then they are removed from this set @@ -78,10 +95,10 @@ def interface_names(self) -> FrozenSet[str]: class _ImplementationWrapper: def __init__(self, implementation: Any, interface: AnInterfaceType): - object.__setattr__(self, '_ImplementationWrapper__impl', implementation) - object.__setattr__(self, '_ImplementationWrapper__interface', interface) - object.__setattr__(self, '_ImplementationWrapper__interface_attrs', interface._pi.interface_names) - object.__setattr__(self, '_ImplementationWrapper__interface_name', interface.__name__) + object.__setattr__(self, "_ImplementationWrapper__impl", implementation) + object.__setattr__(self, "_ImplementationWrapper__interface", interface) + object.__setattr__(self, "_ImplementationWrapper__interface_attrs", interface._pi.interface_names) + object.__setattr__(self, "_ImplementationWrapper__interface_name", interface.__name__) def __getattr__(self, attr: str) -> Any: impl = self.__impl @@ -98,26 +115,39 @@ def __setattr__(self, key: str, value: Any) -> None: def _builtin_attrs(name: str) -> bool: - """ These attributes are ignored when checking ABC types for emptyness. - """ - return name in ('__doc__', '__module__', '__qualname__', '__abstractmethods__', '__dict__', - '__metaclass__', '__weakref__', '__subclasshook__', '__orig_bases__', - '_abc_cache', '_abc_impl', '_abc_registry', '_abc_negative_cache_version', '_abc_negative_cache', - '_pi', '_pi_unwrap_decorators') + """These attributes are ignored when checking ABC types for emptyness.""" + return name in ( + "__doc__", + "__module__", + "__qualname__", + "__abstractmethods__", + "__dict__", + "__metaclass__", + "__weakref__", + "__subclasshook__", + "__orig_bases__", + "_abc_cache", + "_abc_impl", + "_abc_registry", + "_abc_negative_cache_version", + "_abc_negative_cache", + "_pi", + "_pi_unwrap_decorators", + ) def get_pi_attribute(cls: Type, attr_name: str, default: Any = None) -> Any: - if hasattr(cls, '_pi'): + if hasattr(cls, "_pi"): return getattr(cls._pi, attr_name) else: return default def _type_is_interface(cls: type) -> bool: - """ Return True if cls is a pure interface or an empty ABC class""" + """Return True if cls is a pure interface or an empty ABC class""" if cls is object: return False - if hasattr(cls, '_pi'): + if hasattr(cls, "_pi"): return cls._pi.type_is_interface if cls is Generic: return True # this class is just for type hinting @@ -140,7 +170,7 @@ def _type_is_interface(cls: type) -> bool: def _get_abc_interface_props_and_funcs(cls: Type[abc.ABC]) -> Tuple[Set[str], Dict[str, Signature]]: properties: Set[str] = set() function_sigs: Dict[str, Signature] = {} - if not hasattr(cls, '__abstractmethods__'): + if not hasattr(cls, "__abstractmethods__"): return properties, function_sigs for name in cls.__abstractmethods__: if _builtin_attrs(name): @@ -158,16 +188,15 @@ def _get_abc_interface_props_and_funcs(cls: Type[abc.ABC]) -> Tuple[Set[str], Di def _unwrap_function(func: Any) -> Any: - """ Look for decorated functions and return the wrapped function. - """ - while hasattr(func, '__wrapped__'): + """Look for decorated functions and return the wrapped function.""" + while hasattr(func, "__wrapped__"): func = func.__wrapped__ return func def _is_empty_function(func: Any, unwrap: bool = False) -> bool: - """ Return True if func is considered empty. - All functions with no return statement have an implicit return None - this is explicit in the code object. + """Return True if func is considered empty. + All functions with no return statement have an implicit return None - this is explicit in the code object. """ if isinstance(func, (staticmethod, classmethod, types.MethodType)): func = func.__func__ @@ -183,76 +212,86 @@ def _is_empty_function(func: Any, unwrap: bool = False) -> bool: # quick check byte_code = code_obj.co_code - if byte_code.startswith(b'\x81\x01'): + if byte_code.startswith(b"\x81\x01"): byte_code = byte_code[2:] # remove GEN_START async def opcode - if byte_code.startswith(b'\x97\x00'): + if byte_code.startswith(b"\x97\x00"): byte_code = byte_code[2:] # remove RESUME opcode added in 3.11 - if byte_code.startswith(b'\t\x00'): + if byte_code.startswith(b"\t\x00"): byte_code = byte_code[2:] # remove NOP opcode - if byte_code.startswith(b'K\x00'): + if byte_code.startswith(b"K\x00"): byte_code = byte_code[2:] # remove RETURN_GENERATOR async def opcode in py311 - if byte_code.startswith(b'\x01'): + if byte_code.startswith(b"\x01"): byte_code = byte_code[2:] # remove POP_TOP - if byte_code.startswith(b'\x97\x00'): + if byte_code.startswith(b"\x97\x00"): byte_code = byte_code[2:] # remove RESUME opcode added in 3.11 - if byte_code.startswith(b'\t\x00'): + if byte_code.startswith(b"\t\x00"): byte_code = byte_code[2:] # remove NOP opcode - if byte_code in (b'd\x00\x00S', b'd\x00S\x00') and code_obj.co_consts[0] is None: + if byte_code in (b"d\x00\x00S", b"d\x00S\x00") and code_obj.co_consts[0] is None: return True - if byte_code in (b'd\x01\x00S', b'd\x01S\x00') and code_obj.co_consts[1] is None: + if byte_code in (b"d\x01\x00S", b"d\x01S\x00") and code_obj.co_consts[1] is None: return True - if byte_code == b'y\x00' and code_obj.co_consts[0] is None: # RETURN_CONST in 3.12+ + if byte_code == b"y\x00" and code_obj.co_consts[0] is None: # RETURN_CONST in 3.12+ return True # convert bytes to instructions instructions = list(dis.get_instructions(code_obj)) if len(instructions) < 2: return True # this never happens - if instructions[0].opname == 'GEN_START': + if instructions[0].opname == "GEN_START": instructions.pop(0) - if instructions[0].opname == 'RESUME': + if instructions[0].opname == "RESUME": instructions.pop(0) - if instructions[0].opname == 'NOP': + if instructions[0].opname == "NOP": instructions.pop(0) - if instructions[0].opname == 'RETURN_GENERATOR': + if instructions[0].opname == "RETURN_GENERATOR": instructions.pop(0) - if instructions[0].opname == 'POP_TOP': + if instructions[0].opname == "POP_TOP": instructions.pop(0) # All generator functions end with these 2 opcodes in 3.12+ - if (len(instructions) > 2 and - instructions[-2].opname == 'CALL_INTRINSIC_1' and - instructions[-1].opname == 'RERAISE'): + if ( + len(instructions) > 2 + and instructions[-2].opname == "CALL_INTRINSIC_1" + and instructions[-1].opname == "RERAISE" + ): instructions = instructions[:-2] # remove last 2 instructions - if instructions[0].opname == 'RESUME': + if instructions[0].opname == "RESUME": instructions.pop(0) - if instructions[0].opname == 'NOP': + if instructions[0].opname == "NOP": instructions.pop(0) - if instructions[-1].opname == 'RETURN_VALUE': # returns TOS (top of stack) + if instructions[-1].opname == "RETURN_VALUE": # returns TOS (top of stack) instruction = instructions[-2] - if not (instruction.opname == 'LOAD_CONST' and code_obj.co_consts[instruction.arg] is None): # TOS is None + if not (instruction.opname == "LOAD_CONST" and code_obj.co_consts[instruction.arg] is None): # TOS is None return False # return is not None instructions = instructions[:-2] - if len(instructions) > 0 and instructions[-1].opname == 'RETURN_CONST' and instructions[-1].argval is None: # returns constant + if ( + len(instructions) > 0 and instructions[-1].opname == "RETURN_CONST" and instructions[-1].argval is None + ): # returns constant instructions.pop(-1) if len(instructions) == 0: return True # look for raise NotImplementedError - if instructions[-1].opname == 'RAISE_VARARGS': + if instructions[-1].opname == "RAISE_VARARGS": # the thing we are raising should be the result of __call__ (instantiating exception object) - if instructions[-2].opname in ('CALL_FUNCTION', 'CALL'): + if instructions[-2].opname in ("CALL_FUNCTION", "CALL"): for instr in instructions[-3::-1]: - if instr.opname == 'LOAD_GLOBAL': - return bool(instr.argval == 'NotImplementedError') + if instr.opname == "LOAD_GLOBAL": + return bool(instr.argval == "NotImplementedError") return False def _is_descriptor(obj: Any) -> bool: # in our context we only care about __get__ - return hasattr(obj, '__get__') + return hasattr(obj, "__get__") class _ParamTypes: - def __init__(self, pos_only: List[Parameter], pos_or_kw: List[Parameter], - vararg: List[Parameter], kw_only: List[Parameter], varkw: List[Parameter]): + def __init__( + self, + pos_only: List[Parameter], + pos_or_kw: List[Parameter], + vararg: List[Parameter], + kw_only: List[Parameter], + varkw: List[Parameter], + ): self.pos_only = pos_only self.pos_or_kw = pos_or_kw self.vararg = vararg @@ -267,12 +306,13 @@ def _signature_info(arg_spec: Iterable[Parameter]) -> _ParamTypes: for param in arg_spec: param_types[param.kind].append(param) - return _ParamTypes(param_types[Parameter.POSITIONAL_ONLY], - param_types[Parameter.POSITIONAL_OR_KEYWORD], - param_types[Parameter.VAR_POSITIONAL], - param_types[Parameter.KEYWORD_ONLY], - param_types[Parameter.VAR_KEYWORD] - ) + return _ParamTypes( + param_types[Parameter.POSITIONAL_ONLY], + param_types[Parameter.POSITIONAL_OR_KEYWORD], + param_types[Parameter.VAR_POSITIONAL], + param_types[Parameter.KEYWORD_ONLY], + param_types[Parameter.VAR_KEYWORD], + ) def _positional_args_match(func_list, base_list, vararg, base_kwo): @@ -283,7 +323,7 @@ def _positional_args_match(func_list, base_list, vararg, base_kwo): return False # extra parameters must be have defaults (be optional) base_kwo = [p.name for p in base_kwo] - for p in func_list[len(base_list):]: + for p in func_list[len(base_list) :]: if p.default is Parameter.empty and p.name not in base_kwo: return False return True @@ -363,12 +403,12 @@ def _ensure_everything_is_abstract(attributes): for name, value in attributes.items(): if _builtin_attrs(name): pass # shortcut - elif name == '__annotations__': + elif name == "__annotations__": interface_attribute_names.extend(value.keys()) elif value is None: interface_attribute_names.append(name) continue # do not add to class namespace - elif getattr(value, '__isabstractmethod__', False): + elif getattr(value, "__isabstractmethod__", False): if isinstance(value, (staticmethod, classmethod, types.FunctionType)): if isinstance(value, (staticmethod, classmethod)): func = value.__func__ @@ -403,7 +443,7 @@ def _ensure_everything_is_abstract(attributes): functions.extend([value.fget, value.fset, value.fdel]) # may contain Nones continue # do not add to class namespace else: - raise InterfaceError('Interface class attributes must have a value of None\n{}={}'.format(name, value)) + raise InterfaceError("Interface class attributes must have a value of None\n{}={}".format(name, value)) namespace[name] = value return namespace, functions, interface_method_signatures, interface_attribute_names @@ -414,16 +454,16 @@ def _ensure_annotations(names, namespace, base_interfaces): annotations: Dict[str, Any] = {} base_annos: Dict[str, Any] = {} for base in reversed(base_interfaces): - base_annos.update(getattr(base, '__annotations__', {})) + base_annos.update(getattr(base, "__annotations__", {})) for name in names: if name not in annotations and name not in namespace: annotations[name] = base_annos.get(name, Any) - annotations.update(namespace.get('__annotations__', {})) - namespace['__annotations__'] = annotations + annotations.update(namespace.get("__annotations__", {})) + namespace["__annotations__"] = annotations def _check_method_signatures(attributes, clsname, interface_method_signatures): - """ Scan attributes dict for interface method overrides and check the function signatures are consistent """ + """Scan attributes dict for interface method overrides and check the function signatures are consistent""" for name, base_sig in interface_method_signatures.items(): if name not in attributes: continue @@ -432,7 +472,7 @@ def _check_method_signatures(attributes, clsname, interface_method_signatures): if _is_descriptor(value): continue else: - raise InterfaceError('Interface method over-ridden with non-method') + raise InterfaceError("Interface method over-ridden with non-method") if isinstance(value, (staticmethod, classmethod)): func = value.__func__ elif isinstance(value, functools.singledispatchmethod): @@ -441,8 +481,9 @@ def _check_method_signatures(attributes, clsname, interface_method_signatures): func = value func_sig = signature(func) if not _signatures_are_consistent(func_sig, base_sig): - msg = '{module}.{clsname}.{name} arguments do not match base method'.format( - module=attributes['__module__'], clsname=clsname, name=name) + msg = "{module}.{clsname}.{name} arguments do not match base method".format( + module=attributes["__module__"], clsname=clsname, name=name + ) raise InterfaceError(msg) @@ -450,15 +491,15 @@ def _do_missing_impl_warnings(cls, clsname): stacklevel = 2 stack = inspect.stack() # walk up stack until we get out of pure_interface module - while stacklevel < len(stack) and 'pure_interface' in stack[stacklevel][1]: + while stacklevel < len(stack) and "pure_interface" in stack[stacklevel][1]: stacklevel += 1 # add extra levels for sub-meta-classes stack.pop(0) - while stack and stack[0][0].f_code.co_name == '__new__': + while stack and stack[0][0].f_code.co_name == "__new__": stacklevel += 1 stack.pop(0) for method_name in cls.__abstractmethods__: - message = 'Incomplete Implementation: {clsname} does not implement {method_name}' + message = "Incomplete Implementation: {clsname} does not implement {method_name}" message = message.format(clsname=clsname, method_name=method_name) missing_method_warnings.append(message) warnings.warn(message, stacklevel=stacklevel) @@ -492,18 +533,20 @@ def _class_structural_type_check(cls, subclass): if is_development: stacklevel = 2 stack = inspect.stack() - while stacklevel < len(stack) and 'pure_interface' in stack[stacklevel - 1][1]: + while stacklevel < len(stack) and "pure_interface" in stack[stacklevel - 1][1]: stacklevel += 1 - warnings.warn('Class {module}.{sub_name} implements {cls_name}.\n' - 'Consider inheriting {cls_name} or using {cls_name}.register({sub_name})' - .format(cls_name=cls.__name__, sub_name=subclass.__name__, module=subclass.__module__), - stacklevel=stacklevel) + warnings.warn( + "Class {module}.{sub_name} implements {cls_name}.\n" + "Consider inheriting {cls_name} or using {cls_name}.register({sub_name})".format( + cls_name=cls.__name__, sub_name=subclass.__name__, module=subclass.__module__ + ), + stacklevel=stacklevel, + ) return True def _get_adapter(cls: AnInterfaceType, obj_type: Type) -> Optional[Callable]: - """ Returns a callable that adapts objects of type obj_type to this interface or None if no adapter exists. - """ + """Returns a callable that adapts objects of type obj_type to this interface or None if no adapter exists.""" adapters = {} # type: ignore # registered interfaces can come from cls.register(AnotherInterface) or @sub_interface_of(AnotherInterface)(cls) candidate_interfaces = [cls] + cls.__subclasses__() + list(cls._pi.registered_types) @@ -533,11 +576,12 @@ class InterfaceType(abc.ABCMeta): * optionally check overriding method signatures match those on base class. * if the type is a concrete class then patch the abstract properties with AttributeProperies. """ + _pi: _PIAttributes def __new__(mcs, clsname, bases, attributes, **kwargs): # Interface is not in globals() when we are constructing the Interface class itself. - has_interface = any(Interface in base.mro() for base in bases) if 'Interface' in globals() else True + has_interface = any(Interface in base.mro() for base in bases) if "Interface" in globals() else True if not has_interface: # Don't interfere if meta class is only included to permit interface inheritance, # but no actual interface is being used. @@ -547,13 +591,13 @@ def __new__(mcs, clsname, bases, attributes, **kwargs): base_types = [(cls, _type_is_interface(cls)) for cls in bases] - if clsname == 'Interface' and attributes.get('__module__', '') == 'pure_interface.interface': + if clsname == "Interface" and attributes.get("__module__", "") == "pure_interface.interface": this_type_is_an_interface = True else: - assert 'Interface' in globals() + assert "Interface" in globals() this_type_is_an_interface = Interface in bases if this_type_is_an_interface and not all(is_interface for cls, is_interface in base_types): - raise InterfaceError('All bases must be interface types when declaring an interface') + raise InterfaceError("All bases must be interface types when declaring an interface") interface_method_signatures = dict() interface_attribute_names = list() abstract_properties = set() @@ -561,12 +605,12 @@ def __new__(mcs, clsname, bases, attributes, **kwargs): base, base_is_interface = base_types[i] if base is object: continue - base_abstract_properties = get_pi_attribute(base, 'abstractproperties', set()) + base_abstract_properties = get_pi_attribute(base, "abstractproperties", set()) abstract_properties.update(base_abstract_properties) if base_is_interface: - if hasattr(base, '_pi'): - method_signatures = get_pi_attribute(base, 'interface_method_signatures', {}) - attribute_names = get_pi_attribute(base, 'interface_attribute_names', []) + if hasattr(base, "_pi"): + method_signatures = get_pi_attribute(base, "interface_method_signatures", {}) + attribute_names = get_pi_attribute(base, "interface_attribute_names", []) else: attribute_names, method_signatures = _get_abc_interface_props_and_funcs(base) interface_method_signatures.update(method_signatures) @@ -584,7 +628,7 @@ def __new__(mcs, clsname, bases, attributes, **kwargs): _ensure_annotations(interface_attribute_names, attributes, base_interfaces) if this_type_is_an_interface: - if clsname == 'Interface' and attributes.get('__module__', '') == 'pure_interface.interface': + if clsname == "Interface" and attributes.get("__module__", "") == "pure_interface.interface": namespace = attributes functions = [] method_signatures = {} @@ -595,13 +639,12 @@ def __new__(mcs, clsname, bases, attributes, **kwargs): interface_method_signatures.update(method_signatures) interface_attribute_names.extend(attribute_names) abstract_properties.update(interface_attribute_names) - unwrap = getattr(mcs, '_pi_unwrap_decorators', False) + unwrap = getattr(mcs, "_pi_unwrap_decorators", False) for func in functions: if func is None: continue if not _is_empty_function(func, unwrap): - raise InterfaceError('Interface method "{}.{}" must be empty.'.format( - clsname, func.__name__)) + raise InterfaceError('Interface method "{}.{}" must be empty.'.format(clsname, func.__name__)) else: # concrete sub-type namespace = attributes class_properties = set() @@ -610,16 +653,20 @@ def __new__(mcs, clsname, bases, attributes, **kwargs): class_properties |= set(k for k, v in bt.__dict__.items() if _is_descriptor(v)) class_properties |= set(k for k, v in namespace.items() if _is_descriptor(v)) abstract_properties.difference_update(class_properties) - partial_implementation = 'pi_partial_implementation' in namespace + partial_implementation = "pi_partial_implementation" in namespace if partial_implementation: - value = namespace.pop('pi_partial_implementation') + value = namespace.pop("pi_partial_implementation") if not value: - warnings.warn('Partial implementation is indicated by presence of ' - 'pi_partial_implementation attribute, not it''s value') + warnings.warn( + "Partial implementation is indicated by presence of " + "pi_partial_implementation attribute, not it" + "s value" + ) # create class - namespace['_pi'] = _PIAttributes(this_type_is_an_interface, abstract_properties, - interface_method_signatures, interface_attribute_names) + namespace["_pi"] = _PIAttributes( + this_type_is_an_interface, abstract_properties, interface_method_signatures, interface_attribute_names + ) cls = super(InterfaceType, mcs).__new__(mcs, clsname, bases, namespace, **kwargs) # warnings @@ -629,9 +676,9 @@ def __new__(mcs, clsname, bases, attributes, **kwargs): return cls def __call__(cls, *args, **kwargs): - """ Check that abstract properties are created in constructor """ + """Check that abstract properties are created in constructor""" if cls._pi.type_is_interface: - raise InterfaceError('Interfaces cannot be instantiated') + raise InterfaceError("Interfaces cannot be instantiated") self = super(InterfaceType, cls).__call__(*args, **kwargs) for attr in cls._pi.abstractproperties: if not (hasattr(cls, attr) or hasattr(self, attr)): @@ -650,7 +697,7 @@ def provided_by(cls, obj): def _provided_by(cls, obj, allow_implicit=True): if not cls._pi.type_is_interface: - raise InterfaceError('provided_by() can only be called on interfaces') + raise InterfaceError("provided_by() can only be called on interfaces") if isinstance(obj, cls): return True if not allow_implicit: @@ -661,10 +708,10 @@ def _provided_by(cls, obj, allow_implicit=True): def interface_only(cls, implementation): if cls._pi.impl_wrapper_type is None: - type_name = '_{}Only'.format(cls.__name__) - attributes = {'__module__': cls.__module__} - if '__call__' in cls._pi.interface_names: - attributes['__call__'] = getattr(implementation, '__call__') + type_name = "_{}Only".format(cls.__name__) + attributes = {"__module__": cls.__module__} + if "__call__" in cls._pi.interface_names: + attributes["__call__"] = getattr(implementation, "__call__") cls._pi.impl_wrapper_type = type(type_name, (_ImplementationWrapper,), attributes) abc.ABCMeta.register(cls, cls._pi.impl_wrapper_type) return cls._pi.impl_wrapper_type(implementation, cls) @@ -674,17 +721,17 @@ def adapt(cls, obj, allow_implicit=False, interface_only=None): interface_only = is_development if isinstance(obj, _ImplementationWrapper): obj = obj._ImplementationWrapper__impl - adapter: Optional[Callable[[Any], 'InterfaceType']] + adapter: Optional[Callable[[Any], "InterfaceType"]] if InterfaceType._provided_by(cls, obj, allow_implicit=allow_implicit): adapter = no_adaption else: adapter = _get_adapter(cls, type(obj)) if adapter is None: - raise AdaptionError('Cannot adapt {} to {}'.format(obj, cls.__name__)) + raise AdaptionError("Cannot adapt {} to {}".format(obj, cls.__name__)) adapted = adapter(obj) if not InterfaceType._provided_by(cls, adapted, allow_implicit): - raise AdaptionError('Adapter {} does not implement interface {}'.format(adapter, cls.__name__)) + raise AdaptionError("Adapter {} does not implement interface {}".format(adapter, cls.__name__)) if interface_only: adapted = InterfaceType.interface_only(cls, adapted) return adapted @@ -728,19 +775,19 @@ class Interface(abc.ABC, metaclass=InterfaceType): @classmethod def provided_by(cls, obj) -> bool: - """ Returns True if obj provides this interface (structural type-check). - """ + """Returns True if obj provides this interface (structural type-check).""" return InterfaceType.provided_by(cls, obj) @classmethod def interface_only(cls: Type[AnInterface], implementation: AnInterface) -> AnInterface: - """ Returns a wrapper around implementation that provides ONLY this interface. """ + """Returns a wrapper around implementation that provides ONLY this interface.""" return InterfaceType.interface_only(cls, implementation) @classmethod - def adapt(cls: Type[AnInterface], obj: Any, - allow_implicit: bool = False, interface_only: Optional[bool] = None) -> AnInterface: - """ Adapts obj to interface, returning obj if to_interface.provided_by(obj, allow_implicit) is True + def adapt( + cls: Type[AnInterface], obj: Any, allow_implicit: bool = False, interface_only: Optional[bool] = None + ) -> AnInterface: + """Adapts obj to interface, returning obj if to_interface.provided_by(obj, allow_implicit) is True and raising ValueError if no adapter is found If interface_only is True, or interface_only is None and is_development is True then the returned object is wrapped by an object that only provides the methods and properties defined by to_interface. @@ -748,44 +795,46 @@ def adapt(cls: Type[AnInterface], obj: Any, return InterfaceType.adapt(cls, obj, allow_implicit=allow_implicit, interface_only=interface_only) @classmethod - def adapt_or_none(cls: Type[AnInterface], obj, - allow_implicit: bool = False, interface_only: Optional[bool] = None) -> Optional[AnInterface]: - """ Adapt obj to to_interface or return None if adaption fails """ + def adapt_or_none( + cls: Type[AnInterface], obj, allow_implicit: bool = False, interface_only: Optional[bool] = None + ) -> Optional[AnInterface]: + """Adapt obj to to_interface or return None if adaption fails""" return InterfaceType.adapt_or_none(cls, obj, allow_implicit=allow_implicit, interface_only=interface_only) @classmethod def can_adapt(cls, obj, allow_implicit: bool = False) -> bool: - """ Returns True if adapt(obj, allow_implicit) will succeed.""" + """Returns True if adapt(obj, allow_implicit) will succeed.""" return InterfaceType.can_adapt(cls, obj, allow_implicit=allow_implicit) @classmethod - def filter_adapt(cls: Type[AnInterface], objects: Iterable, - allow_implicit: bool = False, interface_only: Optional[bool] = None) -> Iterable[AnInterface]: - """ Generates adaptions of the given objects to this interface. + def filter_adapt( + cls: Type[AnInterface], objects: Iterable, allow_implicit: bool = False, interface_only: Optional[bool] = None + ) -> Iterable[AnInterface]: + """Generates adaptions of the given objects to this interface. Objects that cannot be adapted to this interface are silently skipped. """ - return InterfaceType.filter_adapt(cls, objects, allow_implicit=allow_implicit, - interface_only=interface_only) + return InterfaceType.filter_adapt(cls, objects, allow_implicit=allow_implicit, interface_only=interface_only) @classmethod - def optional_adapt(cls: Type[AnInterface], obj, - allow_implicit: bool = False, interface_only: Optional[bool] = None) -> Optional[AnInterface]: - """ Adapt obj to to_interface or return None if adaption fails """ + def optional_adapt( + cls: Type[AnInterface], obj, allow_implicit: bool = False, interface_only: Optional[bool] = None + ) -> Optional[AnInterface]: + """Adapt obj to to_interface or return None if adaption fails""" return InterfaceType.optional_adapt(cls, obj, allow_implicit=allow_implicit, interface_only=interface_only) def type_is_interface(cls: Type) -> bool: # -> TypeGuard[AnInterfaceType] - """ Return True if cls is a pure interface""" + """Return True if cls is a pure interface""" try: if not issubclass(cls, Interface): return False except TypeError: # handle non-classes return False - return get_pi_attribute(cls, 'type_is_interface', False) + return get_pi_attribute(cls, "type_is_interface", False) def get_type_interfaces(cls: Type) -> List[type]: - """ Returns all interfaces in the cls mro including cls itself if it is an interface """ + """Returns all interfaces in the cls mro including cls itself if it is an interface""" try: bases = cls.mro() except AttributeError: # handle non-classes @@ -795,31 +844,30 @@ def get_type_interfaces(cls: Type) -> List[type]: def get_interface_names(interface: Type) -> FrozenSet[str]: - """ returns a frozen set of names (methods and attributes) defined by the interface. + """returns a frozen set of names (methods and attributes) defined by the interface. if interface is not a Interface subtype then an empty set is returned. """ if type_is_interface(interface): - return get_pi_attribute(interface, 'interface_names') + return get_pi_attribute(interface, "interface_names") else: return frozenset() def get_interface_method_names(interface: Type) -> FrozenSet[str]: - """ returns a frozen set of names of methods defined by the interface. + """returns a frozen set of names of methods defined by the interface. if interface is not a Interface subtype then an empty set is returned """ if type_is_interface(interface): - return get_pi_attribute(interface, 'interface_method_names') + return get_pi_attribute(interface, "interface_method_names") else: return frozenset() def get_interface_attribute_names(interface: Type) -> FrozenSet[str]: - """ returns a frozen set of names of attributes defined by the interface + """returns a frozen set of names of attributes defined by the interface if interface is not a Interface subtype then an empty set is returned """ if type_is_interface(interface): - return frozenset(get_pi_attribute(interface, 'interface_attribute_names', ())) + return frozenset(get_pi_attribute(interface, "interface_attribute_names", ())) else: return frozenset() - diff --git a/pure_interface/mypy_plugin.py b/pure_interface/mypy_plugin.py index fbf1c47..6780acc 100644 --- a/pure_interface/mypy_plugin.py +++ b/pure_interface/mypy_plugin.py @@ -1,13 +1,15 @@ from collections.abc import Callable from typing import Dict, Final, List, Optional, TypeAlias, cast -from mypy import nodes, types, plugin as mypy_plugin +from mypy import nodes +from mypy import plugin as mypy_plugin +from mypy import types from mypy.plugins import common -INTERFACE_FN: Final = 'pure_interface.interface.Interface' -DELEGATE_FN: Final = 'pure_interface.delegation.Delegate' -METADATA_KEY: Final = 'pure-interface' -IS_INTERFACE_KEY: Final = 'is-interface' +INTERFACE_FN: Final = "pure_interface.interface.Interface" +DELEGATE_FN: Final = "pure_interface.delegation.Delegate" +METADATA_KEY: Final = "pure-interface" +IS_INTERFACE_KEY: Final = "is-interface" NOT_ANNOTATED = types.AnyType(types.TypeOfAny.unannotated) @@ -34,8 +36,11 @@ def get_type_interfaces(class_def: nodes.ClassDef) -> List[nodes.ClassDef]: def get_interface_bases(class_def: nodes.ClassDef) -> List[nodes.TypeInfo]: - interface_list = [getattr(expr, 'node') for expr in class_def.base_type_exprs - if type_info_is_interface(getattr(expr, 'node', None))] + interface_list = [ + getattr(expr, "node") + for expr in class_def.base_type_exprs + if type_info_is_interface(getattr(expr, "node", None)) + ] return interface_list @@ -56,12 +61,12 @@ def get_all_interface_info(interfaces: List[nodes.ClassDef]) -> InterfaceInfo: def get_return_type(func_type): - r_type = getattr(func_type, 'ret_type', None) + r_type = getattr(func_type, "ret_type", None) return r_type or NOT_ANNOTATED def ensure_names(context: mypy_plugin.ClassDefContext, delegate: nodes.ClassDef, interface_info: InterfaceInfo) -> bool: - """Adds names to the delegate class """ + """Adds names to the delegate class""" info = delegate.info changed = False for name, s_node in interface_info.items(): @@ -77,11 +82,19 @@ def ensure_names(context: mypy_plugin.ClassDefContext, delegate: nodes.ClassDef, common.add_method_to_class(context.api, delegate, name, args, get_return_type(value.type)) elif type(value) is nodes.Decorator: decorators = value.original_decorators - is_property = any(d for d in decorators if getattr(d, 'fullname', None) in ('builtins.property', 'abc.abstractproperty')) - is_classmethod = any(d for d in decorators - if getattr(d, 'fullname', None) in ('builtins.classmethod', 'abc.abstractclassmethod')) - is_staticmethod = any(d for d in decorators - if getattr(d, 'fullname', None) in ('builtins.staticmethod', 'abc.abstractstaticmethod')) + is_property = any( + d for d in decorators if getattr(d, "fullname", None) in ("builtins.property", "abc.abstractproperty") + ) + is_classmethod = any( + d + for d in decorators + if getattr(d, "fullname", None) in ("builtins.classmethod", "abc.abstractclassmethod") + ) + is_staticmethod = any( + d + for d in decorators + if getattr(d, "fullname", None) in ("builtins.staticmethod", "abc.abstractstaticmethod") + ) r_type = get_return_type(value.func.type) if is_property: common.add_attribute_to_class(context.api, delegate, value.name, r_type) @@ -89,8 +102,15 @@ def ensure_names(context: mypy_plugin.ClassDefContext, delegate: nodes.ClassDef, args = ensure_type_annotations(value.func.arguments) if not is_staticmethod: args = args[1:] - common.add_method_to_class(context.api, delegate, name, args, r_type, - is_classmethod=is_classmethod, is_staticmethod=is_staticmethod) + common.add_method_to_class( + context.api, + delegate, + name, + args, + r_type, + is_classmethod=is_classmethod, + is_staticmethod=is_staticmethod, + ) return changed @@ -108,7 +128,7 @@ def ensure_type_annotations(arguments): def _handle_pi_attr_fallback(context, class_def): interface_list = get_type_interfaces(class_def) if len(interface_list) == 0: - context.api.fail('pi_attr_fallback requires an Interface.', class_def) + context.api.fail("pi_attr_fallback requires an Interface.", class_def) return interface_info = get_all_interface_info(interface_list) ensure_names(context, class_def, interface_info) @@ -116,7 +136,7 @@ def _handle_pi_attr_fallback(context, class_def): def _handle_pi_attr_delegates(context, delegate, rvalue): if not isinstance(rvalue, nodes.DictExpr): - context.api.fail('pi_attr_delegates must be a dictionary.', delegate) + context.api.fail("pi_attr_delegates must be a dictionary.", delegate) return for _, expr in rvalue.items: if type(expr) is nodes.ListExpr: @@ -128,7 +148,7 @@ def _handle_pi_attr_delegates(context, delegate, rvalue): # interface class type_info = expr.node if not type_info_is_interface(type_info): - context.api.fail('pi_attr_delegates values must be interface type') + context.api.fail("pi_attr_delegates values must be interface type") return type_info = cast(nodes.TypeInfo, type_info) interface_info = get_interface_info(type_info.defn) @@ -137,17 +157,16 @@ def _handle_pi_attr_delegates(context, delegate, rvalue): def _handle_pi_attr_mapping(context, delegate, rvalue): if not isinstance(rvalue, nodes.DictExpr): - context.api.fail('pi_attr_mapping must be a dictionary.', delegate) + context.api.fail("pi_attr_mapping must be a dictionary.", delegate) return for expr, _ in rvalue.items: if not isinstance(expr, nodes.StrExpr): - context.api.fail('pi_attr_mapping keys must be strings.', delegate) + context.api.fail("pi_attr_mapping keys must be strings.", delegate) return common.add_attribute_to_class(context.api, delegate, expr.value, NOT_ANNOTATED) class PureInterfacePlugin(mypy_plugin.Plugin): - def get_base_class_hook(self, fullname: str) -> Optional[Callable[[mypy_plugin.ClassDefContext], None]]: if fullname == INTERFACE_FN: return self._mark_class_as_interface @@ -155,10 +174,8 @@ def get_base_class_hook(self, fullname: str) -> Optional[Callable[[mypy_plugin.C return self._create_delegate_attributes return None - def get_class_decorator_hook( - self, fullname: str - ) -> Optional[Callable[[mypy_plugin.ClassDefContext], None]]: - if fullname == 'dataclasses.dataclass': + def get_class_decorator_hook(self, fullname: str) -> Optional[Callable[[mypy_plugin.ClassDefContext], None]]: + if fullname == "dataclasses.dataclass": return self._create_dataclass_attributes return None @@ -181,11 +198,11 @@ def _create_delegate_attributes(context: mypy_plugin.ClassDefContext): if not (len(item.lvalues) == 1 and isinstance(item.lvalues[0], nodes.NameExpr)): continue name = item.lvalues[0].fullname - if name == 'pi_attr_fallback': + if name == "pi_attr_fallback": _handle_pi_attr_fallback(context, class_def) - elif name == 'pi_attr_delegates': + elif name == "pi_attr_delegates": _handle_pi_attr_delegates(context, class_def, item.rvalue) - elif name == 'pi_attr_mapping': + elif name == "pi_attr_mapping": _handle_pi_attr_mapping(context, class_def, item.rvalue) @staticmethod diff --git a/pyproject.toml b/pyproject.toml index ed5a465..054ef09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ Homepage = "https://pypi.org/project/pure-interface/" Repository = "https://github.com/seequent/pure_interface" [project.optional-dependencies] -dev = ['mypy >= 1.4'] +dev = ['mypy >= 1.4', 'tox'] typing = ['mypy'] [build-system] @@ -38,3 +38,9 @@ version = {attr = "pure_interface.__version__"} [tool.mypy] plugins = "pure_interface/mypy_plugin.py" + +[tool.black] +line-length = 120 + +[tool.isort] +profile = "black" diff --git a/tests/__init__.py b/tests/__init__.py index c03795c..12be00c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,3 @@ import warnings -warnings.filterwarnings('ignore', category=UserWarning) +warnings.filterwarnings("ignore", category=UserWarning) diff --git a/tests/mypy_source.py b/tests/mypy_source.py index a8cb3d3..61c75dc 100644 --- a/tests/mypy_source.py +++ b/tests/mypy_source.py @@ -3,10 +3,10 @@ This is working except for dataclasses __init__ call arguments. """ +import abc import dataclasses import pure_interface -import abc class BaseInterface(pure_interface.Interface): @@ -49,12 +49,12 @@ def height(self, height): def weight(self, arg: int) -> str: if arg > 0: - return '3'*arg + return "3" * arg else: - return '' + return "" def speak(self, volume) -> str: - return 'hello' + return "hello" @classmethod def a_class_method(cls): @@ -68,10 +68,10 @@ def a_static_method() -> int: @dataclasses.dataclass class DC(MyInterface): def weight(self, arg: int) -> str: - return f'{arg}' + return f"{arg}" def speak(self, volume) -> str: - return 'hello' + return "hello" @classmethod def a_class_method(cls): @@ -83,15 +83,14 @@ def a_static_method() -> int: class FBDelegate(pure_interface.Delegate, MyInterface): - pi_attr_fallback = '_a' + pi_attr_fallback = "_a" def __init__(self, impl) -> None: self._a = impl class AttrListDelegate(pure_interface.Delegate): - pi_attr_delegates = {'a': ['height', 'weight'], - 'b': ['speak']} + pi_attr_delegates = {"a": ["height", "weight"], "b": ["speak"]} def __init__(self, impl) -> None: self.a = impl @@ -99,15 +98,14 @@ def __init__(self, impl) -> None: class AttrTypeDelegate(pure_interface.Delegate): - pi_attr_delegates = {'a': BaseInterface} + pi_attr_delegates = {"a": BaseInterface} def __init__(self, impl) -> None: self.a = impl class MappingDelegate(pure_interface.Delegate): - pi_attr_mapping = {'wibble': 'a.speak', - 'bar': 'a.foo'} + pi_attr_mapping = {"wibble": "a.speak", "bar": "a.foo"} def __init__(self, impl) -> None: self.a = impl @@ -115,8 +113,8 @@ def __init__(self, impl) -> None: t = MyThing() fbd = FBDelegate(t) -if fbd.weight(3) == '3': - print('all good') +if fbd.weight(3) == "3": + print("all good") w = fbd.foo * 2 fbd.height = 34.8 diff --git a/tests/temp.py b/tests/temp.py index ff00562..f60a055 100644 --- a/tests/temp.py +++ b/tests/temp.py @@ -1,6 +1,7 @@ -import pure_interface import dataclasses +import pure_interface + class AnInterface(pure_interface.Interface): foo: int @@ -12,8 +13,7 @@ class AnImplementation(AnInterface): pass -x = AnImplementation(foo=4, bar='hello') - -assert(x.foo == 4) -assert(x.bar == 'hello') +x = AnImplementation(foo=4, bar="hello") +assert x.foo == 4 +assert x.bar == "hello" diff --git a/tests/test_adapt_args_anno.py b/tests/test_adapt_args_anno.py index 530e087..fbfb70f 100644 --- a/tests/test_adapt_args_anno.py +++ b/tests/test_adapt_args_anno.py @@ -1,12 +1,14 @@ import unittest + try: from unittest import mock except ImportError: import mock -from pure_interface import adapt_args, AdaptionError +from typing import List, Optional + import pure_interface -from typing import Optional, List +from pure_interface import AdaptionError, adapt_args class I1(pure_interface.Interface): @@ -35,25 +37,25 @@ def other_func(a: I1, b: Optional[I2] = None): class Thing1(I1): def __init__(self): - self.foo = 'foo' + self.foo = "foo" def bar(self): - print('bar:', self.foo) + print("bar:", self.foo) class Thing2(I2): def __init__(self): - self.bar = 'bar' + self.bar = "bar" def foo(self): - print('foo:', self.bar) + print("foo:", self.bar) class TestAdaptArgsWithAnno(unittest.TestCase): def test_adapt_args_works(self): thing1 = Thing1() adapt = mock.MagicMock() - with mock.patch('pure_interface.InterfaceType.optional_adapt', new=adapt): + with mock.patch("pure_interface.InterfaceType.optional_adapt", new=adapt): some_func(3, thing1) self.assertEqual(1, adapt.call_count) @@ -62,7 +64,7 @@ def test_adapt_args_works(self): def test_adapt_optional_args_works_with_none(self): thing1 = Thing1() adapt = mock.MagicMock() - with mock.patch('pure_interface.InterfaceType.optional_adapt', new=adapt): + with mock.patch("pure_interface.InterfaceType.optional_adapt", new=adapt): other_func(thing1) adapt.assert_has_calls((mock.call(I1, thing1), mock.call(I2, None))) @@ -71,41 +73,46 @@ def test_adapt_optional_args_works(self): thing1 = Thing1() thing2 = Thing2() adapt = mock.MagicMock() - with mock.patch('pure_interface.InterfaceType.optional_adapt', new=adapt): + with mock.patch("pure_interface.InterfaceType.optional_adapt", new=adapt): other_func(thing1, thing2) adapt.assert_has_calls([mock.call(I1, thing1), mock.call(I2, thing2)]) def test_unsupported_annotations_are_skipped(self): try: + @adapt_args def some_func(x: int, y: I2): pass + except Exception: - self.fail('Failed to ignore unsupported annotation') + self.fail("Failed to ignore unsupported annotation") adapt = mock.MagicMock() thing2 = Thing2() - with mock.patch('pure_interface.InterfaceType.optional_adapt', new=adapt): + with mock.patch("pure_interface.InterfaceType.optional_adapt", new=adapt): some_func(5, thing2) adapt.assert_called_once_with(I2, thing2) def test_unsupported_optional_annotations_are_skipped(self): try: + @adapt_args def some_func(x: Optional[int], y: I2): pass + except Exception: - self.fail('Failed to ignore unsupported annotation') + self.fail("Failed to ignore unsupported annotation") adapt = mock.MagicMock() thing2 = Thing2() - with mock.patch('pure_interface.InterfaceType.optional_adapt', new=adapt): + with mock.patch("pure_interface.InterfaceType.optional_adapt", new=adapt): some_func(5, thing2) adapt.assert_called_once_with(I2, thing2) def test_no_annotations_warning(self): - with mock.patch('warnings.warn') as warn: + with mock.patch("warnings.warn") as warn: + @adapt_args def no_anno(x, y): pass @@ -114,24 +121,28 @@ def no_anno(x, y): def test_adaption_error_raised_if_arg_not_subclass(self): with self.assertRaises(AdaptionError): + @adapt_args(x=int) def some_func(x): pass def test_adaption_error_raised_if_positional_arg_not_func(self): with self.assertRaises(AdaptionError): + @adapt_args(I2) def some_func(x): pass def test_adaption_error_raised_if_multiple_positional_args(self): with self.assertRaises(AdaptionError): + @adapt_args(I1, I2) def some_func(x): pass def test_adaption_error_raised_if_mixed_args(self): with self.assertRaises(AdaptionError): + @adapt_args(I1, y=I2) def some_func(x, y): pass @@ -147,8 +158,10 @@ def test_mixed_args_type_raises_adaption_error(self): def test_unsupported_generic_annotations_are_skipped(self): try: + @adapt_args def some_func(x: List[int], y: I2): pass + except Exception: - self.fail('Failed to ignore unsupported annotation') + self.fail("Failed to ignore unsupported annotation") diff --git a/tests/test_adapt_args_no_anno.py b/tests/test_adapt_args_no_anno.py index 3b63642..7933c47 100644 --- a/tests/test_adapt_args_no_anno.py +++ b/tests/test_adapt_args_no_anno.py @@ -7,8 +7,8 @@ except ImportError: import mock -from pure_interface import adapt_args, Interface import pure_interface +from pure_interface import Interface, adapt_args class I1(Interface): @@ -27,18 +27,18 @@ def foo(self): class Thing1(I1): def __init__(self): - self.foo = 'foo' + self.foo = "foo" def bar(self): - print('bar:', self.foo) + print("bar:", self.foo) class Thing2(I2): def __init__(self): - self.bar = 'bar' + self.bar = "bar" def foo(self): - print('foo:', self.bar) + print("foo:", self.bar) @adapt_args(y=I1) @@ -55,7 +55,7 @@ class TestAdaptArgsNoAnno(unittest.TestCase): def test_adapt_args_works(self): thing1 = Thing1() adapt = mock.MagicMock() - with mock.patch('pure_interface.InterfaceType.optional_adapt', new=adapt): + with mock.patch("pure_interface.InterfaceType.optional_adapt", new=adapt): some_func(3, thing1) self.assertEqual(1, adapt.call_count) @@ -64,7 +64,7 @@ def test_adapt_args_works(self): def test_adapt_optional_args_works_with_none(self): thing1 = Thing1() adapt = mock.MagicMock() - with mock.patch('pure_interface.InterfaceType.optional_adapt', new=adapt): + with mock.patch("pure_interface.InterfaceType.optional_adapt", new=adapt): other_func(thing1) adapt.assert_called_once_with(I2, None) @@ -73,13 +73,14 @@ def test_adapt_optional_args_works(self): thing1 = Thing1() thing2 = Thing2() adapt = mock.MagicMock() - with mock.patch('pure_interface.InterfaceType.optional_adapt', new=adapt): + with mock.patch("pure_interface.InterfaceType.optional_adapt", new=adapt): other_func(thing1, thing2) adapt.assert_called_once_with(I2, thing2) def test_no_annotations_warning(self): - with mock.patch('warnings.warn') as warn: + with mock.patch("warnings.warn") as warn: + @adapt_args def no_anno(x, y): pass @@ -88,24 +89,28 @@ def no_anno(x, y): def test_error_raised_if_arg_not_subclass(self): with self.assertRaises(pure_interface.errors.AdaptionError): + @adapt_args(x=int) def some_func(x): pass def test_type_error_raised_if_positional_arg_not_func(self): with self.assertRaises(pure_interface.errors.AdaptionError): + @adapt_args(I2) def some_func(x): pass def test_type_error_raised_if_multiple_positional_args(self): with self.assertRaises(pure_interface.errors.AdaptionError): + @adapt_args(I1, I2) def some_func(x): pass def test_type_error_raised_if_mixed_args(self): with self.assertRaises(pure_interface.errors.AdaptionError): + @adapt_args(I1, y=I2) def some_func(x, y): pass diff --git a/tests/test_adaption.py b/tests/test_adaption.py index 05b29e8..f142489 100644 --- a/tests/test_adaption.py +++ b/tests/test_adaption.py @@ -1,9 +1,9 @@ import unittest -from unittest import mock import warnings +from unittest import mock import pure_interface -from pure_interface import interface, Interface +from pure_interface import Interface, interface class ITalker(Interface): @@ -22,12 +22,12 @@ class ISleepSpeaker(ISpeaker, Interface): class Speaker: def speak(self, volume): - return 'speak' + return "speak" class Talker(ITalker): def talk(self): - return 'talk' + return "talk" @pure_interface.adapts(Talker) @@ -41,7 +41,7 @@ def speak(self, volume): class Talker2: def talk(self): - return 'talk' + return "talk" @pure_interface.adapts(Talker2, ISpeaker) @@ -63,7 +63,7 @@ def speak(self, volume): class Talker3: def talk(self): - return 'talk' + return "talk" @pure_interface.adapts(Talker3, ISpeaker) @@ -78,7 +78,7 @@ def none_to_speaker(_none): # Speaker implicitly supplies ISpeaker class Talker4(object): def talk(self): - return 'talk' + return "talk" def bad_adapter(talker): @@ -119,7 +119,6 @@ def __len__(self): class DunderClass(DunderInterface): - def __call__(self, a): super().__call__(a) @@ -137,7 +136,7 @@ def test_adaption_passes(self): s = ISpeaker.adapt(talker, interface_only=False) self.assertTrue(isinstance(s, ISpeaker)) - self.assertEqual(s.speak(5), 'talk') + self.assertEqual(s.speak(5), "talk") def test_implicit_adapter(self): talker = Talker2() @@ -145,17 +144,17 @@ def test_implicit_adapter(self): self.assertIsNone(s) with warnings.catch_warnings(): - warnings.simplefilter('ignore') + warnings.simplefilter("ignore") s = ISpeaker.adapt_or_none(talker, allow_implicit=True, interface_only=False) self.assertTrue(ISpeaker.provided_by(s)) - self.assertEqual(s.speak(5), 'talk') + self.assertEqual(s.speak(5), "talk") def test_callable_adapter_passes(self): talker = Talker3() s = ISpeaker.adapt(talker, interface_only=False) self.assertTrue(ISpeaker.provided_by(s)) - self.assertEqual(s.speak(5), 'talk') + self.assertEqual(s.speak(5), "talk") def test_adapter_call_check(self): pure_interface.register_adapter(bad_adapter, Talker4, ISpeaker) @@ -194,6 +193,7 @@ def test_adapt_to_interface_or_none(self): def test_no_interface_on_class_raises(self): with self.assertRaises(pure_interface.InterfaceError): + @pure_interface.adapts(ISpeaker) class NoInterface(object): pass @@ -203,12 +203,12 @@ def test_adapt_on_class_works(self): s = ISpeaker.adapt(talker, interface_only=False) self.assertTrue(isinstance(s, ISpeaker)) - self.assertEqual(s.speak(4), 'talk') + self.assertEqual(s.speak(4), "talk") def test_filter_adapt(self): a_speaker = Speaker() a_talker = Talker() - input = [None, Talker4(), a_talker, a_speaker, 'text'] + input = [None, Talker4(), a_talker, a_speaker, "text"] # act output = list(ISpeaker.filter_adapt(input, interface_only=False)) # assert @@ -220,10 +220,10 @@ def test_filter_adapt(self): def test_implicit_filter_adapt(self): a_speaker = Speaker() a_talker = Talker() - input = [None, Talker4(), a_talker, a_speaker, 'text'] + input = [None, Talker4(), a_talker, a_speaker, "text"] # act with warnings.catch_warnings(): - warnings.simplefilter('ignore') + warnings.simplefilter("ignore") output = list(ISpeaker.filter_adapt(input, allow_implicit=True, interface_only=False)) # assert self.assertEqual(len(output), 3) @@ -235,12 +235,13 @@ def test_implicit_filter_adapt(self): def test_fail_if_no_to_interface_for_func(self): with self.assertRaises(interface.InterfaceError): + @pure_interface.adapts(int) def foo(arg): return None def test_manual_interface_only(self): - topic_speaker = TopicSpeaker('Python') + topic_speaker = TopicSpeaker("Python") s = ITopicSpeaker.interface_only(topic_speaker) self.assertIsInstance(s, interface._ImplementationWrapper) @@ -253,29 +254,29 @@ def setUpClass(cls): pure_interface.set_is_development(True) def test_wrapping_works(self): - topic_speaker = TopicSpeaker('Python') + topic_speaker = TopicSpeaker("Python") s = ITopicSpeaker.adapt(topic_speaker) - topic_speaker2 = TopicSpeaker('Interfaces') + topic_speaker2 = TopicSpeaker("Interfaces") t = ITopicSpeaker.adapt(topic_speaker2) self.assertIsInstance(s, interface._ImplementationWrapper) self.assertIsInstance(s, ITopicSpeaker) - self.assertEqual(s.speak(5), 'speak') - self.assertEqual(s.topic, 'Python') - self.assertEqual(t.topic, 'Interfaces') + self.assertEqual(s.speak(5), "speak") + self.assertEqual(s.topic, "Python") + self.assertEqual(t.topic, "Interfaces") def test_wrapper_set(self): - topic_speaker = TopicSpeaker('Python') + topic_speaker = TopicSpeaker("Python") s = ITopicSpeaker.adapt(topic_speaker) - s.topic = 'Snakes' + s.topic = "Snakes" - self.assertEqual('Snakes', topic_speaker.topic) + self.assertEqual("Snakes", topic_speaker.topic) self.assertIsInstance(s, interface._ImplementationWrapper) with self.assertRaises(AttributeError): - s._ImplementationWrapper__interface_name = 'hello' + s._ImplementationWrapper__interface_name = "hello" def test_wrapping_works2(self): - topic_speaker = TopicSpeaker('Python') + topic_speaker = TopicSpeaker("Python") s = ISpeaker.adapt(topic_speaker) self.assertIsInstance(s, interface._ImplementationWrapper) @@ -290,7 +291,7 @@ def test_implicit_adapter_passes(self): self.assertIsInstance(s, interface._ImplementationWrapper) self.assertIsInstance(s, ISpeaker) - self.assertEqual(s.speak(5), 'talk') + self.assertEqual(s.speak(5), "talk") def test_callable_adapter_passes(self): talker = Talker3() @@ -298,7 +299,7 @@ def test_callable_adapter_passes(self): self.assertIsInstance(s, interface._ImplementationWrapper) self.assertIsInstance(s, ISpeaker) - self.assertEqual(s.speak(5), 'talk') + self.assertEqual(s.speak(5), "talk") def test_adapt_to_interface_or_none(self): self.assertIsNone(ISpeaker.adapt_or_none(None)) @@ -307,7 +308,7 @@ def test_adapt_to_interface_or_none(self): def test_filter_adapt(self): a_speaker = Speaker() a_talker = Talker() - input_list = [None, Talker4(), a_talker, a_speaker, 'text'] + input_list = [None, Talker4(), a_talker, a_speaker, "text"] # act output = list(ISpeaker.filter_adapt(input_list)) # assert @@ -316,7 +317,7 @@ def test_filter_adapt(self): def test_implicit_filter_adapt(self): a_speaker = Speaker() a_talker = Talker() - input_list = [None, Talker4(), a_talker, a_speaker, 'text'] + input_list = [None, Talker4(), a_talker, a_speaker, "text"] # act output = list(ISpeaker.filter_adapt(input_list, allow_implicit=True)) # assert @@ -336,7 +337,7 @@ def test_adapter_to_sub_interface_used(self): self.assertIsInstance(speaker, SleepSpeaker) def test_adapter_preference(self): - """ adapt should prefer interface adapter over sub-interface adapter """ + """adapt should prefer interface adapter over sub-interface adapter""" class IA(Interface): foo = None @@ -364,14 +365,14 @@ def test_optional_adapt(self): a_speaker = Speaker() allow = object() interface_only = object() - with mock.patch('pure_interface.InterfaceType.adapt') as adapt: + with mock.patch("pure_interface.InterfaceType.adapt") as adapt: # act s = ISpeaker.optional_adapt(a_speaker, allow_implicit=allow, interface_only=interface_only) none = ISpeaker.optional_adapt(None, allow_implicit=allow, interface_only=interface_only) # assert adapt.assert_called_once_with(ISpeaker, a_speaker, allow_implicit=allow, interface_only=interface_only) self.assertIs(s, adapt.return_value) - self.assertIsNone(none, 'optional_adapt(None) did not return None') + self.assertIsNone(none, "optional_adapt(None) did not return None") def test_adapt_interface_only(self): talker = Talker() @@ -380,7 +381,7 @@ def test_adapt_interface_only(self): try: ISpeaker.adapt(talker_only) except: - self.fail('adaption of interface only failed.') + self.fail("adaption of interface only failed.") def test_adapt_callable_is_callable(self): dunder = DunderClass() @@ -388,13 +389,13 @@ def test_adapt_callable_is_callable(self): try: dunder_only(1) except TypeError: - self.fail('calling interface only failed') + self.fail("calling interface only failed") try: len(dunder) except: - self.fail('len() interface only failed') + self.fail("len() interface only failed") def test_can_adapt(self): - self.assertFalse(ITalker.can_adapt('hello')) + self.assertFalse(ITalker.can_adapt("hello")) self.assertTrue(ITalker.can_adapt(Talker())) diff --git a/tests/test_dataclass_support.py b/tests/test_dataclass_support.py index 306a437..5f54515 100644 --- a/tests/test_dataclass_support.py +++ b/tests/test_dataclass_support.py @@ -1,111 +1,113 @@ -from dataclasses import dataclass import unittest +from dataclasses import dataclass from pure_interface import * class IFoo(Interface): - a: int - b: str - - def foo(self): - pass - - -@dataclass -class Foo(IFoo): - c: float = 12.0 - - def foo(self): - return 'a={}, b={}, c={}'.format(self.a, self.b, self.c) - - -class IBar(IFoo, Interface): - a: Foo - - -class TestDataClasses(unittest.TestCase): - def test_data_class(self): - try: - f = Foo(a=1, b='two') - except Exception as exc: - self.fail(str(exc)) - - self.assertEqual(1, f.a) - self.assertEqual('two', f.b) - self.assertEqual(12.0, f.c) - - def test_data_arg_order(self): - try: - f = Foo(2, 'two', 34.0) - except Exception as exc: - self.fail(str(exc)) - self.assertEqual(2, f.a) - self.assertEqual('two', f.b) - self.assertEqual(34.0, f.c) - self.assertEqual('a=2, b=two, c=34.0', f.foo()) - - def test_data_class_with_args(self): - try: - @dataclass(frozen=True) - class FrozenFoo(IFoo): - def foo(self): - return 'a={}, b={}'.format(self.a, self.b) - - except Exception as exc: - self.fail(str(exc)) - - f = Foo(a=1, b='two') - self.assertEqual(1, f.a) - self.assertEqual('two', f.b) - - def test_read_only_attr(self): - @dataclass - class RoFoo(IFoo): - c: int - - @property - def b(self): - return 'str' - - def foo(self): - return 'a={}, b={}, c={}'.format(self.a, self.b, self.c) - - f = RoFoo(a=1, c=3) - self.assertEqual({'a': int, 'c': int}, RoFoo.__annotations__) - self.assertEqual(1, f.a) - self.assertEqual('str', f.b) - - def test_attr_present(self): - @dataclass - class AFoo(IFoo): - a = 10 - - def foo(self): - return 'a={}, b={}, c={}'.format(self.a, self.b, self.c) - - f = AFoo(b='str') - self.assertEqual({'b': str}, AFoo.__annotations__) - self.assertEqual(10, f.a) - self.assertEqual('str', f.b) - - def test_annotations_override(self): - """ ensure overridden annotations are used correctly """ - @dataclass - class Bar(IBar): - - def foo(self): - return 'a={}, b={}'.format(self.a, self.b) - - self.assertEqual({'a': int, 'b': str}, IFoo.__annotations__) - self.assertEqual({'a': Foo, 'b': str}, IBar.__annotations__) - self.assertEqual({'a': Foo, 'b': str}, Bar.__annotations__) - b = Bar(a=Foo(a=1, b='two'), b='three') - self.assertIsInstance(b.a, Foo) - - def test_non_direct_subclass(self): - """ ensure no extra annotations are added to the class""" - class Baz(Foo): - e: str - - self.assertEqual({'e': str}, Baz.__annotations__) + a: int + b: str + + def foo(self): + pass + + +@dataclass +class Foo(IFoo): + c: float = 12.0 + + def foo(self): + return "a={}, b={}, c={}".format(self.a, self.b, self.c) + + +class IBar(IFoo, Interface): + a: Foo + + +class TestDataClasses(unittest.TestCase): + def test_data_class(self): + try: + f = Foo(a=1, b="two") + except Exception as exc: + self.fail(str(exc)) + + self.assertEqual(1, f.a) + self.assertEqual("two", f.b) + self.assertEqual(12.0, f.c) + + def test_data_arg_order(self): + try: + f = Foo(2, "two", 34.0) + except Exception as exc: + self.fail(str(exc)) + self.assertEqual(2, f.a) + self.assertEqual("two", f.b) + self.assertEqual(34.0, f.c) + self.assertEqual("a=2, b=two, c=34.0", f.foo()) + + def test_data_class_with_args(self): + try: + + @dataclass(frozen=True) + class FrozenFoo(IFoo): + def foo(self): + return "a={}, b={}".format(self.a, self.b) + + except Exception as exc: + self.fail(str(exc)) + + f = Foo(a=1, b="two") + self.assertEqual(1, f.a) + self.assertEqual("two", f.b) + + def test_read_only_attr(self): + @dataclass + class RoFoo(IFoo): + c: int + + @property + def b(self): + return "str" + + def foo(self): + return "a={}, b={}, c={}".format(self.a, self.b, self.c) + + f = RoFoo(a=1, c=3) + self.assertEqual({"a": int, "c": int}, RoFoo.__annotations__) + self.assertEqual(1, f.a) + self.assertEqual("str", f.b) + + def test_attr_present(self): + @dataclass + class AFoo(IFoo): + a = 10 + + def foo(self): + return "a={}, b={}, c={}".format(self.a, self.b, self.c) + + f = AFoo(b="str") + self.assertEqual({"b": str}, AFoo.__annotations__) + self.assertEqual(10, f.a) + self.assertEqual("str", f.b) + + def test_annotations_override(self): + """ensure overridden annotations are used correctly""" + + @dataclass + class Bar(IBar): + def foo(self): + return "a={}, b={}".format(self.a, self.b) + + self.assertEqual({"a": int, "b": str}, IFoo.__annotations__) + self.assertEqual({"a": Foo, "b": str}, IBar.__annotations__) + self.assertEqual({"a": Foo, "b": str}, Bar.__annotations__) + b = Bar(a=Foo(a=1, b="two"), b="three") + self.assertIsInstance(b.a, Foo) + + def test_non_direct_subclass(self): + """ensure no extra annotations are added to the class""" + + class Baz(Foo): + e: str + + self.assertEqual({"e": str}, Baz.__annotations__) diff --git a/tests/test_delegate.py b/tests/test_delegate.py index 053d19c..683b32b 100644 --- a/tests/test_delegate.py +++ b/tests/test_delegate.py @@ -1,10 +1,9 @@ import dataclasses - -import pure_interface import unittest from unittest import mock -from pure_interface import delegation, Interface +import pure_interface +from pure_interface import Interface, delegation class ITalker(Interface): @@ -24,15 +23,15 @@ def speak(self, volume): class Speaker(ISpeaker): def speak(self, volume): - return 'speak' + return "speak" class Talker(ITalker): def talk(self): - return 'talk' + return "talk" def chat(self): - return 'chat' + return "chat" class IPoint(Interface): @@ -60,11 +59,11 @@ def __init__(self, x=0, y=1): self.y = int(y) def to_str(self) -> str: - return f'{self.x}, {self.y}' + return f"{self.x}, {self.y}" class DFallback(delegation.Delegate, ITalker): - pi_attr_fallback = 'impl' + pi_attr_fallback = "impl" def __init__(self, impl): self.impl = impl @@ -75,14 +74,15 @@ class DSubFallback(DFallback, ISubTalker): class DSubFallback2(DFallback, ISubTalker): - pi_attr_fallback = 'impl' + pi_attr_fallback = "impl" class DAttrMap(delegation.Delegate, IPoint): - pi_attr_mapping = {'x': 'a.x', - 'y': 'b.y', - 'to_str': 'b.to_str', - } + pi_attr_mapping = { + "x": "a.x", + "y": "b.y", + "to_str": "b.to_str", + } def __init__(self, a, b): self.a = a @@ -90,8 +90,7 @@ def __init__(self, a, b): class DDelegateList(delegation.Delegate, IPoint): - pi_attr_delegates = {'a': ['x', 'to_str'], - 'b': ['x', 'y']} + pi_attr_delegates = {"a": ["x", "to_str"], "b": ["x", "y"]} def __init__(self, a, b): self.a = a @@ -99,8 +98,7 @@ def __init__(self, a, b): class DDelegateIFace(delegation.Delegate, IPoint, ITalker): - pi_attr_delegates = {'a': IPoint, - 'b': ITalker} + pi_attr_delegates = {"a": IPoint, "b": ITalker} def __init__(self, a, b): self.a = a @@ -108,21 +106,21 @@ def __init__(self, a, b): class DelegateOverride(delegation.Delegate, ITalker): - pi_attr_delegates = {'a': ITalker} + pi_attr_delegates = {"a": ITalker} def __init__(self, a): self.a = a def talk(self): - return 'hello' + return "hello" class DelegateAttrOverride(delegation.Delegate, IPoint): - pi_attr_fallback = 'a' + pi_attr_fallback = "a" def __init__(self, a): self.a = a - self._x = 'x' + self._x = "x" @property def x(self): @@ -137,8 +135,7 @@ class DoubleDottedDelegate(delegation.Delegate): x: int y: int - pi_attr_mapping = {'x': 'a.b.x', - 'y': 'a.b.y'} + pi_attr_mapping = {"x": "a.b.x", "y": "a.b.y"} def __init__(self): a = Talker() @@ -147,7 +144,7 @@ def __init__(self): class ScaledPoint(pure_interface.Delegate, IPoint): - pi_attr_fallback = '_p' + pi_attr_fallback = "_p" def __init__(self, point): self._p = point @@ -161,23 +158,23 @@ def y(self, value): self._p.y = int(value // 2) def to_str(self) -> str: - return f'{self.x}, {self.y}' + return f"{self.x}, {self.y}" class ScaledPoint3(ScaledPoint, IPoint3): - pi_attr_fallback = '_p' + pi_attr_fallback = "_p" class DelegateTest(unittest.TestCase): def test_descriptor_get_class(self): - d = pure_interface.delegation._Delegated('foo.bar') + d = pure_interface.delegation._Delegated("foo.bar") e = d.__get__(None, mock.Mock) self.assertIs(d, e) def test_descriptor_get(self): m = mock.Mock() - d = pure_interface.delegation._Delegated('foo.bar.baz') + d = pure_interface.delegation._Delegated("foo.bar.baz") v = d.__get__(m, type(m)) @@ -186,7 +183,7 @@ def test_descriptor_get(self): def test_descriptor_set(self): m = mock.Mock() v = mock.Mock() - d = pure_interface.delegation._Delegated('foo.bar.baz') + d = pure_interface.delegation._Delegated("foo.bar.baz") d.__set__(m, v) w = d.__get__(m, type(m)) @@ -197,7 +194,7 @@ def test_descriptor_set(self): def test_fallback(self): d = DFallback(Talker()) - self.assertEqual('talk', d.talk()) + self.assertEqual("talk", d.talk()) with self.assertRaises(AttributeError): d.x @@ -230,7 +227,7 @@ def test_delegate_iface(self): self.assertEqual(1, d.x) self.assertEqual(2, d.y) - self.assertEqual('talk', d.talk()) + self.assertEqual("talk", d.talk()) def test_attr_set(self): a = Point(1, 2) @@ -247,31 +244,33 @@ def test_attr_set(self): def test_check_duplicate(self): with self.assertRaises(ValueError): + class BadDelegate(delegation.Delegate): - pi_attr_mapping = {'x': 'a.x'} - pi_attr_delegates = {'x': ['foo', 'bar']} + pi_attr_mapping = {"x": "a.x"} + pi_attr_delegates = {"x": ["foo", "bar"]} def test_check_delegate_in_attr_map(self): with self.assertRaises(ValueError): + class BadDelegate(delegation.Delegate): - pi_attr_mapping = {'foo': 'a.x'} - pi_attr_delegates = {'x': ['foo', 'bar']} + pi_attr_mapping = {"foo": "a.x"} + pi_attr_delegates = {"x": ["foo", "bar"]} def test_delegate_method_override(self): t = Talker() d = DelegateOverride(t) - self.assertEqual('hello', d.talk()) + self.assertEqual("hello", d.talk()) def test_delegate_override(self): t = Talker() d = DelegateOverride(t) - self.assertEqual('hello', d.talk()) + self.assertEqual("hello", d.talk()) def test_delegate_attr_override(self): p = Point() d = DelegateAttrOverride(p) - self.assertEqual('x', d.x) - d.x = 'y' + self.assertEqual("x", d.x) + d.x = "y" self.assertEqual(0, p.x) def test_double_dotted_delegate(self): @@ -282,11 +281,11 @@ def test_double_dotted_delegate(self): self.assertEqual(1, d.a.b.x) def test_delegate_subclass(self): - """test that subclass methods are not delegated """ + """test that subclass methods are not delegated""" p = PointImpl(1, 2, 3) d3 = ScaledPoint3(p) self.assertEqual(4, d3.y) - self.assertEqual('1, 4', d3.to_str()) + self.assertEqual("1, 4", d3.to_str()) d3.y = 8 # delegates to p self.assertEqual(4, p.y) self.assertEqual(3, d3.z) @@ -299,7 +298,7 @@ def test_delegate_subclass_fallback(self): def test_delegate_subclass_fallback2(self): """Check subclass fallbacks are used for missing attributes.""" d = DSubFallback2(Talker()) - self.assertEqual('chat', d.chat()) + self.assertEqual("chat", d.chat()) def test_delegate_provides_fails(self): with self.assertRaises(pure_interface.InterfaceError): @@ -307,7 +306,6 @@ def test_delegate_provides_fails(self): class CompositionTest(unittest.TestCase): - def test_type_composition(self): a = Point(1, 2) b = Talker() @@ -322,14 +320,14 @@ def test_type_composition(self): self.assertTrue(issubclass(T, delegation.Delegate)) self.assertEqual(1, t.x) self.assertEqual(2, t.y) - self.assertEqual('talk', t.talk()) + self.assertEqual("talk", t.talk()) def test_type_composition_checks(self): with self.assertRaises(ValueError): delegation.composed_type(IPoint) with self.assertRaises(ValueError): - delegation.composed_type('hello') + delegation.composed_type("hello") with self.assertRaises(ValueError): delegation.composed_type(Talker) @@ -370,8 +368,9 @@ def test_type_composition_chain(self): def test_unused_fallback_is_benign(self): try: + class UnusedFallbackDelegate(delegation.Delegate): - pi_attr_fallback = 'a' + pi_attr_fallback = "a" def __init__(self): self.a = Talker() @@ -386,6 +385,6 @@ def test_fail_on_unsupported_type(self): delegation.composed_type(str, int) def test_too_many_interfaces(self): - with mock.patch('pure_interface.delegation._letters', 'a'): + with mock.patch("pure_interface.delegation._letters", "a"): with self.assertRaises(ValueError): delegation.composed_type(ITalker, IPoint) diff --git a/tests/test_func_sigs3.py b/tests/test_func_sigs3.py index 5239b7d..cd14933 100644 --- a/tests/test_func_sigs3.py +++ b/tests/test_func_sigs3.py @@ -10,7 +10,7 @@ def func1(*, a, b): # kw only no defaults pass -def func2(*, a, b='b'): # kw only with default +def func2(*, a, b="b"): # kw only with default pass @@ -18,11 +18,11 @@ def func3(a, *, b, c): # p_or_kw and kw_only pass -def func4(a='a', *, b): +def func4(a="a", *, b): pass -def func5(a, *, b='b'): +def func5(a, *, b="b"): pass @@ -42,11 +42,11 @@ def no_args(): pass -def func_bad_name(*, a, z='z'): +def func_bad_name(*, a, z="z"): pass -def func1ex(*, a, b, c='c'): +def func1ex(*, a, b, c="c"): pass @@ -54,26 +54,25 @@ def func1ex2(*, b, a): # order doesn't matter pass -def func1ex3(*, b='b', a): +def func1ex3(*, b="b", a): pass -def func3ex(a, d='d', *, b, c): # p_or_kw and kw_only +def func3ex(a, d="d", *, b, c): # p_or_kw and kw_only pass -def func3ex2(a, *, b, c, d='d'): # p_or_kw and kw_only +def func3ex2(a, *, b, c, d="d"): # p_or_kw and kw_only pass -def func5ex(a, *, b='b', c='c'): +def func5ex(a, *, b="b", c="c"): pass def _test_call(spec_func, spec_sig, impl_func, impl_sig, args, kwargs): spec_func(*args, **kwargs) - num_po_args = len([p for p in spec_sig.parameters.values() - if p.kind == p.POSITIONAL_ONLY]) + num_po_args = len([p for p in spec_sig.parameters.values() if p.kind == p.POSITIONAL_ONLY]) try: impl_func(*args, **kwargs) except TypeError: @@ -83,18 +82,16 @@ def _test_call(spec_func, spec_sig, impl_func, impl_sig, args, kwargs): except TypeError: return False non_po_args = ba.args[num_po_args:] - if not all(k == ba.arguments[k] for k in non_po_args - if k not in ('args', 'kwargs')): + if not all(k == ba.arguments[k] for k in non_po_args if k not in ("args", "kwargs")): return False - kwargs = ba.arguments.get('kwargs', {}) + kwargs = ba.arguments.get("kwargs", {}) if not all(k == v for k, v in kwargs.items()): return False return True def test_call(spec_func: types.FunctionType, impl_func: types.FunctionType) -> bool: - """ call the function with parameters as indicated by the parameter list - """ + """call the function with parameters as indicated by the parameter list""" spec_sig = pure_interface.interface.signature(spec_func) impl_sig = pure_interface.interface.signature(impl_func) @@ -111,7 +108,7 @@ def test_call(spec_func: types.FunctionType, impl_func: types.FunctionType) -> b pos_args = po_args + pok_args for kwo_args in iter_kw_args(spec_sig): # test args can be positional or keyword - for i in range(len(pos_or_kw)+1): + for i in range(len(pos_or_kw) + 1): args = po_args + pok_args[:i] kwargs = {x: x for x in pok_args[i:]} kwargs.update(kwo_args) @@ -131,12 +128,14 @@ def test_call(spec_func: types.FunctionType, impl_func: types.FunctionType) -> b kwo_args = {p.name: p.name for p in kw_only} # test *args if any(p.kind == p.VAR_POSITIONAL for p in spec_sig.parameters.values()): - if not _test_call(spec_func, spec_sig, impl_func, impl_sig, pok_args + ['more', 'random', 'arguments'], kwo_args): + if not _test_call( + spec_func, spec_sig, impl_func, impl_sig, pok_args + ["more", "random", "arguments"], kwo_args + ): return False # test **kwargs if any(p.kind == p.VAR_KEYWORD for p in spec_sig.parameters.values()): kwargs = kwo_args.copy() - kwargs.update({x: x for x in ('more', 'random', 'keywords')}) + kwargs.update({x: x for x in ("more", "random", "keywords")}) if not _test_call(spec_func, spec_sig, impl_func, impl_sig, pok_args, kwargs): return False kwargs.update(pok_def) @@ -149,7 +148,7 @@ def iter_kw_args(spec_sig): kw_only = [p for p in spec_sig.parameters.values() if p.kind == p.KEYWORD_ONLY] kwo_args = [p.name for p in kw_only] kwo_req = [p.name for p in kw_only if p.default is p.empty] - for i in range(len(kwo_req), len(kwo_args)+1): + for i in range(len(kwo_req), len(kwo_args) + 1): kwargs = {x: x for x in kwo_args[:i]} yield kwargs @@ -163,11 +162,17 @@ def check_signatures(self, int_func, impl_func, expected_result): interface_sig = pure_interface.interface.signature(int_func) concrete_sig = pure_interface.interface.signature(impl_func) reality = test_call(int_func, impl_func) - self.assertEqual(expected_result, reality, - '{}, {}. Reality does not match expectations'.format(int_func.__name__, impl_func.__name__)) + self.assertEqual( + expected_result, + reality, + "{}, {}. Reality does not match expectations".format(int_func.__name__, impl_func.__name__), + ) result = pure_interface.interface._signatures_are_consistent(concrete_sig, interface_sig) - self.assertEqual(expected_result, result, - '{}, {}. Signature test gave wrong answer'.format(int_func.__name__, impl_func.__name__)) + self.assertEqual( + expected_result, + result, + "{}, {}. Signature test gave wrong answer".format(int_func.__name__, impl_func.__name__), + ) def test_kw_only(self): self.check_signatures(func1, func2, True) diff --git a/tests/test_func_sigs_po.py b/tests/test_func_sigs_po.py index 794aeb3..955e08a 100644 --- a/tests/test_func_sigs_po.py +++ b/tests/test_func_sigs_po.py @@ -11,7 +11,7 @@ def func1(a, b, /): # kw only no defaults pass -def func2(a, b='b', /): # pos only with default +def func2(a, b="b", /): # pos only with default pass @@ -19,19 +19,19 @@ def func3(a, /, b): # pos only and p_or_kw pass -def func4(a, /, b='b'): +def func4(a, /, b="b"): pass -def func5(a, /, b='b', *, c): +def func5(a, /, b="b", *, c): pass -def func6(a, /, *, b='b', c): +def func6(a, /, *, b="b", c): pass -def func7(a, b='b', /, *, c): +def func7(a, b="b", /, *, c): pass @@ -51,11 +51,11 @@ def no_args(): pass -def func_diff_name(a, z='z', /): +def func_diff_name(a, z="z", /): pass -def func1ex(a, b, c='c', /): +def func1ex(a, b, c="c", /): pass @@ -63,19 +63,19 @@ def func1ex2(b, /, a): pass -def func3ex(a, b, c='c'): +def func3ex(a, b, c="c"): pass -def func3ex2(a, b, c, d='d'): +def func3ex2(a, b, c, d="d"): pass -def func5ex(a, b='b', /, *, c='c'): +def func5ex(a, b="b", /, *, c="c"): pass -def func5ex2(a, b='b', c='c'): +def func5ex2(a, b="b", c="c"): pass @@ -86,13 +86,19 @@ def setUpClass(cls): def check_signatures(self, int_func, impl_func, expected_result): reality = test_func_sigs3.test_call(int_func, impl_func) - self.assertEqual(expected_result, reality, - '{}, {}. Reality does not match expectations'.format(int_func.__name__, impl_func.__name__)) + self.assertEqual( + expected_result, + reality, + "{}, {}. Reality does not match expectations".format(int_func.__name__, impl_func.__name__), + ) interface_sig = pure_interface.interface.signature(int_func) concrete_sig = pure_interface.interface.signature(impl_func) result = pure_interface.interface._signatures_are_consistent(concrete_sig, interface_sig) - self.assertEqual(expected_result, result, - '{}, {}. Signature test gave wrong answer'.format(int_func.__name__, impl_func.__name__)) + self.assertEqual( + expected_result, + result, + "{}, {}. Signature test gave wrong answer".format(int_func.__name__, impl_func.__name__), + ) def test_pos_only(self): self.check_signatures(func1, func2, True) diff --git a/tests/test_function_sigs.py b/tests/test_function_sigs.py index a65aed5..7c8c96f 100644 --- a/tests/test_function_sigs.py +++ b/tests/test_function_sigs.py @@ -1,10 +1,8 @@ -from pure_interface import Interface -import pure_interface - -import unittest import types +import unittest -from pure_interface import interface +import pure_interface +from pure_interface import Interface, interface class IAnimal(Interface): @@ -26,11 +24,11 @@ def func1(a, b, c): pass -def func2(a, b, c='c'): +def func2(a, b, c="c"): pass -def func3(a='a', b='b'): +def func3(a="a", b="b"): pass @@ -46,7 +44,7 @@ def func6(a, **kwargs): pass -def func7(a, b='b', *args): +def func7(a, b="b", *args): pass @@ -62,7 +60,7 @@ def func10(*args, **kwargs): pass -def func11(a='a', **kwargs): +def func11(a="a", **kwargs): pass @@ -76,17 +74,16 @@ def _test_call(spec_func, impl_func, impl_sig, args, kwargs): ba = impl_sig.bind(*args, **kwargs) except TypeError: return False - if not all(k == v for k, v in ba.arguments.items() if k not in ('args', 'kwargs')): + if not all(k == v for k, v in ba.arguments.items() if k not in ("args", "kwargs")): return False - kwargs = ba.arguments.get('kwargs', {}) + kwargs = ba.arguments.get("kwargs", {}) if not all(k == v for k, v in kwargs.items()): return False return True def test_call(spec_func: types.FunctionType, impl_func: types.FunctionType) -> bool: - """ call the function with parameters as indicated by the parameter list - """ + """call the function with parameters as indicated by the parameter list""" spec_sig = pure_interface.interface.signature(spec_func) impl_sig = pure_interface.interface.signature(impl_func) pos_or_kw = [p for p in spec_sig.parameters.values() if p.kind == p.POSITIONAL_OR_KEYWORD] @@ -95,25 +92,25 @@ def test_call(spec_func: types.FunctionType, impl_func: types.FunctionType) -> b pok_req = [p.name for p in pos_or_kw if p.default is p.empty] n_req = len(pok_req) # test args can be positional or keyword - for i in range(len(pos_or_kw)+1): + for i in range(len(pos_or_kw) + 1): args = pok_args[:i] kwargs = {x: x for x in pok_args[i:]} if not _test_call(spec_func, impl_func, impl_sig, args, kwargs): return False # test defaults may be omitted for i in range(len(pok_def)): - args = pok_args[:n_req + i] + args = pok_args[: n_req + i] if not _test_call(spec_func, impl_func, impl_sig, args, {}): return False if not _test_call(spec_func, impl_func, impl_sig, (), {x: x for x in args}): return False # test *args if any(p.kind == p.VAR_POSITIONAL for p in spec_sig.parameters.values()): - if not _test_call(spec_func, impl_func, impl_sig, pok_args + ['more', 'random', 'arguments'], {}): + if not _test_call(spec_func, impl_func, impl_sig, pok_args + ["more", "random", "arguments"], {}): return False # test **kwargs if any(p.kind == p.VAR_KEYWORD for p in spec_sig.parameters.values()): - extra_kw = {x: x for x in ('more', 'random', 'keywords')} + extra_kw = {x: x for x in ("more", "random", "keywords")} if not _test_call(spec_func, impl_func, impl_sig, pok_args, extra_kw): return False extra_kw.update(pok_def) @@ -131,11 +128,17 @@ def check_signatures(self, int_func, impl_func, expected_result): interface_sig = pure_interface.interface.signature(int_func) concrete_sig = pure_interface.interface.signature(impl_func) reality = test_call(int_func, impl_func) - self.assertEqual(expected_result, reality, - '{}, {}. Reality does not match expectations'.format(int_func.__name__, impl_func.__name__)) + self.assertEqual( + expected_result, + reality, + "{}, {}. Reality does not match expectations".format(int_func.__name__, impl_func.__name__), + ) result = pure_interface.interface._signatures_are_consistent(concrete_sig, interface_sig) - self.assertEqual(expected_result, result, - '{}, {}. Signature test gave wrong answer'.format(int_func.__name__, impl_func.__name__)) + self.assertEqual( + expected_result, + result, + "{}, {}. Signature test gave wrong answer".format(int_func.__name__, impl_func.__name__), + ) def test_tests(self): self.check_signatures(func1, func1, True) @@ -208,7 +211,7 @@ def pos_kwarg_vararg(a, c=4, *args): self.check_signatures(func4, pos_kwarg_vararg, False) def test_all(self): - def all(a, b='b', *args, **kwargs): + def all(a, b="b", *args, **kwargs): pass self.check_signatures(func1, all, True) @@ -221,11 +224,12 @@ def all(a, b='b', *args, **kwargs): self.check_signatures(all, all, True) def test_binding_order(self): - def all(a, c='c', *args, **kwargs): + def all(a, c="c", *args, **kwargs): pass def rev(a, c, b): pass + self.check_signatures(func1, all, False) self.check_signatures(func1, rev, False) @@ -244,12 +248,14 @@ def test_some_more(self): def test_diff_names_fails(self): # concrete subclass with self.assertRaises(pure_interface.InterfaceError): + class Animal(IAnimal): def speak(self, loudness): pass # abstract subclass with self.assertRaises(pure_interface.InterfaceError): + class Animal2(IAnimal): def speak(self, loudness): pass @@ -257,12 +263,14 @@ def speak(self, loudness): def test_too_few_fails(self): # concrete subclass with self.assertRaises(pure_interface.InterfaceError): + class Animal(IAnimal): def speak(self): pass # abstract subclass with self.assertRaises(pure_interface.InterfaceError): + class Animal2(IAnimal): def speak(self): pass @@ -270,6 +278,7 @@ def speak(self): def test_too_many_fails(self): # concrete subclass with self.assertRaises(pure_interface.InterfaceError): + class Animal(IAnimal): def speak(self, volume, msg): pass @@ -280,6 +289,7 @@ def walk(self, distance): pass with self.assertRaises(pure_interface.InterfaceError): + class Animal(IWalkingAnimal): speak = ADescriptor() @@ -288,46 +298,49 @@ def walk(self, volume): # abstract subclass with self.assertRaises(pure_interface.InterfaceError): + class Animal2(IAnimal): def speak(self, volume, msg): pass def test_new_with_default_passes(self): class Animal(IAnimal): - def speak(self, volume, msg='hello'): - return '{} ({})'.format(msg, volume) + def speak(self, volume, msg="hello"): + return "{} ({})".format(msg, volume) # abstract subclass class IAnimal2(IAnimal): - def speak(self, volume, msg='hello'): + def speak(self, volume, msg="hello"): pass class Animal3(IAnimal2): - def speak(self, volume, msg='hello'): - return '{} ({})'.format(msg, volume) + def speak(self, volume, msg="hello"): + return "{} ({})".format(msg, volume) a = Animal() b = Animal3() - self.assertEqual(a.speak('loud'), 'hello (loud)') - self.assertEqual(b.speak('loud'), 'hello (loud)') + self.assertEqual(a.speak("loud"), "hello (loud)") + self.assertEqual(b.speak("loud"), "hello (loud)") def test_adding_default_passes(self): class Animal(IAnimal): - def speak(self, volume='loud'): - return 'hello ({})'.format(volume) + def speak(self, volume="loud"): + return "hello ({})".format(volume) a = Animal() - self.assertEqual(a.speak(), 'hello (loud)') + self.assertEqual(a.speak(), "hello (loud)") def test_increasing_required_params_fails(self): # concrete subclass with self.assertRaises(pure_interface.InterfaceError): + class Plant(IPlant): def grow(self, height): return height + 5 # abstract subclass with self.assertRaises(pure_interface.InterfaceError): + class Plant2(IPlant): def grow(self, height): pass @@ -340,9 +353,11 @@ def setUpClass(cls): def test_too_many_passes(self): try: + class Animal(IAnimal): def speak(self, volume, msg): pass + a = Animal() except pure_interface.InterfaceError as exc: - self.fail('Unexpected error {}'.format(exc)) + self.fail("Unexpected error {}".format(exc)) diff --git a/tests/test_generic_support.py b/tests/test_generic_support.py index 11c6509..2cbe740 100644 --- a/tests/test_generic_support.py +++ b/tests/test_generic_support.py @@ -1,8 +1,9 @@ import unittest +from typing import Iterable, List, TypeVar + import pure_interface -from typing import TypeVar, List, Iterable -T = TypeVar('T') +T = TypeVar("T") class IMyInterface(pure_interface.Interface, Iterable[str]): @@ -17,30 +18,28 @@ def foo(self) -> T: class Impl(List[str], IMyInterface): def foo(self): - return 'foo' + return "foo" class StrImpl(List[str], IMyGenericInterface[str]): def foo(self): - return 'foo' + return "foo" class GenericImpl(List[T], IMyGenericInterface[T]): def foo(self): - return T('foo') + return T("foo") class TestGenericSupport(unittest.TestCase): - def test_generics_are_interfaces(self): self.assertTrue(pure_interface.type_is_interface(IMyInterface)) self.assertTrue(pure_interface.type_is_interface(IMyGenericInterface)) def test_can_use_iterable(self): imp = Impl() - imp.append('hello') + imp.append("hello") imp = StrImpl() - imp.append('hello') + imp.append("hello") imp = GenericImpl[int]() imp.append(34) - diff --git a/tests/test_implementation_checks.py b/tests/test_implementation_checks.py index 45597b4..16d86b5 100644 --- a/tests/test_implementation_checks.py +++ b/tests/test_implementation_checks.py @@ -1,12 +1,11 @@ import abc import unittest import warnings +from unittest import mock from pure_interface import * from pure_interface import interface -from unittest import mock - class ADescriptor(object): def __init__(self, value): @@ -88,7 +87,8 @@ def foo(self): class ICrossImplementation(Interface): - """ interface to test class attributes implemented as properties and vice versa """ + """interface to test class attributes implemented as properties and vice versa""" + a = None b = None @@ -107,7 +107,7 @@ def test_instantiation_fails(self): Interface() with self.assertRaises(InterfaceError) as exc: IPlant() - assert 'Interfaces cannot be instantiated' in str(exc.exception) + assert "Interfaces cannot be instantiated" in str(exc.exception) with self.assertRaises(InterfaceError): IAnimal() with self.assertRaises(InterfaceError): @@ -124,29 +124,30 @@ class Concrete(AnInterface): try: c = Concrete() except Exception as exc: - self.fail('Instantiation failed {}'.format(exc)) + self.fail("Instantiation failed {}".format(exc)) def test_concrete_base_detection2(self): class B(object): def __init__(self): - self.foo = 'bar' + self.foo = "bar" with self.assertRaises(InterfaceError): + class Concrete(B, Interface): pass def test_concrete_abc_detection(self): class B(metaclass=abc.ABCMeta): def __init__(self): - self.foo = 'bar' + self.foo = "bar" with self.assertRaises(InterfaceError): + class Concrete(B, Interface): pass def test_interface_abc_detection(self): class IABC(metaclass=abc.ABCMeta): - @abc.abstractmethod def foo(self): pass @@ -164,8 +165,8 @@ class PIEmptyABC(Interface, IABC): self.assertTrue(EmptyABCPI._pi.type_is_interface) self.assertTrue(PIEmptyABC._pi.type_is_interface) - self.assertTrue('foo' in PIEmptyABC._pi.interface_method_names) - self.assertTrue('bar' in PIEmptyABC._pi.interface_attribute_names) + self.assertTrue("foo" in PIEmptyABC._pi.interface_method_names) + self.assertTrue("bar" in PIEmptyABC._pi.interface_attribute_names) with self.assertRaises(InterfaceError): EmptyABCPI() with self.assertRaises(InterfaceError): @@ -173,9 +174,11 @@ class PIEmptyABC(Interface, IABC): def test_can_use_type_methods(self): try: + class MyInterface(Interface): def register(self): pass + except InterfaceError as exc: self.fail(str(exc)) @@ -183,9 +186,11 @@ def test_decorators_not_unwrapped(self): def d(f): def w(): return f() + return w with self.assertRaises(InterfaceError): + class MyInterface(Interface): @d def foo(self): @@ -193,6 +198,7 @@ def foo(self): def test_can_override_func_with_descriptor(self): try: + class MyDescriptor(object): def __init__(self, function): self.__function = function @@ -207,8 +213,9 @@ class Simple(ISimple): @MyDescriptor def foo(): return 1 + except: - self.fail('Overriding function with descriptor failed') + self.fail("Overriding function with descriptor failed") s = Simple() self.assertEqual(s.foo(), 1) @@ -219,7 +226,7 @@ def test_missing_methods_warning(self): # act with warnings.catch_warnings(): - warnings.simplefilter('ignore') + warnings.simplefilter("ignore") class SimpleSimon(ISimple): pass @@ -227,14 +234,15 @@ class SimpleSimon(ISimple): # assert self.assertEqual(len(interface.missing_method_warnings), 1) msg = interface.missing_method_warnings[0] - self.assertIn('SimpleSimon', msg) - self.assertIn('foo', msg) + self.assertIn("SimpleSimon", msg) + self.assertIn("foo", msg) def test_is_development_flag_stops_warnings(self): interface.is_development = False warn = mock.MagicMock() - with mock.patch('warnings.warn', warn): + with mock.patch("warnings.warn", warn): + class SimpleSimon(ISimple): pass @@ -244,7 +252,8 @@ def test_partial_implementation_attribute(self): interface.is_development = True warn = mock.MagicMock() - with mock.patch('warnings.warn', warn): + with mock.patch("warnings.warn", warn): + class SimpleSimon(ISimple): pi_partial_implementation = True @@ -254,16 +263,17 @@ def test_partial_implementation_warning(self): interface.is_development = True warn = mock.MagicMock() - with mock.patch('warnings.warn', warn): + with mock.patch("warnings.warn", warn): + class SimpleSimon(ISimple): pi_partial_implementation = False self.assertEqual(warn.call_count, 1) - self.assertTrue(warn.call_args[0][0].startswith('Partial implementation is indicated')) + self.assertTrue(warn.call_args[0][0].startswith("Partial implementation is indicated")) def test_super_class_properties_detected(self): class HeightDescr(object): - height = ADescriptor('really tall') + height = ADescriptor("really tall") class Test(HeightDescr, IPlant): pass @@ -418,7 +428,7 @@ def height(self): def test_getattr_property_passes(self): class Plant(IPlant): def __getattr__(self, item): - if item == 'height': + if item == "height": return 10 else: raise AttributeError(item) @@ -428,6 +438,7 @@ def __getattr__(self, item): def test_class_and_static_methods(self): try: + class Concrete(IFunkyMethods): @classmethod def acm(cls): @@ -444,6 +455,7 @@ def asm(): @staticmethod def sm(): return 4 + except Exception as exc: self.fail(str(exc)) @@ -460,10 +472,11 @@ def a(self): class TestAttributeImplementations(unittest.TestCase): def test_class_attribute_in_interface(self): - self.assertIn('a', get_interface_attribute_names(IAttribute)) + self.assertIn("a", get_interface_attribute_names(IAttribute)) def test_class_attribute_must_be_none(self): with self.assertRaises(InterfaceError): + class IAttribute2(Interface): a = False @@ -479,7 +492,7 @@ class A(IAttribute): a = A() def test_class_attribute_in_dir(self): - self.assertIn('a', dir(IAttribute)) + self.assertIn("a", dir(IAttribute)) def test_instance_attribute_passes(self): class A(IAttribute): @@ -525,22 +538,23 @@ def test_mock_spec_includes_attrs(self): self.fail("class attribute not mocked") def test_raising_property(self): - """ Issue 23 """ + """Issue 23""" try: a = RaisingProperty() except: - self.fail('Instantiation with property that raises failed') + self.fail("Instantiation with property that raises failed") def test_attr_overridden_with_func(self): # forgotten @property decorator try: + class Function(IAttribute): def a(self): return 2 Function() except: - self.fail('Overriding attribute with function should not crash') + self.fail("Overriding attribute with function should not crash") self.assertEqual(frozenset(), Function._pi.abstractproperties) @@ -555,7 +569,7 @@ class IOther(IA): class A(IA): @property def next(self): - raise RuntimeError('property accessed') + raise RuntimeError("property accessed") class B(A): pass @@ -567,7 +581,8 @@ class C(B, IOther): class TestCrossImplementations(unittest.TestCase): - """ test class attributes implemented as properties and vice versa """ + """test class attributes implemented as properties and vice versa""" + def test_cross_implementations(self): class CrossImplementation(ICrossImplementation): def __init__(self): @@ -582,8 +597,8 @@ def b(self): def d(self): return 4 - self.assertEqual(frozenset(['a', 'c']), CrossImplementation._pi.abstractproperties) - self.assertEqual(['a', 'b', 'c', 'd'], CrossImplementation._pi.interface_attribute_names) + self.assertEqual(frozenset(["a", "c"]), CrossImplementation._pi.abstractproperties) + self.assertEqual(["a", "b", "c", "d"], CrossImplementation._pi.interface_attribute_names) class TestAnnoationChecks(unittest.TestCase): @@ -591,16 +606,16 @@ def test_annotations(self): class IAnnotation(Interface): a: int - self.assertIn('a', get_interface_attribute_names(IAnnotation)) - self.assertIn('a', dir(IAnnotation)) + self.assertIn("a", get_interface_attribute_names(IAnnotation)) + self.assertIn("a", dir(IAnnotation)) def test_annotations2(self): class IAnnotation(Interface): a: int b = None - self.assertIn('a', get_interface_attribute_names(IAnnotation)) - self.assertIn('b', get_interface_attribute_names(IAnnotation)) + self.assertIn("a", get_interface_attribute_names(IAnnotation)) + self.assertIn("b", get_interface_attribute_names(IAnnotation)) def test_works_with_init_subclass_kwargs(self): saved_kwargs = {} diff --git a/tests/test_inheritance.py b/tests/test_inheritance.py index 5ffd9fb..1c69a05 100644 --- a/tests/test_inheritance.py +++ b/tests/test_inheritance.py @@ -27,7 +27,7 @@ def __init__(self): self._height = 10 def get_height(self, units): - return '{} {}'.format(self._height, units) + return "{} {}".format(self._height, units) def set_height(self, height): self._height = height @@ -68,6 +68,7 @@ def test_bad_mixin_class_is_checked(self): pure_interface.set_is_development(True) with self.assertRaises(pure_interface.InterfaceError): + class Growing(BadGrowingMixin, IGrowingThing): pass diff --git a/tests/test_isinstance.py b/tests/test_isinstance.py index d8ca68a..36c5589 100644 --- a/tests/test_isinstance.py +++ b/tests/test_isinstance.py @@ -1,7 +1,7 @@ import io import unittest -from unittest import mock import warnings +from unittest import mock import pure_interface from tests.interface_module import IAnimal @@ -20,7 +20,7 @@ class Animal(object): def test_duck_type_fallback_passes(self): class Animal2(object): def speak(self, volume): - print('hello') + print("hello") @property def height(self): @@ -29,14 +29,14 @@ def height(self): a = Animal2() self.assertFalse(isinstance(a, IAnimal)) with warnings.catch_warnings(): - warnings.simplefilter('ignore') + warnings.simplefilter("ignore") self.assertTrue(IAnimal.provided_by(a)) self.assertIn(Animal2, IAnimal._pi.structural_subclasses) def test_duck_type_fallback_can_fail(self): class Animal2(object): def speak(self, volume): - print('hello') + print("hello") a = Animal2() self.assertFalse(isinstance(a, IAnimal)) @@ -45,7 +45,7 @@ def speak(self, volume): def test_concrete_subclass_check(self): class Cat(IAnimal): def speak(self, volume): - print('meow') + print("meow") @property def height(self): @@ -63,14 +63,14 @@ def test_warning_issued_once(self): class Cat2(object): def speak(self, volume): - print('meow') + print("meow") @property def height(self): return 35 warn = mock.MagicMock() - with mock.patch('warnings.warn', warn): + with mock.patch("warnings.warn", warn): IAnimal.provided_by(Cat2()) IAnimal.provided_by(Cat2()) @@ -81,14 +81,14 @@ def test_warning_not_issued(self): class Cat3(object): def speak(self, volume): - print('meow') + print("meow") @property def height(self): return 35 warn = mock.MagicMock() - with mock.patch('warnings.warn', warn): + with mock.patch("warnings.warn", warn): IAnimal.provided_by(Cat3()) warn.assert_not_called() @@ -98,40 +98,39 @@ def test_warning_contents(self): class Cat4(object): def speak(self, volume): - print('meow') + print("meow") @property def height(self): return 35 s = io.StringIO() - with mock.patch('sys.stderr', new=s): + with mock.patch("sys.stderr", new=s): IAnimal.provided_by(Cat4()) msg = s.getvalue() - self.assertIn('Cat4', msg) + self.assertIn("Cat4", msg) self.assertIn(Cat4.__module__, msg) - self.assertNotIn('pure_interface', msg.split('\n')[0]) - self.assertIn('IAnimal', msg) + self.assertNotIn("pure_interface", msg.split("\n")[0]) + self.assertIn("IAnimal", msg) def test_warning_contents_adapt(self): pure_interface.set_is_development(True) class Cat5(object): def speak(self, volume): - print('meow') + print("meow") @property def height(self): return 35 s = io.StringIO() - with mock.patch('sys.stderr', new=s): + with mock.patch("sys.stderr", new=s): IAnimal.adapt(Cat5(), allow_implicit=True) msg = s.getvalue() - self.assertIn('Cat5', msg) + self.assertIn("Cat5", msg) self.assertIn(Cat5.__module__, msg) - self.assertNotIn('pure_interface', msg.split('\n')[0]) - self.assertIn('IAnimal', msg) - + self.assertNotIn("pure_interface", msg.split("\n")[0]) + self.assertIn("IAnimal", msg) diff --git a/tests/test_meta_classes.py b/tests/test_meta_classes.py index 870d887..5fb2e5b 100644 --- a/tests/test_meta_classes.py +++ b/tests/test_meta_classes.py @@ -60,7 +60,7 @@ def test_submeta_class(self): innocent_bystander = InnocentBystander() innocent_bystander.method() except Exception as exc: - self.fail('No exception expected. Got\n' + str(exc)) + self.fail("No exception expected. Got\n" + str(exc)) def test_submeta_class_with_interface(self): with self.assertRaises(TypeError): @@ -73,18 +73,18 @@ def test_bystander(self): def test_dir_subclass(self): listing = dir(SubclassWithInterface) - self.assertIn('method2', listing) - self.assertIn('method', listing) + self.assertIn("method2", listing) + self.assertIn("method", listing) def test_dir_subsubclass(self): listing = dir(SubSubclassWithInterface) - self.assertIn('method2', listing) - self.assertIn('method', listing) - self.assertIn('foo', listing) + self.assertIn("method2", listing) + self.assertIn("method", listing) + self.assertIn("foo", listing) def test_dir_subsubsubclass(self): listing = dir(SubSubSubclassWithInterface) - self.assertIn('method2', listing) - self.assertIn('method', listing) - self.assertIn('foo', listing) - self.assertIn('bar', listing) + self.assertIn("method2", listing) + self.assertIn("method", listing) + self.assertIn("foo", listing) + self.assertIn("bar", listing) diff --git a/tests/test_module_funcs.py b/tests/test_module_funcs.py index e190a84..f44705b 100644 --- a/tests/test_module_funcs.py +++ b/tests/test_module_funcs.py @@ -59,26 +59,26 @@ def test_type_is_interface(self): self.assertFalse(type_is_interface(object)) self.assertFalse(type_is_interface(Cat)) self.assertFalse(type_is_interface(Car)) - self.assertFalse(type_is_interface('hello')) + self.assertFalse(type_is_interface("hello")) def test_get_interface_method_names(self): - self.assertEqual(get_interface_method_names(IAnimal), {'speak'}) - self.assertEqual(get_interface_method_names(ILandAnimal), {'speak', 'num_legs'}) + self.assertEqual(get_interface_method_names(IAnimal), {"speak"}) + self.assertEqual(get_interface_method_names(ILandAnimal), {"speak", "num_legs"}) self.assertEqual(get_interface_method_names(Cat), set()) self.assertEqual(get_interface_method_names(Car), set()) - self.assertEqual(get_interface_method_names('hello'), set()) + self.assertEqual(get_interface_method_names("hello"), set()) def test_get_interface_attribute_names(self): - self.assertEqual(get_interface_attribute_names(IAnimal), {'weight', 'species'}) - self.assertEqual(get_interface_attribute_names(ILandAnimal), {'weight', 'species', 'height'}) + self.assertEqual(get_interface_attribute_names(IAnimal), {"weight", "species"}) + self.assertEqual(get_interface_attribute_names(ILandAnimal), {"weight", "species", "height"}) self.assertEqual(get_interface_attribute_names(Cat), set()) - self.assertEqual(get_interface_attribute_names('hello'), set()) + self.assertEqual(get_interface_attribute_names("hello"), set()) def test_get_interface_names(self): - self.assertEqual(get_interface_names(IAnimal), {'speak', 'weight', 'species'}) - self.assertEqual(get_interface_names(ILandAnimal), {'speak', 'weight', 'species', 'height', 'num_legs'}) + self.assertEqual(get_interface_names(IAnimal), {"speak", "weight", "species"}) + self.assertEqual(get_interface_names(ILandAnimal), {"speak", "weight", "species", "height", "num_legs"}) self.assertEqual(get_interface_names(Cat), set()) - self.assertEqual(get_interface_names('hello'), set()) + self.assertEqual(get_interface_names("hello"), set()) def test_get_type_interfaces(self): self.assertEqual(get_type_interfaces(IAnimal), [IAnimal]) @@ -88,4 +88,4 @@ def test_get_type_interfaces(self): self.assertEqual(get_type_interfaces(Mongrel), [ILandAnimal, IAnimal]) self.assertEqual(get_type_interfaces(Car), []) self.assertEqual(get_type_interfaces(len), []) - self.assertEqual(get_type_interfaces('hello'), []) + self.assertEqual(get_type_interfaces("hello"), []) diff --git a/tests/test_no_content_checks.py b/tests/test_no_content_checks.py index 37b4525..eb82a69 100644 --- a/tests/test_no_content_checks.py +++ b/tests/test_no_content_checks.py @@ -1,8 +1,7 @@ import abc -import pure_interface - import unittest +import pure_interface import pure_interface.errors @@ -13,7 +12,7 @@ def speak(self, volume): pass def move(self, to): - """ a comment """ + """a comment""" pass def sleep(self, duration): @@ -39,12 +38,12 @@ def speak(self, volume): raise NotImplementedError() def move(self, to): - """ a comment """ - raise NotImplementedError('subclass must provide') + """a comment""" + raise NotImplementedError("subclass must provide") def sleep(self, duration): """a comment""" - msg = 'msg {}'.format(self.__class__.__name__) + msg = "msg {}".format(self.__class__.__name__) raise NotImplementedError(msg) def test_async_function_passes(self): @@ -53,8 +52,8 @@ async def speak(self, volume): pass async def move(self, to): - """ a comment """ - raise NotImplementedError('subclass must provide') + """a comment""" + raise NotImplementedError("subclass must provide") async def sleep(self, duration): """a comment""" @@ -62,38 +61,43 @@ async def sleep(self, duration): def test_raise_other_fails(self): with self.assertRaises(pure_interface.InterfaceError): + class INotAnimal(pure_interface.Interface): def bad_method(self): """a comment""" - msg = 'msg {}'.format(self.__class__.__name__) + msg = "msg {}".format(self.__class__.__name__) raise RuntimeError(msg) def test_function_with_body_fails(self): with self.assertRaises(pure_interface.errors.InterfaceError): + class IAnimal(pure_interface.Interface): def speak(self, volume): if volume > 0: - print('hello' + '!' * int(volume)) + print("hello" + "!" * int(volume)) def test_abstract_function_with_body_fails(self): with self.assertRaises(pure_interface.errors.InterfaceError): + class IAnimal(pure_interface.Interface): @abc.abstractmethod def speak(self, volume): if volume > 0: - print('hello' + '!' * int(volume)) + print("hello" + "!" * int(volume)) def test_abstract_classmethod_with_body_fails(self): with self.assertRaises(pure_interface.errors.InterfaceError): + class IAnimal(pure_interface.Interface): @classmethod @abc.abstractmethod def speak(cls, volume): if volume > 0: - print('hello' + '!' * int(volume)) + print("hello" + "!" * int(volume)) def test_property_with_body_fails(self): with self.assertRaises(pure_interface.errors.InterfaceError): + class IAnimal(pure_interface.Interface): @property def height(self): diff --git a/tests/test_singledispatch.py b/tests/test_singledispatch.py index 01d3bc0..9ae0013 100644 --- a/tests/test_singledispatch.py +++ b/tests/test_singledispatch.py @@ -1,6 +1,6 @@ +import unittest from functools import singledispatchmethod from typing import Any -import unittest import pure_interface @@ -12,7 +12,7 @@ class IPerson(pure_interface.Interface): def greet(self, other_person: Any) -> str: pass - self.assertSetEqual(pure_interface.get_interface_method_names(IPerson), {'greet'}) + self.assertSetEqual(pure_interface.get_interface_method_names(IPerson), {"greet"}) def test_single_dispatch_checked(self): class IPerson(pure_interface.Interface): @@ -21,8 +21,8 @@ def greet(self) -> str: pure_interface.set_is_development(True) with self.assertRaises(pure_interface.InterfaceError): + class Person(IPerson): @singledispatchmethod def greet(self, other_person: Any) -> str: pass - diff --git a/tests/test_sub_interfaces.py b/tests/test_sub_interfaces.py index 86ff16c..4a76563 100644 --- a/tests/test_sub_interfaces.py +++ b/tests/test_sub_interfaces.py @@ -34,9 +34,8 @@ def larger_int(i): @dataclasses.dataclass class Larger(ILarger): - def e(self): - return 'e' + return "e" def f(self, arg1, arg2, *kwargs): return arg1, arg2, kwargs @@ -58,49 +57,58 @@ def test_large_adapts(self): try: s = ISmaller.adapt(4) except pure_interface.InterfaceError: - self.fail('ISmaller does not adapt ILarger') + self.fail("ISmaller does not adapt ILarger") self.assertEqual(s.b, 4) def test_fails_when_empty(self): - with self.assertRaisesRegex(pure_interface.InterfaceError, 'Sub-interface IEmpty is empty'): + with self.assertRaisesRegex(pure_interface.InterfaceError, "Sub-interface IEmpty is empty"): + @pure_interface.sub_interface_of(ILarger) class IEmpty(pure_interface.Interface): pass def test_fails_when_arg_not_interface(self): - with self.assertRaisesRegex(pure_interface.InterfaceError, - "sub_interface_of argument is not an interface type"): + with self.assertRaisesRegex( + pure_interface.InterfaceError, "sub_interface_of argument is not an interface type" + ): + @pure_interface.sub_interface_of(int) class ISubInterface(pure_interface.Interface): def __sub__(self, other): pass def test_fails_when_class_not_interface(self): - with self.assertRaisesRegex(pure_interface.InterfaceError, - 'class decorated by sub_interface_of must be an interface type'): + with self.assertRaisesRegex( + pure_interface.InterfaceError, "class decorated by sub_interface_of must be an interface type" + ): + @pure_interface.sub_interface_of(ILarger) class NotInterface: pass def test_fails_when_attr_mismatch(self): - with self.assertRaisesRegex(pure_interface.InterfaceError, - 'NotSmaller has attributes that are not on ILarger: z'): + with self.assertRaisesRegex( + pure_interface.InterfaceError, "NotSmaller has attributes that are not on ILarger: z" + ): + @pure_interface.sub_interface_of(ILarger) class INotSmaller(pure_interface.Interface): z: int def test_fails_when_methods_mismatch(self): - with self.assertRaisesRegex(pure_interface.InterfaceError, - 'NotSmaller has methods that are not on ILarger: x'): + with self.assertRaisesRegex(pure_interface.InterfaceError, "NotSmaller has methods that are not on ILarger: x"): + @pure_interface.sub_interface_of(ILarger) class INotSmaller(pure_interface.Interface): def x(self): pass def test_fails_when_signatures_mismatch(self): - with self.assertRaisesRegex(pure_interface.InterfaceError, - 'Signature of method f on ILarger and INotSmaller must match exactly'): + with self.assertRaisesRegex( + pure_interface.InterfaceError, "Signature of method f on ILarger and INotSmaller must match exactly" + ): + @pure_interface.sub_interface_of(ILarger) class INotSmaller(pure_interface.Interface): def f(self, arg1, arg2, foo=3): diff --git a/tests/test_tracker.py b/tests/test_tracker.py index 7ab3055..7eadc57 100644 --- a/tests/test_tracker.py +++ b/tests/test_tracker.py @@ -1,7 +1,7 @@ import unittest from pure_interface import * -from pure_interface import adapts, AdapterTracker +from pure_interface import AdapterTracker, adapts class ISpeaker(Interface): @@ -14,7 +14,7 @@ class ISleeper(Interface): class Talker: - def __init__(self, saying='talk'): + def __init__(self, saying="talk"): self._saying = saying def talk(self): @@ -28,7 +28,7 @@ def __init__(self, is_asleep=False): @adapts(Talker, ISleeper) def talker_to_sleeper(talker): - return Sleeper(talker.talk() == 'zzz') + return Sleeper(talker.talk() == "zzz") @adapts(Talker) @@ -62,13 +62,13 @@ def test_adapt_multiple_interfaces(self): def test_adapt_multiple_instances(self): tracker = AdapterTracker() - t1 = Talker('hello') - t2 = Talker('zzz') + t1 = Talker("hello") + t2 = Talker("zzz") speaker1 = tracker.adapt(t1, ISpeaker) speaker2 = tracker.adapt(t2, ISpeaker) - self.assertEqual('hello', speaker1.speak(5)) - self.assertEqual('zzz', speaker2.speak(5)) + self.assertEqual("hello", speaker1.speak(5)) + self.assertEqual("zzz", speaker2.speak(5)) def test_mapping_factory_is_used(self): mocks = [] @@ -78,7 +78,7 @@ def factory(): return mocks[-1] tracker = AdapterTracker(mapping_factory=factory) - t = Talker('hello') + t = Talker("hello") tracker.adapt(t, ISpeaker) self.assertTrue(len(mocks) > 1) @@ -94,7 +94,7 @@ def test_adapt_or_none_works(self): def test_adapt_or_none_returns_none(self): tracker = AdapterTracker() - speaker = tracker.adapt_or_none('hello', ISpeaker) + speaker = tracker.adapt_or_none("hello", ISpeaker) self.assertIsNone(speaker) @@ -107,4 +107,3 @@ def test_clear(self): speaker2 = tracker.adapt_or_none(t, ISpeaker) self.assertIsNot(speaker1, speaker2) -