Skip to content

Commit 013d258

Browse files
author
Scott Sanderson
committed
Initial commit.
1 parent 5c29737 commit 013d258

File tree

8 files changed

+499
-0
lines changed

8 files changed

+499
-0
lines changed

.gitignore

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
.bundle
2+
db/*.sqlite3
3+
log/*.log
4+
*.log
5+
tmp/**/*
6+
tmp/*
7+
*.swp
8+
*~
9+
#mac autosaving file
10+
.DS_Store
11+
*.py[co]
12+
13+
# Installer logs
14+
pip-log.txt
15+
16+
# Unit test / coverage reports
17+
.coverage
18+
htmlcov
19+
.tox
20+
test.log
21+
.noseids
22+
*.xlsx
23+
24+
# Compiled python files
25+
*.py[co]
26+
27+
# Packages
28+
*.egg
29+
*.egg-info
30+
dist
31+
build
32+
eggs
33+
cover
34+
parts
35+
bin
36+
var
37+
sdist
38+
develop-eggs
39+
.installed.cfg
40+
coverage.xml
41+
nosetests.xml
42+
43+
# C Extensions
44+
*.o
45+
*.so
46+
*.out
47+
48+
# Vim
49+
*.swp
50+
*.swo
51+
52+
# Built documentation
53+
docs/_build/*
54+
55+
# database of vbench
56+
benchmarks.db
57+
58+
# Vagrant temp folder
59+
.vagrant
60+
61+
# pypi
62+
MANIFEST
63+
64+
# pytest
65+
.cache

.travis.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
language: python
2+
sudo: false
3+
python:
4+
- "3.4"
5+
- "3.5"
6+
7+
before_script:
8+
- pip install tox
9+
10+
script:
11+
- if [[ $TRAVIS_PYTHON_VERSION = '3.4' ]]; then tox -e py34; fi
12+
- if [[ $TRAVIS_PYTHON_VERSION = '3.5' ]]; then tox -e py35; fi

interface/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .interface import implements, Interface
2+
3+
__all__ = [
4+
'Interface',
5+
'implements',
6+
]

interface/interface.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
"""
2+
interface
3+
---------
4+
"""
5+
from functools import wraps
6+
import inspect
7+
from operator import itemgetter
8+
from textwrap import dedent
9+
from weakref import WeakKeyDictionary
10+
11+
first = itemgetter(0)
12+
13+
14+
def compatible(meth_sig, iface_sig):
15+
"""
16+
Check if ``method``'s signature is compatible with ``signature``.
17+
"""
18+
# TODO: Allow method to provide defaults and optional extensions to
19+
# ``signature``.
20+
return meth_sig == iface_sig
21+
22+
23+
def strict_issubclass(t, parent):
24+
return issubclass(t, parent) and t is not parent
25+
26+
27+
class InterfaceMeta(type):
28+
"""
29+
Metaclass for interfaces.
30+
31+
Supplies a ``_signatures`` attribute and a ``check_implementation`` method.
32+
"""
33+
def __new__(mcls, name, bases, clsdict):
34+
signatures = {}
35+
for k, v in clsdict.items():
36+
try:
37+
signatures[k] = inspect.signature(v)
38+
except TypeError:
39+
pass
40+
41+
clsdict['_signatures'] = signatures
42+
return super().__new__(mcls, name, bases, clsdict)
43+
44+
def _diff_signatures(self, type_):
45+
"""
46+
Diff our method signatures against the methods provided by type_.
47+
48+
Parameters
49+
----------
50+
type_ : type
51+
The type to check.
52+
53+
Returns
54+
-------
55+
missing, mismatched : list[str], dict[str -> signature]
56+
``missing`` is a list of missing method names.
57+
``mismatched`` is a dict mapping method names to incorrect
58+
signatures.
59+
"""
60+
missing = []
61+
mismatched = {}
62+
for name, iface_sig in self._signatures.items():
63+
try:
64+
f = getattr(type_, name)
65+
except AttributeError:
66+
missing.append(name)
67+
continue
68+
f_sig = inspect.signature(f)
69+
if not compatible(f_sig, iface_sig):
70+
mismatched[name] = f_sig
71+
return missing, mismatched
72+
73+
def check_conforms(self, type_):
74+
"""
75+
Check whether a type implements our interface.
76+
77+
Parameters
78+
----------
79+
type_ : type
80+
The type to check.
81+
82+
Raises
83+
------
84+
TypeError
85+
If ``type_`` doesn't conform to our interface.
86+
87+
Returns
88+
-------
89+
None
90+
"""
91+
missing, mismatched = self._diff_signatures(type_)
92+
if not missing and not mismatched:
93+
return
94+
raise self._invalid_implementation(type_, missing, mismatched)
95+
96+
def _invalid_implementation(self, t, missing, mismatched):
97+
"""
98+
Make a TypeError explaining why ``t`` doesn't implement our interface.
99+
"""
100+
assert missing or mismatched, "Implementation wasn't invalid."
101+
102+
message = "class {C} failed to implement interface {I}:".format(
103+
C=t.__name__,
104+
I=self.__name__,
105+
)
106+
if missing:
107+
message += dedent(
108+
"""
109+
110+
The following methods were not implemented:
111+
{missing_methods}"""
112+
).format(missing_methods=self._format_missing_methods(missing))
113+
114+
if mismatched:
115+
message += (
116+
"\n\nThe following methods were implemented but had invalid"
117+
" signatures:\n"
118+
"{mismatched_methods}"
119+
).format(
120+
mismatched_methods=self._format_mismatched_methods(mismatched),
121+
)
122+
return TypeError(message)
123+
124+
def _format_missing_methods(self, missing):
125+
return "\n".join(sorted([
126+
" - {name}{sig}".format(name=name, sig=self._signatures[name])
127+
for name in missing
128+
]))
129+
130+
def _format_mismatched_methods(self, mismatched):
131+
return "\n".join(sorted([
132+
" - {name}{actual} != {name}{expected}".format(
133+
name=name,
134+
actual=bad_sig,
135+
expected=self._signatures[name],
136+
)
137+
for name, bad_sig in mismatched.items()
138+
]))
139+
140+
141+
class Interface(metaclass=InterfaceMeta):
142+
"""
143+
Base class for interface definitions.
144+
"""
145+
146+
147+
class Implements:
148+
"""
149+
Base class for an implementation of an interface.
150+
"""
151+
152+
153+
class ImplementsMeta(type):
154+
"""
155+
Metaclass for implementations of particular interfaces.
156+
"""
157+
def __new__(mcls, name, bases, clsdict, base=False):
158+
newtype = super().__new__(mcls, name, bases, clsdict)
159+
160+
if base:
161+
# Don't do checks on the types returned by ``implements``.
162+
return newtype
163+
164+
for iface in newtype.interfaces():
165+
iface.check_conforms(newtype)
166+
167+
return newtype
168+
169+
def __init__(mcls, name, bases, clsdict, base=False):
170+
super().__init__(name, bases, clsdict)
171+
172+
def interfaces(self):
173+
"""
174+
Return a generator of interfaces implemented by this type.
175+
176+
Yields
177+
------
178+
iface : Interface
179+
"""
180+
for base in self.mro():
181+
if strict_issubclass(base, Implements):
182+
yield base.interface
183+
184+
185+
def weakmemoize_implements(f):
186+
"One-off weakmemoize implementation for ``implements``."
187+
_memo = WeakKeyDictionary()
188+
189+
@wraps(f)
190+
def _f(I):
191+
try:
192+
return _memo[I]
193+
except KeyError:
194+
pass
195+
ret = f(I)
196+
_memo[I] = ret
197+
return ret
198+
return _f
199+
200+
201+
@weakmemoize_implements
202+
def implements(I):
203+
"""
204+
Make a base for classes that implement ``I``.
205+
206+
Parameters
207+
----------
208+
I : Interface
209+
210+
Returns
211+
-------
212+
base : type
213+
A type validating that subclasses must implement all interface
214+
methods of I.
215+
"""
216+
if not issubclass(I, Interface):
217+
raise TypeError(
218+
"implements() expected an Interface, but got %s." % I
219+
)
220+
221+
name = "Implements{I}".format(I=I.__name__)
222+
doc = dedent(
223+
"""\
224+
Implementation of {I}.
225+
226+
Methods
227+
-------
228+
{methods}"""
229+
).format(
230+
I=I.__name__,
231+
methods="\n".join(
232+
"{name}{sig}".format(name=name, sig=sig)
233+
for name, sig in sorted(list(I._signatures.items()), key=first)
234+
)
235+
)
236+
return ImplementsMeta(
237+
name,
238+
(Implements,),
239+
{'__doc__': doc, 'interface': I},
240+
base=True,
241+
)

interface/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)