diff --git a/.gitignore b/.gitignore index 8e1a225..de0ddf4 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,4 @@ ENV/ # build output dist/ +/.pypirc diff --git a/pure_interface/__init__.py b/pure_interface/__init__.py index 9a21b77..7515c9a 100644 --- a/pure_interface/__init__.py +++ b/pure_interface/__init__.py @@ -7,4 +7,4 @@ from .adaption import adapts, register_adapter, AdapterTracker, adapt_args from .delegation import Delegate -__version__ = '8.0.1' +__version__ = '8.0.2' diff --git a/pure_interface/adaption.py b/pure_interface/adaption.py index 3430acb..78819b3 100644 --- a/pure_interface/adaption.py +++ b/pure_interface/adaption.py @@ -3,7 +3,7 @@ import functools import inspect import types -from typing import Any, Type, Callable, Optional +from typing import Any, Type, TypeVar, Callable, Optional, Union import typing import warnings @@ -46,16 +46,20 @@ 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 + + def register_adapter( - adapter: Callable[[Type], Type[Interface]], - from_type: Type, + 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. :param from_type: a type to adapt from - :param to_interface: a (non-concrete) Interface subclass to adapt to. + :param to_interface: an Interface class to adapt to. """ if not callable(adapter): raise AdaptionError('adapter must be callable') @@ -110,7 +114,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 - a typing.Union[interface, None] a.k.a. Optional[interface]. Lets be nice and support those too. + a Union[interface, None] a.k.a. Optional[interface]. Lets be nice and support those too. """ try: if issubclass(annotation, Interface): @@ -118,8 +122,8 @@ def _interface_from_anno(annotation: Any) -> Optional[InterfaceType]: except TypeError: pass if hasattr(annotation, '__origin__') and hasattr(annotation, '__args__'): - # could be a typing.Union - if annotation.__origin__ is not typing.Union: + # could be a Union + if annotation.__origin__ is not Union: return None for arg_type in annotation.__args__: if type_is_interface(arg_type): diff --git a/pure_interface/interface.py b/pure_interface/interface.py index f7e5ad1..1e46e73 100644 --- a/pure_interface/interface.py +++ b/pure_interface/interface.py @@ -7,6 +7,7 @@ from abc import abstractclassmethod, abstractmethod, abstractstaticmethod import collections import dis +import functools import inspect from inspect import Parameter, signature, Signature import sys @@ -382,16 +383,21 @@ def _ensure_everything_is_abstract(attributes): func = value.__func__ functions.append(func) interface_method_signatures[name] = signature(func) - value = abstractstaticmethod(func) + value = staticmethod(abstractmethod(func)) elif isinstance(value, classmethod): func = value.__func__ interface_method_signatures[name] = signature(func) functions.append(func) - value = abstractclassmethod(func) + value = classmethod(abstractmethod(func)) elif isinstance(value, types.FunctionType): functions.append(value) interface_method_signatures[name] = signature(value) value = abstractmethod(value) + elif isinstance(value, functools.singledispatchmethod): + func = value.func + functions.append(func) + interface_method_signatures[name] = signature(func) + value = func # ignore the singledispatchmethod decorator elif isinstance(value, property): interface_attribute_names.append(name) functions.extend([value.fget, value.fset, value.fdel]) # may contain Nones @@ -405,10 +411,10 @@ def _ensure_everything_is_abstract(attributes): def _ensure_annotations(names, namespace, base_interfaces): # annotations need to be kept in order for dataclass decorator # we only want dataclass annotations for attributes that don't already exist - annotations = {} - base_annos = {} + annotations: Dict[str, Any] = {} + base_annos: Dict[str, Any] = {} for base in reversed(base_interfaces): - base_annos.update(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) @@ -422,13 +428,15 @@ def _check_method_signatures(attributes, clsname, interface_method_signatures): if name not in attributes: continue value = attributes[name] - if not isinstance(value, (staticmethod, classmethod, types.FunctionType)): + if not isinstance(value, (staticmethod, classmethod, types.FunctionType, functools.singledispatchmethod)): if _is_descriptor(value): continue else: raise InterfaceError('Interface method over-ridden with non-method') if isinstance(value, (staticmethod, classmethod)): func = value.__func__ + elif isinstance(value, functools.singledispatchmethod): + func = value.func else: func = value func_sig = signature(func) diff --git a/tests/test_adaption.py b/tests/test_adaption.py index d687acc..05b29e8 100644 --- a/tests/test_adaption.py +++ b/tests/test_adaption.py @@ -337,6 +337,7 @@ def test_adapter_to_sub_interface_used(self): def test_adapter_preference(self): """ adapt should prefer interface adapter over sub-interface adapter """ + class IA(Interface): foo = None diff --git a/tests/test_singledispatch.py b/tests/test_singledispatch.py new file mode 100644 index 0000000..01d3bc0 --- /dev/null +++ b/tests/test_singledispatch.py @@ -0,0 +1,28 @@ +from functools import singledispatchmethod +from typing import Any +import unittest + +import pure_interface + + +class TestSingleDispatch(unittest.TestCase): + def test_single_dispatch_allowed(self): + class IPerson(pure_interface.Interface): + @singledispatchmethod + def greet(self, other_person: Any) -> str: + pass + + self.assertSetEqual(pure_interface.get_interface_method_names(IPerson), {'greet'}) + + def test_single_dispatch_checked(self): + class IPerson(pure_interface.Interface): + def greet(self) -> str: + pass + + pure_interface.set_is_development(True) + with self.assertRaises(pure_interface.InterfaceError): + class Person(IPerson): + @singledispatchmethod + def greet(self, other_person: Any) -> str: + pass +