Skip to content

Commit 4c4eb73

Browse files
authored
Merge pull request #35 from ssanderson/sub-interfaces
ENH: Add subinterfaces.
2 parents 9ed44ec + 80d5719 commit 4c4eb73

File tree

12 files changed

+299
-38
lines changed

12 files changed

+299
-38
lines changed

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@
9292
# Add any paths that contain custom static files (such as style sheets) here,
9393
# relative to this directory. They are copied after the builtin static files,
9494
# so a file named "default.css" will overwrite the builtin "default.css".
95-
html_static_path = ["_static"]
95+
# html_static_path = ["_static"]
9696

9797
# Custom sidebar templates, must be a dictionary that maps document names
9898
# to template names.

docs/errors.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.. _error-detection:
2+
13
Error Detection
24
---------------
35

docs/index.rst

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
:mod:`interface` is a library for declaring interfaces and for statically
55
asserting that classes implement those interfaces. It provides stricter
66
semantics than Python's built-in :mod:`abc` module, and it aims to produce
7-
`exceptionally useful error messages`_ when interfaces aren't satisfied.
7+
:ref:`exceptionally useful error messages <error-detection>` when interfaces aren't satisfied.
88

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

@@ -50,5 +50,3 @@ Indices and tables
5050
* :ref:`genindex`
5151
* :ref:`modindex`
5252
* :ref:`search`
53-
54-
.. _`exceptionally useful error messages` : errors.html

docs/usage.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,42 @@ of ``get_all``:
172172

173173
Consider changing ReadOnlyMapping.get_all or making these attributes part of ReadOnlyMapping.
174174
class ReadOnlyMapping(interface.Interface):
175+
176+
Interface Subclassing
177+
~~~~~~~~~~~~~~~~~~~~~
178+
179+
Interfaces can inherit requirements from other interfaces via subclassing. For
180+
example, if we want to create interfaces for read-write and read-only mappings,
181+
we could do so as follows:
182+
183+
.. code-block:: python
184+
185+
class ReadOnlyMapping(interface.Interface):
186+
def get(self, key):
187+
pass
188+
189+
def keys(self):
190+
pass
191+
192+
193+
class ReadWriteMapping(ReadOnlyMapping):
194+
195+
def set(self, key, value):
196+
pass
197+
198+
def delete(self, key):
199+
pass
200+
201+
202+
An interface that subclasses from another interface inherits all the function
203+
signature requirements from its parent interface. In the example above, a class
204+
implementing ``ReadWriteMapping`` would have to implement ``get``, ``keys``,
205+
``set``, and ``delete``.
206+
207+
.. warning::
208+
209+
Subclassing from an interface is not the same as implementing an
210+
interface. Subclassing from an interface **creates a new interface** that
211+
adds additional methods to the parent interface. Implementing an interface
212+
creates a new class whose method signatures must be compatible with the
213+
interface being implemented.

interface/functional.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,16 @@ def sliding_window(iterable, n):
4646
for item in it:
4747
items.append(item)
4848
yield tuple(items)
49+
50+
51+
def merge(dicts):
52+
dicts = list(dicts)
53+
if len(dicts) == 0:
54+
return {}
55+
elif len(dicts) == 1:
56+
return dicts[0]
57+
else:
58+
out = dicts[0].copy()
59+
for other in dicts[1:]:
60+
out.update(other)
61+
return out

interface/interface.py

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from .compat import raise_from, with_metaclass
77
from .default import default, warn_if_defaults_use_non_interface_members
88
from .formatting import bulleted_list
9-
from .functional import complement, keyfilter, valfilter
9+
from .functional import complement, keyfilter, merge, valfilter
1010
from .typecheck import compatible
1111
from .typed_signature import TypedSignature
1212
from .utils import is_a, unique
@@ -21,8 +21,22 @@ class InvalidImplementation(TypeError):
2121
"""
2222

2323

24+
class InvalidSubInterface(TypeError):
25+
"""
26+
Raised when on attempt to define a subclass of an interface that's not
27+
compatible with the parent definition.
28+
"""
29+
30+
2431
CLASS_ATTRIBUTE_WHITELIST = frozenset(
25-
["__doc__", "__module__", "__name__", "__qualname__", "__weakref__"]
32+
[
33+
"__doc__",
34+
"__module__",
35+
"__name__",
36+
"__qualname__",
37+
"__weakref__",
38+
"_INTERFACE_IGNORE_MEMBERS",
39+
]
2640
)
2741

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

7488

89+
def _merge_parent_signatures(bases):
90+
return merge(filter(None, (getattr(b, "_signatures") for b in bases)))
91+
92+
93+
def _merge_parent_defaults(bases):
94+
return merge(filter(None, (getattr(b, "_defaults") for b in bases)))
95+
96+
7597
class InterfaceMeta(type):
7698
"""
7799
Metaclass for interfaces.
@@ -80,22 +102,43 @@ class InterfaceMeta(type):
80102
"""
81103

82104
def __new__(mcls, name, bases, clsdict):
83-
signatures = {}
84-
defaults = {}
85-
for k, v in keyfilter(is_interface_field_name, clsdict).items():
105+
signatures = _merge_parent_signatures(bases)
106+
defaults = _merge_parent_defaults(bases)
107+
ignored = clsdict.get("_INTERFACE_IGNORE_MEMBERS", set())
108+
109+
for field, v in keyfilter(is_interface_field_name, clsdict).items():
110+
if field in ignored:
111+
continue
112+
86113
try:
87-
signatures[k] = TypedSignature(v)
114+
signature = TypedSignature(v)
88115
except TypeError as e:
89116
errmsg = (
90117
"Couldn't parse signature for field "
91118
"{iface_name}.{fieldname} of type {attrtype}.".format(
92-
iface_name=name, fieldname=k, attrtype=getname(type(v)),
119+
iface_name=name, fieldname=field, attrtype=getname(type(v)),
93120
)
94121
)
95122
raise_from(TypeError(errmsg), e)
96123

124+
# If we already have a signature for this field from a parent, then
125+
# our new signature must be a subtype of the parent signature, so
126+
# that any valid call to the new signature must also be a valid
127+
# call to the parent signature.
128+
if field in signatures and not compatible(signature, signatures[field]):
129+
conflicted = signatures[field]
130+
raise InvalidSubInterface(
131+
"\nInterface field {new}.{field} conflicts with inherited field of "
132+
"the same name.\n"
133+
" - {field}{new_sig} != {field}{old_sig}".format(
134+
new=name, field=field, new_sig=signature, old_sig=conflicted,
135+
)
136+
)
137+
else:
138+
signatures[field] = signature
139+
97140
if isinstance(v, default):
98-
defaults[k] = v
141+
defaults[field] = v
99142

100143
warn_if_defaults_use_non_interface_members(
101144
name, defaults, set(signatures.keys())
@@ -139,7 +182,7 @@ def _diff_signatures(self, type_):
139182
if not issubclass(impl_sig.type, iface_sig.type):
140183
mistyped[name] = impl_sig.type
141184

142-
if not compatible(impl_sig.signature, iface_sig.signature):
185+
if not compatible(impl_sig, iface_sig):
143186
mismatched[name] = impl_sig
144187

145188
return missing, mistyped, mismatched
@@ -258,6 +301,9 @@ def _format_mismatched_methods(self, mismatched):
258301
)
259302

260303

304+
empty_set = frozenset([])
305+
306+
261307
class Interface(with_metaclass(InterfaceMeta)):
262308
"""
263309
Base class for interface definitions.
@@ -313,6 +359,10 @@ def delete(self, key):
313359
:func:`implements`
314360
"""
315361

362+
# Don't consider these members part of the interface definition for
363+
# children of `Interface`.
364+
_INTERFACE_IGNORE_MEMBERS = {"__new__", "from_class"}
365+
316366
def __new__(cls, *args, **kwargs):
317367
raise TypeError("Can't instantiate interface %s" % getname(cls))
318368

@@ -349,7 +399,10 @@ def from_class(cls, existing_class, subset=None, name=None):
349399
)
350400

351401

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

354407

355408
class ImplementsMeta(type):

interface/tests/_py3_typecheck_tests.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
from inspect import signature
2-
31
from ..typecheck import compatible
2+
from ..typed_signature import TypedSignature
43

54

65
def test_allow_new_params_with_defaults_with_kwonly():
7-
@signature
6+
@TypedSignature
87
def iface(a, b, c): # pragma: nocover
98
pass
109

11-
@signature
10+
@TypedSignature
1211
def impl(a, b, c, d=3, e=5, *, f=5): # pragma: nocover
1312
pass
1413

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

1817

1918
def test_allow_reorder_kwonlys():
20-
@signature
19+
@TypedSignature
2120
def foo(a, b, c, *, d, e, f): # pragma: nocover
2221
pass
2322

24-
@signature
23+
@TypedSignature
2524
def bar(a, b, c, *, f, d, e): # pragma: nocover
2625
pass
2726

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

3130

3231
def test_allow_default_changes():
33-
@signature
32+
@TypedSignature
3433
def foo(a, b, c=3, *, d=1, e, f): # pragma: nocover
3534
pass
3635

37-
@signature
36+
@TypedSignature
3837
def bar(a, b, c=5, *, f, e, d=12): # pragma: nocover
3938
pass
4039

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

4443

4544
def test_disallow_kwonly_to_positional():
46-
@signature
45+
@TypedSignature
4746
def foo(a, *, b): # pragma: nocover
4847
pass
4948

50-
@signature
49+
@TypedSignature
5150
def bar(a, b): # pragma: nocover
5251
pass
5352

0 commit comments

Comments
 (0)