Last active
August 29, 2015 14:21
-
-
Save ianschenck/3bb9a6720ce5adefea96 to your computer and use it in GitHub Desktop.
interfaces
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import interface | |
class IFoo(interface.Interface): | |
def foo(self): | |
"""foo this object.""" | |
class IBar(interface.Interface): | |
"""IBar provides a `bar` method.""" | |
def bar(self, a, b=None): | |
"""bar this object.""" | |
# If the interface isn't implemented, throws an exception at module initialization time. | |
@interface.implements(IFoo, IBar) | |
class FooBar(object): | |
def foo(self): | |
# do something | |
pass | |
def bar(self, a, b=None, c=None): | |
# do something else | |
pass | |
assert issubclass(FooBar, IFoo) # True | |
assert issubclass(FooBar, IBar) # True | |
# Instance checks. | |
assert isinstance(FooBar(), IFoo) # True | |
assert isinstance(FooBar(), IBar) # True | |
# You can combine interfaces. | |
class IFooBar(IFoo, IBar): | |
pass | |
interface.implements(IFooBar)(FooBar) | |
# And it works with properties. | |
class IBaz(interface.Interface): | |
name = property() | |
value = property() | |
@interface.implements(IBaz) | |
class Baz(object): | |
name = "Baz" | |
@property | |
def value(self): | |
return 42 | |
# But you don't *need* implements. And interfaces don't need to be | |
# explicit. | |
class ReadlineCloser(interface.Interface): | |
def readline(self, size=0): | |
"""Read up to `size` bytes or until a newline.""" | |
def close(self): | |
"""Close this file.""" | |
with open('somefile.txt') as f: | |
assert isinstance(f, ReadlineCloser) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import abc | |
import inspect | |
__all__ = ['Interface', 'implements'] | |
class Interface(object): | |
"""Interface is the root of all interfaces. | |
To declare an interface, sub-class Interface and define | |
placeholder methods and properties. Any class properly | |
implementing the interface will return true for `issubclass`, and | |
objects implementing the interface will return True for | |
`isinstance`. An implementing class should not sub-class an | |
interface. See the `@implements` decorator for interface checking | |
at module initialization. | |
""" | |
__metaclass__ = abc.ABCMeta | |
@classmethod | |
def __subclasshook__(cls, C): | |
errors = check_implemented(C, cls) | |
return len(errors) == 0 or NotImplemented | |
IGNORED = set(x[0] for x in inspect.getmembers(Interface)) | |
def implements(*interfaces): | |
"""Check if the decorated class implements all `interfaces`. | |
:type interfaces: list[Interface] | |
:raises NotImplementedError: if an interface is not met. | |
""" | |
def inner(cls): | |
for interface in interfaces: | |
if not issubclass(cls, interface): | |
errors = check_implemented(cls, interface) | |
raise NotImplementedError("\n".join(errors)) | |
return cls | |
return inner | |
def check_implemented(cls, interface): | |
"""Check if a class implements a given interface. | |
:type cls: type | |
:type interface: type | |
""" | |
def _methods(c): | |
return (inspect.ismethod(c) | |
or inspect.isfunction(c) | |
or inspect.ismethoddescriptor(c)) | |
def _props(c): | |
return not _methods(c) | |
interface_funcs = dict(inspect.getmembers(interface, _methods)) | |
cls_funcs = dict(inspect.getmembers(cls, _methods)) | |
errors = [] | |
for name, func in interface_funcs.items(): | |
if name in IGNORED: | |
continue | |
if name not in cls_funcs: | |
errors.append("%s method not implemented" % name) | |
continue | |
error = func_satifies(cls_funcs[name], func) | |
if error is not None: | |
errors.append(error) | |
continue | |
# Check properties | |
interface_props = set(x[0] for x in inspect.getmembers(interface, _props)) | |
cls_props = set(x[0] for x in inspect.getmembers(cls, _props)) | |
unimplemented_props = interface_props - IGNORED - cls_props | |
for prop in unimplemented_props: | |
errors.append("%s property not found" % prop) | |
return errors | |
def func_satifies(cls_func, iface_func): | |
"""Determines if method `cls_func` satisfies `interface_func`. | |
This is not a symmetric comparison, since we have to accept the | |
implications of variadic functions (via `*args` and `*kwargs`) and | |
additional arguments on an implementation that may be provided | |
with defaults. | |
""" | |
# It is impossible to inspect built-in methods, so be generous | |
# and assume they fit. | |
if inspect.ismethoddescriptor(cls_func) and inspect.isroutine(cls_func): | |
return | |
cls_func_spec = inspect.getargspec(cls_func) | |
iface_func_spec = inspect.getargspec(iface_func) | |
# If an interface requires variadic, then the implementation must. | |
if (iface_func_spec.varargs is not None) and (cls_func_spec.varargs is None): | |
return "%s requires implementation to accept *args" % ( | |
iface_func.func_name) | |
if (iface_func_spec.keywords is not None) and (cls_func_spec.keywords is None): | |
return "%s requires implementation to accept **kwargs" % ( | |
iface_func.func_name) | |
# Positional, required arguments must match only in number. Note: | |
# that's not actually true, these parameters could be referenced | |
# by name, but it's much more common for them to be used | |
# positionally. If the implementor of an interface wants to really | |
# do it right, they should keep the names identical as well. | |
iface_required = len( | |
iface_func_spec.args[0: ( | |
len(iface_func_spec.args) - len(iface_func_spec.defaults or []))]) | |
cls_required = len( | |
cls_func_spec.args[0: ( | |
len(cls_func_spec.args) - len(cls_func_spec.defaults or []))]) | |
if iface_required != cls_required: | |
return "%s requires %d positional arguments, %d given" % ( | |
iface_func.func_name, iface_required, cls_required) | |
# Arguments that are optional always follow positional | |
# arguments. The only constraint here is that the implementation | |
# duplicates these arguments in the same order (but may add more | |
# after). | |
iface_optional = tuple(iface_func_spec.args[iface_required:]) | |
cls_optional = tuple(cls_func_spec.args[cls_required:]) | |
if iface_optional != cls_optional[:len(iface_optional)]: | |
return "%s requires optional arguments %s" % ( | |
iface_func.func_name, iface_optional) | |
return None |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
it | |
has | |
lines |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment