Skip to content

Commit b370131

Browse files
author
Scott Sanderson
committed
ENH: Add subinterfaces.
1 parent 10b124f commit b370131

File tree

3 files changed

+99
-11
lines changed

3 files changed

+99
-11
lines changed

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: 58 additions & 11 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,9 +21,14 @@ class InvalidImplementation(TypeError):
2121
"""
2222

2323

24-
CLASS_ATTRIBUTE_WHITELIST = frozenset(
25-
["__doc__", "__module__", "__name__", "__qualname__", "__weakref__"]
26-
)
24+
CLASS_ATTRIBUTE_WHITELIST = frozenset([
25+
'__doc__',
26+
'__module__',
27+
'__name__',
28+
'__qualname__',
29+
'__weakref__',
30+
'_INTERFACE_IGNORE_MEMBERS',
31+
])
2732

2833
is_interface_field_name = complement(CLASS_ATTRIBUTE_WHITELIST.__contains__)
2934

@@ -72,6 +77,14 @@ def _conflicting_defaults(typename, conflicts):
7277
return InvalidImplementation(message)
7378

7479

80+
def _merge_parent_signatures(bases):
81+
return merge(filter(None, (getattr(b, '_signatures') for b in bases)))
82+
83+
84+
def _merge_parent_defaults(bases):
85+
return merge(filter(None, (getattr(b, '_defaults') for b in bases)))
86+
87+
7588
class InterfaceMeta(type):
7689
"""
7790
Metaclass for interfaces.
@@ -80,22 +93,47 @@ class InterfaceMeta(type):
8093
"""
8194

8295
def __new__(mcls, name, bases, clsdict):
83-
signatures = {}
84-
defaults = {}
85-
for k, v in keyfilter(is_interface_field_name, clsdict).items():
96+
signatures = _merge_parent_signatures(bases)
97+
defaults = _merge_parent_defaults(bases)
98+
ignored = clsdict.get('_INTERFACE_IGNORE_MEMBERS', set())
99+
100+
for field, v in keyfilter(is_interface_field_name, clsdict).items():
101+
if field in ignored:
102+
continue
103+
86104
try:
87-
signatures[k] = TypedSignature(v)
105+
signature = TypedSignature(v)
88106
except TypeError as e:
89107
errmsg = (
90108
"Couldn't parse signature for field "
91109
"{iface_name}.{fieldname} of type {attrtype}.".format(
92-
iface_name=name, fieldname=k, attrtype=getname(type(v)),
110+
iface_name=name,
111+
fieldname=field,
112+
attrtype=getname(type(v)),
93113
)
94114
)
95115
raise_from(TypeError(errmsg), e)
96116

117+
if field in signatures:
118+
for base in bases:
119+
if field in bases._signatures:
120+
raise TypeError(
121+
"Interface field {new}.{field} conflicts with "
122+
"parent field {old}.{field}.".format(
123+
new=name,
124+
old=base.name,
125+
field=field,
126+
)
127+
)
128+
else:
129+
# If we hit this there's a bug in
130+
# _extract_parent_signatures above.
131+
raise AssertionError("Inconsistent bases!")
132+
else:
133+
signatures[field] = signature
134+
97135
if isinstance(v, default):
98-
defaults[k] = v
136+
defaults[field] = v
99137

100138
warn_if_defaults_use_non_interface_members(
101139
name, defaults, set(signatures.keys())
@@ -258,6 +296,9 @@ def _format_mismatched_methods(self, mismatched):
258296
)
259297

260298

299+
empty_set = frozenset([])
300+
301+
261302
class Interface(with_metaclass(InterfaceMeta)):
262303
"""
263304
Base class for interface definitions.
@@ -312,6 +353,9 @@ def delete(self, key):
312353
--------
313354
:func:`implements`
314355
"""
356+
# Don't consider these members part of the interface definition for
357+
# children of `Interface`.
358+
_INTERFACE_IGNORE_MEMBERS = {'__new__', 'from_class'}
315359

316360
def __new__(cls, *args, **kwargs):
317361
raise TypeError("Can't instantiate interface %s" % getname(cls))
@@ -349,7 +393,10 @@ def from_class(cls, existing_class, subset=None, name=None):
349393
)
350394

351395

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

354401

355402
class ImplementsMeta(type):

interface/tests/test_interface.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,3 +890,31 @@ class BadImpl failed to implement interface HasMagicMethodsInterface:
890890
- __getitem__(self, key)"""
891891
)
892892
assert actual_message == expected_message
893+
894+
895+
def test_interface_subclass():
896+
897+
class A(Interface): # pragma: nocover
898+
def method_a(self):
899+
pass
900+
901+
class AandB(A): # pragma: nocover
902+
def method_b(self):
903+
pass
904+
905+
with pytest.raises(InvalidImplementation):
906+
class JustA(implements(AandB)):
907+
def method_a(self):
908+
pass
909+
910+
with pytest.raises(InvalidImplementation):
911+
class JustB(implements(AandB)):
912+
def method_b(self):
913+
pass
914+
915+
class Both(implements(AandB)):
916+
def method_a(self):
917+
pass
918+
919+
def method_b(self):
920+
pass

0 commit comments

Comments
 (0)