Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# html_static_path = ["_static"]

# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
Expand Down
2 changes: 2 additions & 0 deletions docs/errors.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. _error-detection:

Error Detection
---------------

Expand Down
4 changes: 1 addition & 3 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
:mod:`interface` is a library for declaring interfaces and for statically
asserting that classes implement those interfaces. It provides stricter
semantics than Python's built-in :mod:`abc` module, and it aims to produce
`exceptionally useful error messages`_ when interfaces aren't satisfied.
:ref:`exceptionally useful error messages <error-detection>` when interfaces aren't satisfied.

:mod:`interface` supports Python 2.7 and Python 3.4+.

Expand Down Expand Up @@ -50,5 +50,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

.. _`exceptionally useful error messages` : errors.html
39 changes: 39 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,42 @@ of ``get_all``:

Consider changing ReadOnlyMapping.get_all or making these attributes part of ReadOnlyMapping.
class ReadOnlyMapping(interface.Interface):

Interface Subclassing
~~~~~~~~~~~~~~~~~~~~~

Interfaces can inherit requirements from other interfaces via subclassing. For
example, if we want to create interfaces for read-write and read-only mappings,
we could do so as follows:

.. code-block:: python

class ReadOnlyMapping(interface.Interface):
def get(self, key):
pass

def keys(self):
pass


class ReadWriteMapping(ReadOnlyMapping):

def set(self, key, value):
pass

def delete(self, key):
pass


An interface that subclasses from another interface inherits all the function
signature requirements from its parent interface. In the example above, a class
implementing ``ReadWriteMapping`` would have to implement ``get``, ``keys``,
``set``, and ``delete``.

.. warning::

Subclassing from an interface is not the same as implementing an
interface. Subclassing from an interface **creates a new interface** that
adds additional methods to the parent interface. Implementing an interface
creates a new class whose method signatures must be compatible with the
interface being implemented.
13 changes: 13 additions & 0 deletions interface/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,16 @@ def sliding_window(iterable, n):
for item in it:
items.append(item)
yield tuple(items)


def merge(dicts):
dicts = list(dicts)
if len(dicts) == 0:
return {}
elif len(dicts) == 1:
return dicts[0]
else:
out = dicts[0].copy()
for other in dicts[1:]:
out.update(other)
return out
73 changes: 63 additions & 10 deletions interface/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from .compat import raise_from, with_metaclass
from .default import default, warn_if_defaults_use_non_interface_members
from .formatting import bulleted_list
from .functional import complement, keyfilter, valfilter
from .functional import complement, keyfilter, merge, valfilter
from .typecheck import compatible
from .typed_signature import TypedSignature
from .utils import is_a, unique
Expand All @@ -21,8 +21,22 @@ class InvalidImplementation(TypeError):
"""


class InvalidSubInterface(TypeError):
"""
Raised when on attempt to define a subclass of an interface that's not
compatible with the parent definition.
"""


CLASS_ATTRIBUTE_WHITELIST = frozenset(
["__doc__", "__module__", "__name__", "__qualname__", "__weakref__"]
[
"__doc__",
"__module__",
"__name__",
"__qualname__",
"__weakref__",
"_INTERFACE_IGNORE_MEMBERS",
]
)

is_interface_field_name = complement(CLASS_ATTRIBUTE_WHITELIST.__contains__)
Expand Down Expand Up @@ -72,6 +86,14 @@ def _conflicting_defaults(typename, conflicts):
return InvalidImplementation(message)


def _merge_parent_signatures(bases):
return merge(filter(None, (getattr(b, "_signatures") for b in bases)))


def _merge_parent_defaults(bases):
return merge(filter(None, (getattr(b, "_defaults") for b in bases)))


class InterfaceMeta(type):
"""
Metaclass for interfaces.
Expand All @@ -80,22 +102,43 @@ class InterfaceMeta(type):
"""

def __new__(mcls, name, bases, clsdict):
signatures = {}
defaults = {}
for k, v in keyfilter(is_interface_field_name, clsdict).items():
signatures = _merge_parent_signatures(bases)
defaults = _merge_parent_defaults(bases)
ignored = clsdict.get("_INTERFACE_IGNORE_MEMBERS", set())

for field, v in keyfilter(is_interface_field_name, clsdict).items():
if field in ignored:
continue

try:
signatures[k] = TypedSignature(v)
signature = TypedSignature(v)
except TypeError as e:
errmsg = (
"Couldn't parse signature for field "
"{iface_name}.{fieldname} of type {attrtype}.".format(
iface_name=name, fieldname=k, attrtype=getname(type(v)),
iface_name=name, fieldname=field, attrtype=getname(type(v)),
)
)
raise_from(TypeError(errmsg), e)

# If we already have a signature for this field from a parent, then
# our new signature must be a subtype of the parent signature, so
# that any valid call to the new signature must also be a valid
# call to the parent signature.
if field in signatures and not compatible(signature, signatures[field]):
conflicted = signatures[field]
raise InvalidSubInterface(
"\nInterface field {new}.{field} conflicts with inherited field of "
"the same name.\n"
" - {field}{new_sig} != {field}{old_sig}".format(
new=name, field=field, new_sig=signature, old_sig=conflicted,
)
)
else:
signatures[field] = signature

if isinstance(v, default):
defaults[k] = v
defaults[field] = v

warn_if_defaults_use_non_interface_members(
name, defaults, set(signatures.keys())
Expand Down Expand Up @@ -139,7 +182,7 @@ def _diff_signatures(self, type_):
if not issubclass(impl_sig.type, iface_sig.type):
mistyped[name] = impl_sig.type

if not compatible(impl_sig.signature, iface_sig.signature):
if not compatible(impl_sig, iface_sig):
mismatched[name] = impl_sig

return missing, mistyped, mismatched
Expand Down Expand Up @@ -258,6 +301,9 @@ def _format_mismatched_methods(self, mismatched):
)


empty_set = frozenset([])


class Interface(with_metaclass(InterfaceMeta)):
"""
Base class for interface definitions.
Expand Down Expand Up @@ -313,6 +359,10 @@ def delete(self, key):
:func:`implements`
"""

# Don't consider these members part of the interface definition for
# children of `Interface`.
_INTERFACE_IGNORE_MEMBERS = {"__new__", "from_class"}

def __new__(cls, *args, **kwargs):
raise TypeError("Can't instantiate interface %s" % getname(cls))

Expand Down Expand Up @@ -349,7 +399,10 @@ def from_class(cls, existing_class, subset=None, name=None):
)


empty_set = frozenset([])
# Signature requirements are inherited, so make sure the base interface doesn't
# require any methods of children.
assert Interface._signatures == {}
assert Interface._defaults == {}


class ImplementsMeta(type):
Expand Down
19 changes: 9 additions & 10 deletions interface/tests/_py3_typecheck_tests.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
from inspect import signature

from ..typecheck import compatible
from ..typed_signature import TypedSignature


def test_allow_new_params_with_defaults_with_kwonly():
@signature
@TypedSignature
def iface(a, b, c): # pragma: nocover
pass

@signature
@TypedSignature
def impl(a, b, c, d=3, e=5, *, f=5): # pragma: nocover
pass

Expand All @@ -17,11 +16,11 @@ def impl(a, b, c, d=3, e=5, *, f=5): # pragma: nocover


def test_allow_reorder_kwonlys():
@signature
@TypedSignature
def foo(a, b, c, *, d, e, f): # pragma: nocover
pass

@signature
@TypedSignature
def bar(a, b, c, *, f, d, e): # pragma: nocover
pass

Expand All @@ -30,11 +29,11 @@ def bar(a, b, c, *, f, d, e): # pragma: nocover


def test_allow_default_changes():
@signature
@TypedSignature
def foo(a, b, c=3, *, d=1, e, f): # pragma: nocover
pass

@signature
@TypedSignature
def bar(a, b, c=5, *, f, e, d=12): # pragma: nocover
pass

Expand All @@ -43,11 +42,11 @@ def bar(a, b, c=5, *, f, e, d=12): # pragma: nocover


def test_disallow_kwonly_to_positional():
@signature
@TypedSignature
def foo(a, *, b): # pragma: nocover
pass

@signature
@TypedSignature
def bar(a, b): # pragma: nocover
pass

Expand Down
Loading