sync with Atlas repo

This commit is contained in:
Luciano Ramalho 2014-11-19 17:10:02 -02:00
parent b3e36a2a41
commit 2f495627fb
12 changed files with 183 additions and 112 deletions

View File

@ -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

View File

@ -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
View 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()

View File

@ -13,5 +13,5 @@ class TumblingDrum(Tombola):
self._balls.extend(iterable)
shuffle(self._balls)
def pop(self):
def pick(self):
return self._balls.pop()

View File

@ -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>

View 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

View 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

View File

@ -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:

View File

@ -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)

View File

@ -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.

View File

@ -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
"""

View File

@ -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):