Skip to content

Commit 44b461c

Browse files
author
Scott Sanderson
committed
ENH: Add support for default implementations.
1 parent ef35070 commit 44b461c

File tree

5 files changed

+226
-27
lines changed

5 files changed

+226
-27
lines changed

interface/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from .interface import implements, IncompleteImplementation, Interface
1+
from .interface import default, implements, InvalidImplementation, Interface
22

33
__all__ = [
4-
'IncompleteImplementation',
4+
'default',
5+
'InvalidImplementation',
56
'Interface',
67
'implements',
78
]

interface/default.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class default(object):
2+
"""Default implementation of a function in terms of interface methods.
3+
"""
4+
def __init__(self, implementation):
5+
self.implementation = implementation
6+
7+
def __repr__(self):
8+
return "{}({})".format(type(self).__name__, self.implementation)

interface/interface.py

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
interface
33
---------
44
"""
5+
from collections import defaultdict
56
from functools import wraps
67
import inspect
78
from operator import attrgetter, itemgetter
89
from textwrap import dedent
910
from weakref import WeakKeyDictionary
1011

11-
from .compat import raise_from, with_metaclass
12-
from .functional import complement, keyfilter
12+
from .compat import raise_from, viewkeys, with_metaclass
13+
from .default import default # noqa reexport
14+
from .functional import complement, keyfilter, valfilter
1315
from .typecheck import compatible
1416
from .typed_signature import TypedSignature
1517
from .utils import is_a, unique
@@ -18,7 +20,7 @@
1820
getname = attrgetter('__name__')
1921

2022

21-
class IncompleteImplementation(TypeError):
23+
class InvalidImplementation(TypeError):
2224
"""
2325
Raised when a class intending to implement an interface fails to do so.
2426
"""
@@ -47,6 +49,39 @@ def static_get_type_attr(t, name):
4749
raise AttributeError(name)
4850

4951

52+
def _conflicting_defaults(typename, conflicts):
53+
"""Format an error message for conflicting default implementations.
54+
55+
Parameters
56+
----------
57+
typename : str
58+
Name of the type for which we're producing an error.
59+
conflicts : dict[str -> list[Interface]]
60+
Map from strings to interfaces providing a default with that name.
61+
62+
Returns
63+
-------
64+
message : str
65+
User-facing error message.
66+
"""
67+
message = "\nclass {C} received conflicting default implementations:".format(
68+
C=typename,
69+
)
70+
for attrname, interfaces in conflicts.items():
71+
message += dedent(
72+
"""
73+
74+
The following interfaces provided default implementations for {attr!r}:
75+
{interfaces}"""
76+
).format(
77+
attr=attrname,
78+
interfaces="\n".join(sorted([
79+
" - {name}".format(name=iface.__name__) for iface in interfaces
80+
]))
81+
)
82+
return InvalidImplementation(message)
83+
84+
5085
class InterfaceMeta(type):
5186
"""
5287
Metaclass for interfaces.
@@ -55,6 +90,7 @@ class InterfaceMeta(type):
5590
"""
5691
def __new__(mcls, name, bases, clsdict):
5792
signatures = {}
93+
defaults = {}
5894
for k, v in keyfilter(is_interface_field_name, clsdict).items():
5995
try:
6096
signatures[k] = TypedSignature(v)
@@ -69,7 +105,11 @@ def __new__(mcls, name, bases, clsdict):
69105
)
70106
raise_from(TypeError(errmsg), e)
71107

108+
if isinstance(v, default):
109+
defaults[k] = v
110+
72111
clsdict['_signatures'] = signatures
112+
clsdict['_defaults'] = defaults
73113
return super(InterfaceMeta, mcls).__new__(mcls, name, bases, clsdict)
74114

75115
def _diff_signatures(self, type_):
@@ -129,9 +169,20 @@ def verify(self, type_):
129169
-------
130170
None
131171
"""
132-
missing, mistyped, mismatched = self._diff_signatures(type_)
172+
raw_missing, mistyped, mismatched = self._diff_signatures(type_)
173+
174+
# See if we have defaults for missing methods.
175+
missing = []
176+
defaults_to_use = {}
177+
for name in raw_missing:
178+
try:
179+
defaults_to_use[name] = self._defaults[name].implementation
180+
except KeyError:
181+
missing.append(name)
182+
133183
if not any((missing, mistyped, mismatched)):
134-
return
184+
return defaults_to_use
185+
135186
raise self._invalid_implementation(type_, missing, mistyped, mismatched)
136187

137188
def _invalid_implementation(self, t, missing, mistyped, mismatched):
@@ -176,7 +227,7 @@ def _invalid_implementation(self, t, missing, mistyped, mismatched):
176227
I=getname(self),
177228
mismatched_methods=self._format_mismatched_methods(mismatched),
178229
)
179-
return IncompleteImplementation(message)
230+
return InvalidImplementation(message)
180231

181232
def _format_missing_methods(self, missing):
182233
return "\n".join(sorted([
@@ -231,18 +282,31 @@ def __new__(mcls, name, bases, clsdict, interfaces=empty_set):
231282
return newtype
232283

233284
errors = []
285+
default_impls = {}
286+
default_providers = defaultdict(list)
234287
for iface in newtype.interfaces():
235288
try:
236-
iface.verify(newtype)
237-
except IncompleteImplementation as e:
289+
defaults_from_iface = iface.verify(newtype)
290+
for name, impl in defaults_from_iface.items():
291+
default_impls[name] = impl
292+
default_providers[name].append(iface)
293+
except InvalidImplementation as e:
238294
errors.append(e)
239295

296+
# The list of providers for `name`, if there's more than one.
297+
duplicate_defaults = valfilter(lambda ifaces: len(ifaces) > 1, default_providers)
298+
if duplicate_defaults:
299+
errors.append(_conflicting_defaults(newtype.__name__, duplicate_defaults))
300+
else:
301+
for name, impl in default_impls.items():
302+
setattr(newtype, name, impl)
303+
240304
if not errors:
241305
return newtype
242306
elif len(errors) == 1:
243307
raise errors[0]
244308
else:
245-
raise IncompleteImplementation("\n\n".join(map(str, errors)))
309+
raise InvalidImplementation("\n\n".join(map(str, errors)))
246310

247311
def __init__(mcls, name, bases, clsdict, interfaces=empty_set):
248312
mcls._interfaces = interfaces
@@ -336,5 +400,6 @@ def implements(*interfaces):
336400
return result
337401
return implements
338402

403+
339404
implements = _make_implements()
340405
del _make_implements

0 commit comments

Comments
 (0)