sync with Atlas repo
This commit is contained in:
parent
b3e36a2a41
commit
2f495627fb
@ -42,7 +42,6 @@ Tests for update using a `dict` or a sequence of pairs::
|
||||
# BEGIN STRKEYDICT
|
||||
|
||||
import collections
|
||||
import collections.abc
|
||||
|
||||
|
||||
class StrKeyDict(collections.UserDict): # <1>
|
||||
@ -58,15 +57,4 @@ class StrKeyDict(collections.UserDict): # <1>
|
||||
def __setitem__(self, key, item):
|
||||
self.data[str(key)] = item # <4>
|
||||
|
||||
def update(self, iterable=None, **kwds):
|
||||
if iterable is not None:
|
||||
if isinstance(iterable, collections.abc.Mapping): # <5>
|
||||
pairs = iterable.items()
|
||||
else:
|
||||
pairs = ((k, v) for k, v in iterable) # <6>
|
||||
for key, value in pairs:
|
||||
self[key] = value # <7>
|
||||
if kwds:
|
||||
self.update(kwds) # <8>
|
||||
|
||||
# END STRKEYDICT
|
||||
|
@ -11,9 +11,9 @@ class BingoCage(Tombola): # <1>
|
||||
def load(self, items):
|
||||
self._balls.extend(items)
|
||||
|
||||
def pop(self):
|
||||
def pick(self):
|
||||
try:
|
||||
position = random.randrange(len(self._balls)) # <4>
|
||||
position = random.randrange(len(self._balls)) # <3>
|
||||
except ValueError:
|
||||
raise LookupError('pop from empty BingoCage')
|
||||
return self._balls.pop(position)
|
||||
return self._balls.pop(position) # <4>
|
||||
|
19
interfaces/diamond.py
Normal file
19
interfaces/diamond.py
Normal file
@ -0,0 +1,19 @@
|
||||
class A:
|
||||
def ping(self):
|
||||
print('ping:', self)
|
||||
|
||||
|
||||
class B(A):
|
||||
def pong(self):
|
||||
print('pong:', self)
|
||||
|
||||
|
||||
class C(A):
|
||||
def pong(self):
|
||||
print('PONG:', self)
|
||||
|
||||
|
||||
class D(B, C):
|
||||
def pingpong(self):
|
||||
super().ping()
|
||||
super().pong()
|
@ -13,5 +13,5 @@ class TumblingDrum(Tombola):
|
||||
self._balls.extend(iterable)
|
||||
shuffle(self._balls)
|
||||
|
||||
def pop(self):
|
||||
def pick(self):
|
||||
return self._balls.pop()
|
||||
|
@ -17,7 +17,7 @@ class LotteryBlower(Tombola):
|
||||
self._balls.extend(iterable)
|
||||
self.randomizer.shuffle(self._balls) # <2>
|
||||
|
||||
def pop(self):
|
||||
def pick(self):
|
||||
return self._balls.pop() # <3>
|
||||
|
||||
def loaded(self): # <4>
|
||||
|
40
interfaces/pypy_difference.rst
Normal file
40
interfaces/pypy_difference.rst
Normal file
@ -0,0 +1,40 @@
|
||||
|
||||
====================================
|
||||
Differences between PyPy and CPython
|
||||
====================================
|
||||
|
||||
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'.
|
||||
|
||||
.. _PyPy: http://pypy.readthedocs.org/en/latest/cpython_differences.html#subclasses-of-built-in-types
|
||||
|
||||
Subclasses of built-in types
|
||||
----------------------------
|
||||
|
||||
Officially, CPython has no rule at all for when exactly
|
||||
overridden method of subclasses of built-in types get
|
||||
implicitly called or not. As an approximation, these methods
|
||||
are never called by other built-in methods of the same object.
|
||||
For example, an overridden ``__getitem__()`` in a subclass of
|
||||
``dict`` will not be called by e.g. the built-in ``get()``
|
||||
method.
|
||||
|
||||
The above is true both in CPython and in PyPy. Differences
|
||||
can occur about whether a built-in function or method will
|
||||
call an overridden method of *another* object than ``self``.
|
||||
In PyPy, they are generally always called, whereas not in
|
||||
CPython. For example, in PyPy, ``dict1.update(dict2)``
|
||||
considers that ``dict2`` is just a general mapping object, and
|
||||
will thus call overridden ``keys()`` and ``__getitem__()``
|
||||
methods on it. So the following code prints ``42`` on PyPy
|
||||
but ``foo`` on CPython::
|
||||
|
||||
>>> class D(dict):
|
||||
... def __getitem__(self, key):
|
||||
... return 42
|
||||
...
|
||||
>>>
|
||||
>>> d1 = {}
|
||||
>>> d2 = D(a='foo')
|
||||
>>> d1.update(d2)
|
||||
>>> print(d1['a'])
|
||||
42
|
31
interfaces/subclassing_builtins.rst
Normal file
31
interfaces/subclassing_builtins.rst
Normal file
@ -0,0 +1,31 @@
|
||||
====================================
|
||||
Subclassing built-in caveats
|
||||
====================================
|
||||
|
||||
::
|
||||
|
||||
>>> class D1(dict):
|
||||
... def __getitem__(self, key):
|
||||
... return 42
|
||||
...
|
||||
>>> d1 = D1(a='foo')
|
||||
>>> d1
|
||||
{'a': 'foo'}
|
||||
>>> d1['a']
|
||||
42
|
||||
>>> d1.get('a')
|
||||
'foo'
|
||||
|
||||
::
|
||||
|
||||
>>> class D2(dict):
|
||||
... def get(self, key):
|
||||
... return 42
|
||||
...
|
||||
>>> d2 = D2(a='foo')
|
||||
>>> d2
|
||||
{'a': 'foo'}
|
||||
>>> d2['a']
|
||||
'foo'
|
||||
>>> d2.get('a')
|
||||
42
|
@ -1,23 +1,26 @@
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class Tombola(metaclass=ABCMeta): # <1>
|
||||
class Tombola(ABC): # <1>
|
||||
|
||||
@abstractmethod
|
||||
def __init__(self, iterable): # <2>
|
||||
raise NotImplementedError
|
||||
"""New instance is loaded from an iterable."""
|
||||
|
||||
@abstractmethod
|
||||
def load(self):
|
||||
raise NotImplementedError
|
||||
def load(self, iterable):
|
||||
"""Add items from an iterable."""
|
||||
|
||||
@abstractmethod
|
||||
def pop(self):
|
||||
raise NotImplementedError
|
||||
def pick(self): # <3>
|
||||
"""Remove item at random, returning it.
|
||||
|
||||
def loaded(self): # <3>
|
||||
This method should raise `LookupError` when the instance is empty.
|
||||
"""
|
||||
|
||||
def loaded(self): # <4>
|
||||
try:
|
||||
item = self.pop()
|
||||
item = self.pick()
|
||||
except LookupError:
|
||||
return False
|
||||
else:
|
||||
|
@ -4,49 +4,30 @@ import doctest
|
||||
|
||||
from tombola import Tombola
|
||||
|
||||
TESTS = 'tombola_tests.rst'
|
||||
|
||||
MODULES = 'bingo lotto tombolist drum'.split()
|
||||
TEST_FILE = 'tombola_tests.rst'
|
||||
MODULE_NAMES = 'bingo lotto tombolist drum'.split()
|
||||
TEST_MSG = '{0:16} {1.attempted:2} tests, {1.failed:2} failed - {2}'
|
||||
|
||||
|
||||
def test_module(module_name, verbose=False):
|
||||
def test(cls, verbose=False):
|
||||
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
tombola_class = None
|
||||
for name in dir(module):
|
||||
obj = getattr(module, name)
|
||||
if (isinstance(obj, type) and
|
||||
issubclass(obj, Tombola) and
|
||||
obj.__module__ == module_name):
|
||||
tombola_class = obj
|
||||
break # stop at first matching class
|
||||
|
||||
if tombola_class is None:
|
||||
print('ERROR: no Tombola subclass found in', module_name)
|
||||
sys.exit(1)
|
||||
|
||||
res = doctest.testfile(TESTS,
|
||||
globs={'TombolaUnderTest': tombola_class},
|
||||
res = doctest.testfile(TEST_FILE,
|
||||
globs={'TombolaUnderTest': cls},
|
||||
verbose=verbose,
|
||||
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)
|
||||
print('{:10} {}'.format(module_name, res))
|
||||
tag = 'FAIL' if res.failed else 'OK'
|
||||
print(TEST_MSG.format(cls.__name__, res, tag))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
args = sys.argv[:]
|
||||
if '-v' in args:
|
||||
args.remove('-v')
|
||||
verbose = True
|
||||
else:
|
||||
verbose = False
|
||||
for name in MODULE_NAMES: # import modules to test, by name
|
||||
importlib.import_module(name)
|
||||
|
||||
if len(args) == 2:
|
||||
module_names = [args[1]]
|
||||
else:
|
||||
module_names = MODULES
|
||||
verbose = '-v' in sys.argv
|
||||
|
||||
for name in module_names:
|
||||
print('*' * 60, name)
|
||||
test_module(name, verbose)
|
||||
real_subclasses = Tombola.__subclasses__()
|
||||
virtual_subclasses = list(Tombola._abc_registry)
|
||||
|
||||
for cls in real_subclasses + virtual_subclasses:
|
||||
test(cls, verbose)
|
||||
|
@ -7,64 +7,71 @@ Every concrete subclass of Tombola should pass these tests.
|
||||
|
||||
Create and load instance from iterable::
|
||||
|
||||
>>> balls = list(range(3))
|
||||
>>> globe = TombolaUnderTest(balls)
|
||||
>>> globe.loaded()
|
||||
True
|
||||
>>> balls = list(range(3))
|
||||
>>> globe = TombolaUnderTest(balls)
|
||||
>>> globe.loaded()
|
||||
True
|
||||
|
||||
|
||||
Pop and collect balls::
|
||||
Pick and collect balls::
|
||||
|
||||
>>> picks = []
|
||||
>>> picks.append(globe.pop())
|
||||
>>> picks.append(globe.pop())
|
||||
>>> picks.append(globe.pop())
|
||||
>>> picks = []
|
||||
>>> picks.append(globe.pick())
|
||||
>>> picks.append(globe.pick())
|
||||
>>> picks.append(globe.pick())
|
||||
|
||||
|
||||
Check state and results::
|
||||
|
||||
>>> globe.loaded()
|
||||
False
|
||||
>>> sorted(picks) == balls
|
||||
True
|
||||
>>> globe.loaded()
|
||||
False
|
||||
>>> sorted(picks) == balls
|
||||
True
|
||||
|
||||
|
||||
Reload::
|
||||
|
||||
>>> globe.load(balls)
|
||||
>>> globe.loaded()
|
||||
True
|
||||
>>> picks = [globe.pop() for i in balls]
|
||||
>>> globe.loaded()
|
||||
False
|
||||
>>> globe.load(balls)
|
||||
>>> globe.loaded()
|
||||
True
|
||||
>>> picks = [globe.pick() for i in balls]
|
||||
>>> globe.loaded()
|
||||
False
|
||||
|
||||
|
||||
Load and pop 20 balls to verify that the order has changed::
|
||||
Check that `LookupError` (or a subclass) is the exception
|
||||
thrown when the device is empty::
|
||||
|
||||
>>> balls = list(range(20))
|
||||
>>> globe = TombolaUnderTest(balls)
|
||||
>>> picks = []
|
||||
>>> while globe.loaded():
|
||||
... picks.append(globe.pop())
|
||||
>>> len(picks) == len(balls)
|
||||
True
|
||||
>>> picks != balls
|
||||
True
|
||||
>>> globe = TombolaUnderTest([])
|
||||
>>> try:
|
||||
... globe.pick()
|
||||
... except LookupError as exc:
|
||||
... print('OK')
|
||||
OK
|
||||
|
||||
|
||||
Also check that the order is not simply reversed either::
|
||||
Load and pick 100 balls to verify that they are all come out::
|
||||
|
||||
>>> picks[::-1] != balls
|
||||
True
|
||||
>>> balls = list(range(100))
|
||||
>>> globe = TombolaUnderTest(balls)
|
||||
>>> picks = []
|
||||
>>> while globe.loaded():
|
||||
... picks.append(globe.pick())
|
||||
>>> len(picks) == len(balls)
|
||||
True
|
||||
>>> set(picks) == set(balls)
|
||||
True
|
||||
|
||||
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.
|
||||
|
||||
Check that `LookupError` (or a subclass) is the exception thrown when the device is empty::
|
||||
Check that the order has changed is not simply reversed either::
|
||||
|
||||
>>> globe = TombolaUnderTest([])
|
||||
>>> try:
|
||||
... globe.pop()
|
||||
... except LookupError as exc:
|
||||
... print('OK')
|
||||
OK
|
||||
>>> picks != balls
|
||||
True
|
||||
>>> picks[::-1] != balls
|
||||
True
|
||||
|
||||
Note: the previous 2 tests have a *very* small chance of failing
|
||||
even if the implementation is OK. The probability of the 100
|
||||
balls coming out, by chance, in the order they were loaded is
|
||||
1/100!, or approximately 1.07e-158. It's much easier to win the
|
||||
Lotto or to become a billionaire working as a programmer.
|
||||
|
@ -2,18 +2,20 @@ from random import randrange
|
||||
|
||||
from tombola import Tombola
|
||||
|
||||
@Tombola.register # <1>
|
||||
class TomboList(list): # <2>
|
||||
|
||||
class TomboList(list): # <1>
|
||||
|
||||
def pop(self):
|
||||
if self: # <2>
|
||||
return super().pop(randrange(len(self))) # <3>
|
||||
def pick(self):
|
||||
if self: # <3>
|
||||
position = randrange(len(self))
|
||||
return super().pop(position) # <4>
|
||||
else:
|
||||
raise LookupError('pop from empty TomboList')
|
||||
|
||||
def load(self, iterable): self.extend(iterable) # <4>
|
||||
def load(self, iterable): self.extend(iterable) # <5>
|
||||
|
||||
def loaded(self): return bool(self) # <5>
|
||||
def loaded(self): return bool(self) # <6>
|
||||
|
||||
|
||||
Tombola.register(TomboList) # <6>
|
||||
"""
|
||||
Tombola.register(TomboList) # <- Python 3.2 or earlier
|
||||
"""
|
||||
|
@ -28,7 +28,7 @@
|
||||
"""
|
||||
# BEGIN CLASSIC_STRATEGY
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import namedtuple
|
||||
|
||||
Customer = namedtuple('Customer', 'name fidelity')
|
||||
@ -69,7 +69,7 @@ class Order: # the Context
|
||||
return fmt.format(self.total(), self.due())
|
||||
|
||||
|
||||
class Promotion(metaclass=ABCMeta): # the Strategy
|
||||
class Promotion(ABC): # the Strategy: an Abstract Base Class
|
||||
|
||||
@abstractmethod
|
||||
def discount(self, order):
|
||||
|
Loading…
x
Reference in New Issue
Block a user