Skip to content

Commit

Permalink
Merge pull request #106 from seequent/type-hints
Browse files Browse the repository at this point in the history
Fixes type hint, permits & handles singledispatchmethod
  • Loading branch information
tim-mitchell authored Apr 28, 2024
2 parents de7e14f + f55402e commit 5360334
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 14 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,4 @@ ENV/

# build output
dist/
/.pypirc
2 changes: 1 addition & 1 deletion pure_interface/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
18 changes: 11 additions & 7 deletions pure_interface/adaption.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -110,16 +114,16 @@ 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):
return annotation
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):
Expand Down
20 changes: 14 additions & 6 deletions pure_interface/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions tests/test_adaption.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 28 additions & 0 deletions tests/test_singledispatch.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 5360334

Please sign in to comment.