Skip to content

Commit 8cd72cf

Browse files
author
Scott Sanderson
committed
ENH: Better error when failing multiple interfaces.
1 parent 0fd3199 commit 8cd72cf

File tree

3 files changed

+247
-84
lines changed

3 files changed

+247
-84
lines changed

interface/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from .interface import implements, Interface
1+
from .interface import implements, IncompleteImplementation, Interface
22

33
__all__ = [
4+
'IncompleteImplementation',
45
'Interface',
56
'implements',
67
]

interface/interface.py

Lines changed: 114 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,20 @@
44
"""
55
from functools import wraps
66
import inspect
7-
from operator import itemgetter
7+
from operator import attrgetter, itemgetter
88
from textwrap import dedent
99
from weakref import WeakKeyDictionary
1010

11+
from .utils import is_a, unique
12+
1113
first = itemgetter(0)
14+
getname = attrgetter('__name__')
15+
16+
17+
class IncompleteImplementation(TypeError):
18+
"""
19+
Raised when a class intending to implement an interface fails to do so.
20+
"""
1221

1322

1423
def compatible(meth_sig, iface_sig):
@@ -66,9 +75,9 @@ def _diff_signatures(self, type_):
6675
mismatched[name] = f_sig
6776
return missing, mismatched
6877

69-
def check_conforms(self, type_):
78+
def verify(self, type_):
7079
"""
71-
Check whether a type implements our interface.
80+
Check whether a type implements ``self``.
7281
7382
Parameters
7483
----------
@@ -96,26 +105,30 @@ def _invalid_implementation(self, t, missing, mismatched):
96105
assert missing or mismatched, "Implementation wasn't invalid."
97106

98107
message = "\nclass {C} failed to implement interface {I}:".format(
99-
C=t.__name__,
100-
I=self.__name__,
108+
C=getname(t),
109+
I=getname(self),
101110
)
102111
if missing:
103112
message += dedent(
104113
"""
105114
106-
The following methods were not implemented:
115+
The following methods of {I} were not implemented:
107116
{missing_methods}"""
108-
).format(missing_methods=self._format_missing_methods(missing))
117+
).format(
118+
I=getname(self),
119+
missing_methods=self._format_missing_methods(missing)
120+
)
109121

110122
if mismatched:
111123
message += (
112-
"\n\nThe following methods were implemented but had invalid"
124+
"\n\nThe following methods of {I} were implemented with invalid"
113125
" signatures:\n"
114126
"{mismatched_methods}"
115127
).format(
128+
I=getname(self),
116129
mismatched_methods=self._format_mismatched_methods(mismatched),
117130
)
118-
return TypeError(message)
131+
return IncompleteImplementation(message)
119132

120133
def _format_missing_methods(self, missing):
121134
return "\n".join(sorted([
@@ -140,92 +153,123 @@ class Interface(metaclass=InterfaceMeta):
140153
"""
141154

142155

156+
empty_set = frozenset([])
157+
158+
143159
class ImplementsMeta(type):
144160
"""
145161
Metaclass for implementations of particular interfaces.
146162
"""
147-
def __new__(mcls, name, bases, clsdict, base=False):
163+
def __new__(mcls, name, bases, clsdict, interfaces=empty_set):
164+
assert isinstance(interfaces, frozenset)
165+
148166
newtype = super().__new__(mcls, name, bases, clsdict)
149167

150-
if base:
168+
if interfaces:
151169
# Don't do checks on the types returned by ``implements``.
152170
return newtype
153171

172+
errors = []
154173
for iface in newtype.interfaces():
155-
iface.check_conforms(newtype)
174+
try:
175+
iface.verify(newtype)
176+
except IncompleteImplementation as e:
177+
errors.append(e)
156178

157-
return newtype
179+
if not errors:
180+
return newtype
181+
elif len(errors) == 1:
182+
raise errors[0]
183+
else:
184+
raise IncompleteImplementation("\n\n".join(map(str, errors)))
158185

159-
def __init__(mcls, name, bases, clsdict, base=False):
186+
def __init__(mcls, name, bases, clsdict, interfaces=empty_set):
187+
mcls._interfaces = interfaces
160188
super().__init__(name, bases, clsdict)
161189

162190
def interfaces(self):
163-
"""
164-
Return a generator of interfaces implemented by this type.
191+
yield from unique(self._interfaces_with_duplicates())
165192

166-
Yields
167-
------
168-
iface : Interface
169-
"""
170-
for base in self.mro():
171-
if isinstance(base, ImplementsMeta):
172-
yield base.interface
193+
def _interfaces_with_duplicates(self):
194+
yield from self._interfaces
195+
for t in filter(is_a(ImplementsMeta), self.mro()):
196+
yield from t._interfaces
197+
198+
199+
def format_iface_method_docs(I):
200+
iface_name = getname(I)
201+
return "\n".join([
202+
"{iface_name}.{method_name}{sig}".format(
203+
iface_name=iface_name,
204+
method_name=method_name,
205+
sig=sig,
206+
)
207+
for method_name, sig in sorted(list(I._signatures.items()), key=first)
208+
])
173209

174210

175-
def weakmemoize_implements(f):
176-
"One-off weakmemoize implementation for ``implements``."
211+
def _make_implements():
177212
_memo = WeakKeyDictionary()
178213

179-
@wraps(f)
180-
def _f(I):
214+
def implements(*interfaces):
215+
"""
216+
Make a base for classes that implement ``*interfaces``.
217+
218+
Parameters
219+
----------
220+
I : Interface
221+
222+
Returns
223+
-------
224+
base : type
225+
A type validating that subclasses must implement all interface
226+
methods of I.
227+
"""
228+
if not interfaces:
229+
raise TypeError("implements() requires at least one interface")
230+
231+
interfaces = frozenset(interfaces)
181232
try:
182-
return _memo[I]
233+
return _memo[interfaces]
183234
except KeyError:
184235
pass
185-
ret = f(I)
186-
_memo[I] = ret
187-
return ret
188-
return _f
189236

237+
for I in interfaces:
238+
if not issubclass(I, Interface):
239+
raise TypeError(
240+
"implements() expected an Interface, but got %s." % I
241+
)
242+
243+
ordered_ifaces = tuple(sorted(interfaces, key=getname))
244+
iface_names = list(map(getname, ordered_ifaces))
245+
246+
name = "Implements{I}".format(I="_".join(iface_names))
247+
doc = dedent(
248+
"""\
249+
Implementation of {interfaces}.
250+
251+
Methods
252+
-------
253+
{methods}"""
254+
).format(
255+
interfaces=', '.join(iface_names),
256+
methods="\n".join(map(format_iface_method_docs, ordered_ifaces)),
257+
)
190258

191-
@weakmemoize_implements
192-
def implements(I):
193-
"""
194-
Make a base for classes that implement ``I``.
195-
196-
Parameters
197-
----------
198-
I : Interface
199-
200-
Returns
201-
-------
202-
base : type
203-
A type validating that subclasses must implement all interface
204-
methods of I.
205-
"""
206-
if not issubclass(I, Interface):
207-
raise TypeError(
208-
"implements() expected an Interface, but got %s." % I
259+
result = ImplementsMeta(
260+
name,
261+
(object,),
262+
{'__doc__': doc},
263+
interfaces=interfaces,
209264
)
210265

211-
name = "Implements{I}".format(I=I.__name__)
212-
doc = dedent(
213-
"""\
214-
Implementation of {I}.
266+
# NOTE: It's important for correct weak-memoization that this is set is
267+
# stored somewhere on the resulting type.
268+
assert result._interfaces is interfaces, "Interfaces not stored."
215269

216-
Methods
217-
-------
218-
{methods}"""
219-
).format(
220-
I=I.__name__,
221-
methods="\n".join(
222-
"{name}{sig}".format(name=name, sig=sig)
223-
for name, sig in sorted(list(I._signatures.items()), key=first)
224-
)
225-
)
226-
return ImplementsMeta(
227-
name,
228-
(object,),
229-
{'__doc__': doc, 'interface': I},
230-
base=True,
231-
)
270+
_memo[interfaces] = result
271+
return result
272+
return implements
273+
274+
implements = _make_implements()
275+
del _make_implements

0 commit comments

Comments
 (0)