Skip to content

Commit 2f49562

Browse files
committed
sync with Atlas repo
1 parent b3e36a2 commit 2f49562

File tree

12 files changed

+185
-114
lines changed

12 files changed

+185
-114
lines changed

dicts/strkeydict.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
# BEGIN STRKEYDICT
4343

4444
import collections
45-
import collections.abc
4645

4746

4847
class StrKeyDict(collections.UserDict): # <1>
@@ -58,15 +57,4 @@ def __contains__(self, key):
5857
def __setitem__(self, key, item):
5958
self.data[str(key)] = item # <4>
6059

61-
def update(self, iterable=None, **kwds):
62-
if iterable is not None:
63-
if isinstance(iterable, collections.abc.Mapping): # <5>
64-
pairs = iterable.items()
65-
else:
66-
pairs = ((k, v) for k, v in iterable) # <6>
67-
for key, value in pairs:
68-
self[key] = value # <7>
69-
if kwds:
70-
self.update(kwds) # <8>
71-
7260
# END STRKEYDICT

interfaces/bingo.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ def __init__(self, items):
1111
def load(self, items):
1212
self._balls.extend(items)
1313

14-
def pop(self):
14+
def pick(self):
1515
try:
16-
position = random.randrange(len(self._balls)) # <4>
16+
position = random.randrange(len(self._balls)) # <3>
1717
except ValueError:
1818
raise LookupError('pop from empty BingoCage')
19-
return self._balls.pop(position)
19+
return self._balls.pop(position) # <4>

interfaces/diamond.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
class A:
2+
def ping(self):
3+
print('ping:', self)
4+
5+
6+
class B(A):
7+
def pong(self):
8+
print('pong:', self)
9+
10+
11+
class C(A):
12+
def pong(self):
13+
print('PONG:', self)
14+
15+
16+
class D(B, C):
17+
def pingpong(self):
18+
super().ping()
19+
super().pong()

interfaces/drum.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ def load(self, iterable):
1313
self._balls.extend(iterable)
1414
shuffle(self._balls)
1515

16-
def pop(self):
16+
def pick(self):
1717
return self._balls.pop()

interfaces/lotto.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def load(self, iterable):
1717
self._balls.extend(iterable)
1818
self.randomizer.shuffle(self._balls) # <2>
1919

20-
def pop(self):
20+
def pick(self):
2121
return self._balls.pop() # <3>
2222

2323
def loaded(self): # <4>

interfaces/pypy_difference.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
====================================
3+
Differences between PyPy and CPython
4+
====================================
5+
6+
Note: this is an excerpt from the `PyPy`_ documentation. On Nov. 19, 2014 I ran this test on PyPy 2.4.0 and PyPy3 2.4.0 and the result was not as described, but was the same as with CPython: 'foo'.
7+
8+
.. _PyPy: http://pypy.readthedocs.org/en/latest/cpython_differences.html#subclasses-of-built-in-types
9+
10+
Subclasses of built-in types
11+
----------------------------
12+
13+
Officially, CPython has no rule at all for when exactly
14+
overridden method of subclasses of built-in types get
15+
implicitly called or not. As an approximation, these methods
16+
are never called by other built-in methods of the same object.
17+
For example, an overridden ``__getitem__()`` in a subclass of
18+
``dict`` will not be called by e.g. the built-in ``get()``
19+
method.
20+
21+
The above is true both in CPython and in PyPy. Differences
22+
can occur about whether a built-in function or method will
23+
call an overridden method of *another* object than ``self``.
24+
In PyPy, they are generally always called, whereas not in
25+
CPython. For example, in PyPy, ``dict1.update(dict2)``
26+
considers that ``dict2`` is just a general mapping object, and
27+
will thus call overridden ``keys()`` and ``__getitem__()``
28+
methods on it. So the following code prints ``42`` on PyPy
29+
but ``foo`` on CPython::
30+
31+
>>> class D(dict):
32+
... def __getitem__(self, key):
33+
... return 42
34+
...
35+
>>>
36+
>>> d1 = {}
37+
>>> d2 = D(a='foo')
38+
>>> d1.update(d2)
39+
>>> print(d1['a'])
40+
42
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
====================================
2+
Subclassing built-in caveats
3+
====================================
4+
5+
::
6+
7+
>>> class D1(dict):
8+
... def __getitem__(self, key):
9+
... return 42
10+
...
11+
>>> d1 = D1(a='foo')
12+
>>> d1
13+
{'a': 'foo'}
14+
>>> d1['a']
15+
42
16+
>>> d1.get('a')
17+
'foo'
18+
19+
::
20+
21+
>>> class D2(dict):
22+
... def get(self, key):
23+
... return 42
24+
...
25+
>>> d2 = D2(a='foo')
26+
>>> d2
27+
{'a': 'foo'}
28+
>>> d2['a']
29+
'foo'
30+
>>> d2.get('a')
31+
42

interfaces/tombola.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
1-
from abc import ABCMeta, abstractmethod
1+
from abc import ABC, abstractmethod
22

33

4-
class Tombola(metaclass=ABCMeta): # <1>
4+
class Tombola(ABC): # <1>
55

66
@abstractmethod
77
def __init__(self, iterable): # <2>
8-
raise NotImplementedError
8+
"""New instance is loaded from an iterable."""
99

1010
@abstractmethod
11-
def load(self):
12-
raise NotImplementedError
11+
def load(self, iterable):
12+
"""Add items from an iterable."""
1313

1414
@abstractmethod
15-
def pop(self):
16-
raise NotImplementedError
15+
def pick(self): # <3>
16+
"""Remove item at random, returning it.
1717
18-
def loaded(self): # <3>
18+
This method should raise `LookupError` when the instance is empty.
19+
"""
20+
21+
def loaded(self): # <4>
1922
try:
20-
item = self.pop()
23+
item = self.pick()
2124
except LookupError:
2225
return False
2326
else:

interfaces/tombola_runner.py

Lines changed: 18 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,49 +4,30 @@
44

55
from tombola import Tombola
66

7-
TESTS = 'tombola_tests.rst'
7+
TEST_FILE = 'tombola_tests.rst'
8+
MODULE_NAMES = 'bingo lotto tombolist drum'.split()
9+
TEST_MSG = '{0:16} {1.attempted:2} tests, {1.failed:2} failed - {2}'
810

9-
MODULES = 'bingo lotto tombolist drum'.split()
1011

12+
def test(cls, verbose=False):
1113

12-
def test_module(module_name, verbose=False):
13-
14-
module = importlib.import_module(module_name)
15-
16-
tombola_class = None
17-
for name in dir(module):
18-
obj = getattr(module, name)
19-
if (isinstance(obj, type) and
20-
issubclass(obj, Tombola) and
21-
obj.__module__ == module_name):
22-
tombola_class = obj
23-
break # stop at first matching class
24-
25-
if tombola_class is None:
26-
print('ERROR: no Tombola subclass found in', module_name)
27-
sys.exit(1)
28-
29-
res = doctest.testfile(TESTS,
30-
globs={'TombolaUnderTest': tombola_class},
14+
res = doctest.testfile(TEST_FILE,
15+
globs={'TombolaUnderTest': cls},
3116
verbose=verbose,
3217
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)
33-
print('{:10} {}'.format(module_name, res))
18+
tag = 'FAIL' if res.failed else 'OK'
19+
print(TEST_MSG.format(cls.__name__, res, tag))
3420

3521

3622
if __name__ == '__main__':
3723

38-
args = sys.argv[:]
39-
if '-v' in args:
40-
args.remove('-v')
41-
verbose = True
42-
else:
43-
verbose = False
44-
45-
if len(args) == 2:
46-
module_names = [args[1]]
47-
else:
48-
module_names = MODULES
49-
50-
for name in module_names:
51-
print('*' * 60, name)
52-
test_module(name, verbose)
24+
for name in MODULE_NAMES: # import modules to test, by name
25+
importlib.import_module(name)
26+
27+
verbose = '-v' in sys.argv
28+
29+
real_subclasses = Tombola.__subclasses__()
30+
virtual_subclasses = list(Tombola._abc_registry)
31+
32+
for cls in real_subclasses + virtual_subclasses:
33+
test(cls, verbose)

interfaces/tombola_tests.rst

Lines changed: 47 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,64 +7,71 @@ Every concrete subclass of Tombola should pass these tests.
77

88
Create and load instance from iterable::
99

10-
>>> balls = list(range(3))
11-
>>> globe = TombolaUnderTest(balls)
12-
>>> globe.loaded()
13-
True
10+
>>> balls = list(range(3))
11+
>>> globe = TombolaUnderTest(balls)
12+
>>> globe.loaded()
13+
True
1414

1515

16-
Pop and collect balls::
16+
Pick and collect balls::
1717

18-
>>> picks = []
19-
>>> picks.append(globe.pop())
20-
>>> picks.append(globe.pop())
21-
>>> picks.append(globe.pop())
18+
>>> picks = []
19+
>>> picks.append(globe.pick())
20+
>>> picks.append(globe.pick())
21+
>>> picks.append(globe.pick())
2222

2323

2424
Check state and results::
2525

26-
>>> globe.loaded()
27-
False
28-
>>> sorted(picks) == balls
29-
True
26+
>>> globe.loaded()
27+
False
28+
>>> sorted(picks) == balls
29+
True
3030

3131

3232
Reload::
3333

34-
>>> globe.load(balls)
35-
>>> globe.loaded()
36-
True
37-
>>> picks = [globe.pop() for i in balls]
38-
>>> globe.loaded()
39-
False
34+
>>> globe.load(balls)
35+
>>> globe.loaded()
36+
True
37+
>>> picks = [globe.pick() for i in balls]
38+
>>> globe.loaded()
39+
False
4040

4141

42-
Load and pop 20 balls to verify that the order has changed::
42+
Check that `LookupError` (or a subclass) is the exception
43+
thrown when the device is empty::
4344

44-
>>> balls = list(range(20))
45-
>>> globe = TombolaUnderTest(balls)
46-
>>> picks = []
47-
>>> while globe.loaded():
48-
... picks.append(globe.pop())
49-
>>> len(picks) == len(balls)
50-
True
51-
>>> picks != balls
52-
True
45+
>>> globe = TombolaUnderTest([])
46+
>>> try:
47+
... globe.pick()
48+
... except LookupError as exc:
49+
... print('OK')
50+
OK
5351

5452

55-
Also check that the order is not simply reversed either::
53+
Load and pick 100 balls to verify that they are all come out::
5654

57-
>>> picks[::-1] != balls
58-
True
55+
>>> balls = list(range(100))
56+
>>> globe = TombolaUnderTest(balls)
57+
>>> picks = []
58+
>>> while globe.loaded():
59+
... picks.append(globe.pick())
60+
>>> len(picks) == len(balls)
61+
True
62+
>>> set(picks) == set(balls)
63+
True
5964

60-
Note: last 2 tests each have 1 chance in 20! (factorial) of failing even if the implementation is OK. 1/20!, or approximately 4.11e-19, is the probability of the 20 balls coming out, by chance, in the exact order the were loaded.
6165

62-
Check that `LookupError` (or a subclass) is the exception thrown when the device is empty::
66+
Check that the order has changed is not simply reversed either::
6367

64-
>>> globe = TombolaUnderTest([])
65-
>>> try:
66-
... globe.pop()
67-
... except LookupError as exc:
68-
... print('OK')
69-
OK
68+
>>> picks != balls
69+
True
70+
>>> picks[::-1] != balls
71+
True
7072

73+
Note: the previous 2 tests have a *very* small chance of failing
74+
even if the implementation is OK. The probability of the 100
75+
balls coming out, by chance, in the order they were loaded is
76+
1/100!, or approximately 1.07e-158. It's much easier to win the
77+
Lotto or to become a billionaire working as a programmer.

0 commit comments

Comments
 (0)