Skip to content

Instantly share code, notes, and snippets.

@ianschenck
Last active August 29, 2015 14:21
Show Gist options
  • Save ianschenck/3bb9a6720ce5adefea96 to your computer and use it in GitHub Desktop.
Save ianschenck/3bb9a6720ce5adefea96 to your computer and use it in GitHub Desktop.
interfaces
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)
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment