Skip to content

Commit d7d6701

Browse files
author
Scott Sanderson
authored
Merge pull request #4 from ssanderson/provide-default-impls
ENH: Add support for default implementations.
2 parents ef35070 + dad8d36 commit d7d6701

13 files changed

+524
-40
lines changed

interface/__init__.py

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

34
__all__ = [
4-
'IncompleteImplementation',
5+
'default',
6+
'InvalidImplementation',
57
'Interface',
68
'implements',
79
]

interface/default.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import dis
2+
import warnings
3+
4+
from .compat import PY3
5+
from .formatting import bulleted_list
6+
from .functional import keysorted, sliding_window
7+
8+
9+
class default(object):
10+
"""Default implementation of a function in terms of interface methods.
11+
"""
12+
def __init__(self, implementation):
13+
self.implementation = implementation
14+
15+
def __repr__(self):
16+
return "{}({})".format(type(self).__name__, self.implementation)
17+
18+
19+
class DefaultUsesNonInterfaceMembers(UserWarning):
20+
pass
21+
22+
23+
if PY3: # pragma: nocover-py2
24+
_DEFAULT_USES_NON_INTERFACE_MEMBER_TEMPLATE = (
25+
"Default implementation of {iface}.{method} uses non-interface attributes.\n\n"
26+
"The following attributes may be accessed but are not part of "
27+
"the interface:\n"
28+
"{non_members}\n\n"
29+
"Consider changing the implementation of {method} or making these attributes"
30+
" part of {iface}."
31+
)
32+
33+
def warn_if_defaults_use_non_interface_members(interface_name,
34+
defaults,
35+
members):
36+
"""Warn if an interface default uses non-interface members of self.
37+
"""
38+
for method_name, attrs in non_member_attributes(defaults, members):
39+
warnings.warn(_DEFAULT_USES_NON_INTERFACE_MEMBER_TEMPLATE.format(
40+
iface=interface_name,
41+
method=method_name,
42+
non_members=bulleted_list(attrs),
43+
))
44+
45+
def non_member_attributes(defaults, members):
46+
from .typed_signature import TypedSignature
47+
48+
for default_name, default in keysorted(defaults):
49+
impl = default.implementation
50+
51+
if isinstance(impl, staticmethod):
52+
# staticmethods can't use attributes of the interface.
53+
continue
54+
55+
self_name = TypedSignature(impl).first_argument_name
56+
if self_name is None:
57+
# No parameters.
58+
# TODO: This is probably a bug in the interface, since a method
59+
# with no parameters that's not a staticmethod probably can't
60+
# be called in any natural way.
61+
continue
62+
63+
used = accessed_attributes_of_local(impl, self_name)
64+
non_interface_usages = used - members
65+
66+
if non_interface_usages:
67+
yield default_name, sorted(non_interface_usages)
68+
69+
def accessed_attributes_of_local(f, local_name):
70+
"""
71+
Get a list of attributes of ``local_name`` accessed by ``f``.
72+
73+
The analysis performed by this function is conservative, meaning that
74+
it's not guaranteed to find **all** attributes used.
75+
"""
76+
used = set()
77+
# Find sequences of the form: LOAD_FAST(local_name), LOAD_ATTR(<name>).
78+
# This will find all usages of the form ``local_name.<name>``.
79+
#
80+
# It will **NOT** find usages in which ``local_name`` is aliased to
81+
# another name.
82+
for first, second in sliding_window(dis.get_instructions(f), 2):
83+
if first.opname == 'LOAD_FAST' and first.argval == local_name:
84+
if second.opname in ('LOAD_ATTR', 'STORE_ATTR'):
85+
used.add(second.argval)
86+
return used
87+
88+
else: # pragma: nocover-py3
89+
def warn_if_defaults_use_non_interface_members(*args, **kwargs):
90+
pass
91+
92+
def non_member_warnings(*args, **kwargs):
93+
return iter(())
94+
95+
def accessed_attributes_of_local(*args, **kwargs):
96+
return set()

interface/formatting.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""String formatting utilities.
2+
"""
3+
4+
5+
def bulleted_list(items):
6+
return "\n".join(map(" - {}".format, items))

interface/functional.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
----------
44
Functional programming utilities.
55
"""
6+
from collections import deque
7+
from operator import itemgetter
68
from .compat import viewkeys
79

810

@@ -16,6 +18,10 @@ def keyfilter(f, d):
1618
return {k: v for k, v in d.items() if f(k)}
1719

1820

21+
def keysorted(d):
22+
return sorted(d.items(), key=itemgetter(0))
23+
24+
1925
def valfilter(f, d):
2026
return {k: v for k, v in d.items() if f(v)}
2127

@@ -25,3 +31,19 @@ def dzip(left, right):
2531
k: (left.get(k), right.get(k))
2632
for k in viewkeys(left) & viewkeys(right)
2733
}
34+
35+
36+
def sliding_window(iterable, n):
37+
it = iter(iterable)
38+
items = deque(maxlen=n)
39+
try:
40+
for i in range(n):
41+
items.append(next(it))
42+
except StopIteration:
43+
return
44+
45+
yield tuple(items)
46+
47+
for item in it:
48+
items.append(item)
49+
yield tuple(items)

interface/interface.py

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
interface
33
---------
44
"""
5-
from functools import wraps
6-
import inspect
5+
from collections import defaultdict
76
from operator import attrgetter, itemgetter
87
from textwrap import dedent
98
from weakref import WeakKeyDictionary
109

1110
from .compat import raise_from, with_metaclass
12-
from .functional import complement, keyfilter
11+
from .default import default, warn_if_defaults_use_non_interface_members
12+
from .formatting import bulleted_list
13+
from .functional import complement, keyfilter, valfilter
1314
from .typecheck import compatible
1415
from .typed_signature import TypedSignature
1516
from .utils import is_a, unique
@@ -18,7 +19,7 @@
1819
getname = attrgetter('__name__')
1920

2021

21-
class IncompleteImplementation(TypeError):
22+
class InvalidImplementation(TypeError):
2223
"""
2324
Raised when a class intending to implement an interface fails to do so.
2425
"""
@@ -47,6 +48,37 @@ def static_get_type_attr(t, name):
4748
raise AttributeError(name)
4849

4950

51+
def _conflicting_defaults(typename, conflicts):
52+
"""Format an error message for conflicting default implementations.
53+
54+
Parameters
55+
----------
56+
typename : str
57+
Name of the type for which we're producing an error.
58+
conflicts : dict[str -> list[Interface]]
59+
Map from strings to interfaces providing a default with that name.
60+
61+
Returns
62+
-------
63+
message : str
64+
User-facing error message.
65+
"""
66+
message = "\nclass {C} received conflicting default implementations:".format(
67+
C=typename,
68+
)
69+
for attrname, interfaces in conflicts.items():
70+
message += dedent(
71+
"""
72+
73+
The following interfaces provided default implementations for {attr!r}:
74+
{interfaces}"""
75+
).format(
76+
attr=attrname,
77+
interfaces=bulleted_list(sorted(map(getname, interfaces))),
78+
)
79+
return InvalidImplementation(message)
80+
81+
5082
class InterfaceMeta(type):
5183
"""
5284
Metaclass for interfaces.
@@ -55,6 +87,7 @@ class InterfaceMeta(type):
5587
"""
5688
def __new__(mcls, name, bases, clsdict):
5789
signatures = {}
90+
defaults = {}
5891
for k, v in keyfilter(is_interface_field_name, clsdict).items():
5992
try:
6093
signatures[k] = TypedSignature(v)
@@ -69,7 +102,17 @@ def __new__(mcls, name, bases, clsdict):
69102
)
70103
raise_from(TypeError(errmsg), e)
71104

105+
if isinstance(v, default):
106+
defaults[k] = v
107+
108+
warn_if_defaults_use_non_interface_members(
109+
name,
110+
defaults,
111+
set(signatures.keys())
112+
)
113+
72114
clsdict['_signatures'] = signatures
115+
clsdict['_defaults'] = defaults
73116
return super(InterfaceMeta, mcls).__new__(mcls, name, bases, clsdict)
74117

75118
def _diff_signatures(self, type_):
@@ -129,9 +172,20 @@ def verify(self, type_):
129172
-------
130173
None
131174
"""
132-
missing, mistyped, mismatched = self._diff_signatures(type_)
175+
raw_missing, mistyped, mismatched = self._diff_signatures(type_)
176+
177+
# See if we have defaults for missing methods.
178+
missing = []
179+
defaults_to_use = {}
180+
for name in raw_missing:
181+
try:
182+
defaults_to_use[name] = self._defaults[name].implementation
183+
except KeyError:
184+
missing.append(name)
185+
133186
if not any((missing, mistyped, mismatched)):
134-
return
187+
return defaults_to_use
188+
135189
raise self._invalid_implementation(type_, missing, mistyped, mismatched)
136190

137191
def _invalid_implementation(self, t, missing, mistyped, mismatched):
@@ -176,7 +230,7 @@ def _invalid_implementation(self, t, missing, mistyped, mismatched):
176230
I=getname(self),
177231
mismatched_methods=self._format_mismatched_methods(mismatched),
178232
)
179-
return IncompleteImplementation(message)
233+
return InvalidImplementation(message)
180234

181235
def _format_missing_methods(self, missing):
182236
return "\n".join(sorted([
@@ -231,18 +285,31 @@ def __new__(mcls, name, bases, clsdict, interfaces=empty_set):
231285
return newtype
232286

233287
errors = []
234-
for iface in newtype.interfaces():
288+
default_impls = {}
289+
default_providers = defaultdict(list)
290+
for iface in sorted(newtype.interfaces(), key=getname):
235291
try:
236-
iface.verify(newtype)
237-
except IncompleteImplementation as e:
292+
defaults_from_iface = iface.verify(newtype)
293+
for name, impl in defaults_from_iface.items():
294+
default_impls[name] = impl
295+
default_providers[name].append(iface)
296+
except InvalidImplementation as e:
238297
errors.append(e)
239298

299+
# The list of providers for `name`, if there's more than one.
300+
duplicate_defaults = valfilter(lambda ifaces: len(ifaces) > 1, default_providers)
301+
if duplicate_defaults:
302+
errors.append(_conflicting_defaults(newtype.__name__, duplicate_defaults))
303+
else:
304+
for name, impl in default_impls.items():
305+
setattr(newtype, name, impl)
306+
240307
if not errors:
241308
return newtype
242309
elif len(errors) == 1:
243310
raise errors[0]
244311
else:
245-
raise IncompleteImplementation("\n\n".join(map(str, errors)))
312+
raise InvalidImplementation("\n".join(map(str, errors)))
246313

247314
def __init__(mcls, name, bases, clsdict, interfaces=empty_set):
248315
mcls._interfaces = interfaces
@@ -308,7 +375,7 @@ def implements(*interfaces):
308375
ordered_ifaces = tuple(sorted(interfaces, key=getname))
309376
iface_names = list(map(getname, ordered_ifaces))
310377

311-
name = "Implements{I}".format(I="_".join(iface_names))
378+
name = "Implements{}".format("_".join(iface_names))
312379
doc = dedent(
313380
"""\
314381
Implementation of {interfaces}.
@@ -336,5 +403,6 @@ def implements(*interfaces):
336403
return result
337404
return implements
338405

406+
339407
implements = _make_implements()
340408
del _make_implements

interface/tests/test_functional.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from functools import total_ordering
2+
3+
import pytest
4+
5+
from ..functional import keysorted, sliding_window
6+
7+
8+
def test_sliding_window():
9+
assert list(sliding_window([], 2)) == []
10+
assert list(sliding_window([1, 2, 3], 2)) == [(1, 2), (2, 3)]
11+
assert list(sliding_window([1, 2, 3, 4], 2)) == [(1, 2), (2, 3), (3, 4)]
12+
assert list(sliding_window([1, 2, 3, 4], 3)) == [(1, 2, 3), (2, 3, 4)]
13+
14+
15+
def test_keysorted():
16+
17+
@total_ordering
18+
class Unorderable(object):
19+
20+
def __init__(self, obj):
21+
self.obj = obj
22+
23+
def __eq__(self, other):
24+
raise AssertionError("Can't compare this.")
25+
26+
__ne__ = __lt__ = __eq__
27+
28+
with pytest.raises(AssertionError):
29+
sorted([Unorderable(0), Unorderable(0)])
30+
31+
d = {'c': Unorderable(3), 'b': Unorderable(2), 'a': Unorderable(1)}
32+
items = keysorted(d)
33+
34+
assert items[0][0] == 'a'
35+
assert items[0][1].obj == 1
36+
37+
assert items[1][0] == 'b'
38+
assert items[1][1].obj == 2
39+
40+
assert items[2][0] == 'c'
41+
assert items[2][1].obj == 3

0 commit comments

Comments
 (0)