Skip to content

Commit 3cad51b

Browse files
author
Scott Sanderson
committed
MAINT: Support Python 2.
1 parent 33a3251 commit 3cad51b

File tree

11 files changed

+177
-87
lines changed

11 files changed

+177
-87
lines changed

README.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
|build status|
55

66
``interface`` provides facilities for declaring interfaces and for statically
7-
asserting that classes implement those interfaces.
7+
asserting that classes implement those interfaces. It supports Python 2.7 and
8+
Python 3.4+.
89

910
``interface`` improves on Python's ``abc`` module in two ways:
1011

interface/compat.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import sys
2+
from itertools import repeat
3+
4+
version_info = sys.version_info
5+
6+
PY2 = version_info.major == 2
7+
PY3 = version_info.major == 3
8+
9+
if PY2: # pragma: nocover
10+
from funcsigs import signature, Parameter
11+
12+
def raise_from(e, from_):
13+
raise e
14+
15+
def viewkeys(d):
16+
return d.viewkeys()
17+
18+
else: # pragma: nocover
19+
from inspect import signature, Parameter
20+
exec("def raise_from(e, from_):" # pragma: nocover
21+
" raise e from from_")
22+
23+
def viewkeys(d):
24+
return d.keys()
25+
26+
27+
def zip_longest(left, right):
28+
"""Simple zip_longest that only supports two iterators and None default.
29+
"""
30+
left = iter(left)
31+
right = iter(right)
32+
left_done = False
33+
right_done = False
34+
while True:
35+
try:
36+
left_yielded = next(left)
37+
except StopIteration:
38+
left_done = True
39+
left_yielded = None
40+
left = repeat(None)
41+
try:
42+
right_yielded = next(right)
43+
except StopIteration:
44+
right_done = True
45+
right_yielded = None
46+
right = repeat(None)
47+
48+
if left_done and right_done:
49+
raise StopIteration()
50+
51+
yield left_yielded, right_yielded
52+
53+
54+
# Taken from six version 1.10.0.
55+
def with_metaclass(meta, *bases):
56+
"""Create a base class with a metaclass."""
57+
# This requires a bit of explanation: the basic idea is to make a dummy
58+
# metaclass for one level of class instantiation that replaces itself with
59+
# the actual metaclass.
60+
class metaclass(meta):
61+
62+
def __new__(cls, name, this_bases, d):
63+
return meta(name, bases, d)
64+
return type.__new__(metaclass, 'temporary_class', (), {})

interface/functional.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
----------
44
Functional programming utilities.
55
"""
6+
from .compat import viewkeys
67

78

89
def complement(f):
@@ -20,4 +21,7 @@ def valfilter(f, d):
2021

2122

2223
def dzip(left, right):
23-
return {k: (left.get(k), right.get(k)) for k in left.keys() & right.keys()}
24+
return {
25+
k: (left.get(k), right.get(k))
26+
for k in viewkeys(left) & viewkeys(right)
27+
}

interface/interface.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from textwrap import dedent
99
from weakref import WeakKeyDictionary
1010

11+
from .compat import raise_from, with_metaclass
1112
from .functional import complement, keyfilter
1213
from .typecheck import compatible
1314
from .typed_signature import TypedSignature
@@ -66,10 +67,10 @@ def __new__(mcls, name, bases, clsdict):
6667
attrtype=getname(type(v)),
6768
)
6869
)
69-
raise TypeError(errmsg) from e
70+
raise_from(TypeError(errmsg), e)
7071

7172
clsdict['_signatures'] = signatures
72-
return super().__new__(mcls, name, bases, clsdict)
73+
return super(InterfaceMeta, mcls).__new__(mcls, name, bases, clsdict)
7374

7475
def _diff_signatures(self, type_):
7576
"""
@@ -205,7 +206,7 @@ def _format_mismatched_methods(self, mismatched):
205206
]))
206207

207208

208-
class Interface(metaclass=InterfaceMeta):
209+
class Interface(with_metaclass(InterfaceMeta)):
209210
"""
210211
Base class for interface definitions.
211212
"""
@@ -223,7 +224,7 @@ class ImplementsMeta(type):
223224
def __new__(mcls, name, bases, clsdict, interfaces=empty_set):
224225
assert isinstance(interfaces, frozenset)
225226

226-
newtype = super().__new__(mcls, name, bases, clsdict)
227+
newtype = super(ImplementsMeta, mcls).__new__(mcls, name, bases, clsdict)
227228

228229
if interfaces:
229230
# Don't do checks on the types returned by ``implements``.
@@ -245,15 +246,19 @@ def __new__(mcls, name, bases, clsdict, interfaces=empty_set):
245246

246247
def __init__(mcls, name, bases, clsdict, interfaces=empty_set):
247248
mcls._interfaces = interfaces
248-
super().__init__(name, bases, clsdict)
249+
super(ImplementsMeta, mcls).__init__(name, bases, clsdict)
249250

250251
def interfaces(self):
251-
yield from unique(self._interfaces_with_duplicates())
252+
for elem in unique(self._interfaces_with_duplicates()):
253+
yield elem
252254

253255
def _interfaces_with_duplicates(self):
254-
yield from self._interfaces
256+
for elem in self._interfaces:
257+
yield elem
258+
255259
for t in filter(is_a(ImplementsMeta), self.mro()):
256-
yield from t._interfaces
260+
for elem in t._interfaces:
261+
yield elem
257262

258263

259264
def format_iface_method_docs(I):
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from inspect import signature
2+
3+
from ..typecheck import compatible
4+
5+
6+
def test_allow_new_params_with_defaults_with_kwonly():
7+
8+
@signature
9+
def iface(a, b, c): # pragma: nocover
10+
pass
11+
12+
@signature
13+
def impl(a, b, c, d=3, e=5, *, f=5): # pragma: nocover
14+
pass
15+
16+
assert compatible(impl, iface)
17+
assert not compatible(iface, impl)
18+
19+
20+
def test_allow_reorder_kwonlys():
21+
22+
@signature
23+
def foo(a, b, c, *, d, e, f): # pragma: nocover
24+
pass
25+
26+
@signature
27+
def bar(a, b, c, *, f, d, e): # pragma: nocover
28+
pass
29+
30+
assert compatible(foo, bar)
31+
assert compatible(bar, foo)
32+
33+
34+
def test_allow_default_changes():
35+
36+
@signature
37+
def foo(a, b, c=3, *, d=1, e, f): # pragma: nocover
38+
pass
39+
40+
@signature
41+
def bar(a, b, c=5, *, f, e, d=12): # pragma: nocover
42+
pass
43+
44+
assert compatible(foo, bar)
45+
assert compatible(bar, foo)
46+
47+
48+
def test_disallow_kwonly_to_positional():
49+
50+
@signature
51+
def foo(a, *, b): # pragma: nocover
52+
pass
53+
54+
@signature
55+
def bar(a, b): # pragma: nocover
56+
pass
57+
58+
assert not compatible(foo, bar)
59+
assert not compatible(bar, foo)

interface/tests/test_interface.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ def _fixture(request):
7979
def combine_interfaces():
8080

8181
def combine_with_multiple_types(*interfaces):
82-
return list(map(implements, interfaces))
82+
return tuple(map(implements, interfaces))
8383

8484
def combine_with_single_type(*interfaces):
85-
return [implements(*interfaces)]
85+
return (implements(*interfaces),)
8686

8787
yield combine_with_multiple_types
8888
yield combine_with_single_type
@@ -107,30 +107,23 @@ def shared(self, a, b, c):
107107
bases = combine_interfaces(I1, I2)
108108

109109
with pytest.raises(IncompleteImplementation):
110-
class C(*bases): # pragma: nocover
111-
def i1_method(self, arg1):
112-
pass
113-
114-
def i2_method(self, arg2):
115-
pass
110+
type('C', bases, {
111+
'i1_method': lambda self, arg1: None,
112+
'i2_method': lambda self, arg2: None,
113+
})
116114

117115
with pytest.raises(IncompleteImplementation):
118-
class C(*bases): # pragma: nocover
119-
120-
def i1_method(self, arg1):
121-
pass
122-
123-
def shared(self, a, b, c):
124-
pass
116+
type('C', bases, {
117+
'i1_method': lambda self, arg1: None,
118+
'i2_method': lambda self, a, b, c: None,
119+
})
125120

126121
with pytest.raises(IncompleteImplementation):
127-
class C(*bases): # pragma: nocover
128122

129-
def i2_method(self, arg2):
130-
pass
131-
132-
def shared(self, a, b, c):
133-
pass
123+
type('C', bases, {
124+
'i2_method': lambda self, arg2: None,
125+
'shared': lambda self, a, b, c: None,
126+
})
134127

135128

136129
def test_missing_methods():

interface/tests/test_typecheck.py

Lines changed: 10 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from inspect import signature
2-
1+
from ..compat import PY3, signature
32
from ..typecheck import compatible
43

54

@@ -18,48 +17,6 @@ def bar(): # pragma: nocover
1817
assert compatible(bar, bar)
1918

2019

21-
def test_allow_new_params_with_defaults():
22-
23-
@signature
24-
def iface(a, b, c): # pragma: nocover
25-
pass
26-
27-
@signature
28-
def impl(a, b, c, d=3, e=5, *, f=5): # pragma: nocover
29-
pass
30-
31-
assert compatible(impl, iface)
32-
assert not compatible(iface, impl)
33-
34-
35-
def test_allow_reorder_kwonlys():
36-
37-
@signature
38-
def foo(a, b, c, *, d, e, f): # pragma: nocover
39-
pass
40-
41-
@signature
42-
def bar(a, b, c, *, f, d, e): # pragma: nocover
43-
pass
44-
45-
assert compatible(foo, bar)
46-
assert compatible(bar, foo)
47-
48-
49-
def test_allow_default_changes():
50-
51-
@signature
52-
def foo(a, b, c=3, *, d=1, e, f): # pragma: nocover
53-
pass
54-
55-
@signature
56-
def bar(a, b, c=5, *, f, e, d=12): # pragma: nocover
57-
pass
58-
59-
assert compatible(foo, bar)
60-
assert compatible(bar, foo)
61-
62-
6320
def test_disallow_new_or_missing_positionals():
6421

6522
@signature
@@ -101,15 +58,19 @@ def bar(b, a): # pragma: nocover
10158
assert not compatible(bar, foo)
10259

10360

104-
def test_disallow_kwonly_to_positional():
61+
def test_allow_new_params_with_defaults_no_kwonly():
10562

10663
@signature
107-
def foo(a, *, b): # pragma: nocover
64+
def iface(a, b, c): # pragma: nocover
10865
pass
10966

11067
@signature
111-
def bar(a, b): # pragma: nocover
68+
def impl(a, b, c, d=3, e=5, f=5): # pragma: nocover
11269
pass
11370

114-
assert not compatible(foo, bar)
115-
assert not compatible(bar, foo)
71+
assert compatible(impl, iface)
72+
assert not compatible(iface, impl)
73+
74+
75+
if PY3: # pragma: nocover
76+
from ._py3_typecheck_tests import *

interface/typecheck.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""
22
Utilities for typed interfaces.
33
"""
4-
from inspect import Parameter
5-
from itertools import starmap, takewhile, zip_longest
4+
from itertools import starmap, takewhile
65

6+
from .compat import Parameter, zip_longest
77
from .functional import complement, dzip, valfilter
88

99

@@ -23,7 +23,7 @@ def compatible(impl_sig, iface_sig):
2323
implementation.
2424
2525
Consequently, the following differences are allowed between the signature
26-
of an implementation methodand the signature of its interface definition:
26+
of an implementation method and the signature of its interface definition:
2727
2828
1. An implementation may add new arguments to an interface iff:
2929
a. All new arguments have default values.

interface/typed_signature.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
This is useful for when we care about the distinction between different kinds
55
of callables, e.g., between methods, classmethods, and staticmethods.
66
"""
7-
from inspect import signature
7+
from .compat import signature
88

99

1010
class TypedSignature(object):

setup.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ def extras_require():
2121

2222

2323
def install_requires():
24+
requires = ['six']
2425
if sys.version_info[:2] < (3, 5):
25-
return ["typing>=3.5.2"]
26-
return []
26+
requires.append("typing>=3.5.2")
27+
if sys.version_info[0] == 2:
28+
requires.append("funcsigs>=1.0.2")
29+
return requires
2730

2831

2932
setup(

0 commit comments

Comments
 (0)