ch08, 09, 10: example files

This commit is contained in:
Luciano Ramalho 2020-06-11 14:58:15 -03:00
parent 42861b64d8
commit bf4a2be8b9
111 changed files with 4707 additions and 0 deletions

View File

@ -0,0 +1 @@
== Type Hints in Function Definitions

View File

@ -0,0 +1,67 @@
#!/usr/bin/env python3
import sys
from array import array
from typing import Mapping, MutableSequence, Callable, Iterable, Sequence, Union, Any
OPERATORS: Mapping[str, Callable[[float, float], float]] = {
'+': lambda a, b: a + b,
'-': lambda a, b: a - b,
'*': lambda a, b: a * b,
'/': lambda a, b: a / b,
'^': lambda a, b: a ** b,
}
Stack = MutableSequence[float]
def parse_token(token: str) -> Union[str, float]:
try:
return float(token)
except ValueError:
return token
def evaluate(tokens: Iterable[str], stack: Stack) -> None:
for token in tokens:
atom = parse_token(token)
if isinstance(atom, float):
stack.append(atom)
else: # not float, must be operator
op = OPERATORS[atom]
x, y = stack.pop(), stack.pop()
result = op(y, x)
stack.append(result)
def display(s: Stack) -> str:
items = (repr(n) for n in s)
return ''.join(items) + ''
def repl(input_fn: Callable[[Any], str] = input) -> None:
"""Read-Eval-Print-Loop"""
print('Use CTRL+C to quit.', file=sys.stderr)
stack: Stack = array('d')
while True:
try:
line = input_fn('> ') # Read
except (EOFError, KeyboardInterrupt):
break
try:
evaluate(line.split(), stack) # Eval
except IndexError:
print('*** Not enough arguments.', file=sys.stderr)
except KeyError as exc:
print('*** Unknown operator:', exc.args[0], file=sys.stderr)
print(display(stack)) # Print
print()
if __name__ == '__main__':
repl()

View File

@ -0,0 +1,51 @@
from pytest import mark, approx # type: ignore
from dialogue import Dialogue # type: ignore
from calc import evaluate, repl, display, Stack
TOLERANCE = .0001
@mark.parametrize("source, want", [
('2', 2),
('2 3 +', 5),
('5 3 -', 2),
('3 5 * 2 +', 17),
('2 3 4 5 * * *', 120),
('1.1 1.1 1.1 + +', approx(3.3, TOLERANCE)),
('100 32 - 5 * 9 /', approx(37.78, TOLERANCE)),
])
def test_evaluate(source, want) -> None:
stack: Stack = []
evaluate(source.split(), stack)
assert want == stack[-1]
@mark.parametrize("value, want", [
([], ''),
([3.], '3.0 →'),
([3., 4., 5.], '3.0 │ 4.0 │ 5.0 →'),
])
def test_display(value, want) -> None:
assert want == display(value)
@mark.parametrize("session", [
"""
> 3
3.0
""",
"""
> 3 5 6
3.0 5.0 6.0
> *
3.0 30.0
> -
-27.0
""",
])
def test_repl(capsys, session) -> None:
dlg = Dialogue(session)
repl(dlg.fake_input)
captured = capsys.readouterr()
assert dlg.session.strip() == captured.out.strip()

View File

@ -0,0 +1,36 @@
import typing
from typing import Optional
def f(a: str, *b: int, **c: float) -> None:
if typing.TYPE_CHECKING:
# reveal_type(b)
reveal_type(c)
print(a, b, c)
def g(__a: int) -> None:
print(__a)
def h(a: int, /) -> None:
print(a)
def tag(
name: str,
/,
*content: str,
class_: Optional[str] = None,
foo: Optional[str] = None,
**attrs: str,
) -> str:
return repr((name, content, class_, attrs))
f(a='1')
f('1', 2, 3, x=4, y=5)
g(__a=1)
# h(a=1)
print(tag('li', 'first', 'second', id='#123'))
print(tag('li', 'first', 'second', class_='menu', id='#123'))

View File

@ -0,0 +1,15 @@
class Bird:
pass
class Duck(Bird): # <1>
def quack(self):
print('Quack!')
def alert(birdie): # <2>
birdie.quack()
def alert_duck(birdie: Duck) -> None: # <3>
birdie.quack()
def alert_bird(birdie: Bird) -> None: # <4>
birdie.quack()

View File

@ -0,0 +1,6 @@
from birds import *
daffy = Duck()
alert(daffy) # <1>
alert_duck(daffy) # <2>
alert_bird(daffy) # <3>

View File

@ -0,0 +1,9 @@
from typing import Protocol # <1>
class GooseLike(Protocol):
def honk(self, times: int) -> None: ... # <2>
def swim(self) -> None: ...
def alert(waterfowl: GooseLike) -> None: # <3>
waterfowl.honk(2)

View File

@ -0,0 +1,10 @@
from lake import alert
class Parrot:
def honk(self, times: int) -> None: # <1>
print('Honk! ' * times * 2)
ze_carioca = Parrot()
alert(ze_carioca) # <2>

View File

@ -0,0 +1,13 @@
from lake import alert # <1>
class Swan: # <2>
def honk(self, repetitions: int) -> None: # <3>
print('Honk! ' * repetitions)
def swim(self) -> None: # <4>
pass
bella = Swan()
alert(bella) # <5>

View File

@ -0,0 +1,6 @@
from birds import *
woody = Bird()
alert(woody)
alert_duck(woody)
alert_bird(woody)

29
08-def-type-hints/bus.py Normal file
View File

@ -0,0 +1,29 @@
"""
>>> import copy
>>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
>>> bus2 = copy.copy(bus1)
>>> bus3 = copy.deepcopy(bus1)
>>> bus1.drop('Bill')
>>> bus2.passengers
['Alice', 'Claire', 'David']
>>> bus3.passengers
['Alice', 'Bill', 'Claire', 'David']
"""
# tag::BUS_CLASS[]
class Bus:
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
# end::BUS_CLASS[]

View File

@ -0,0 +1,35 @@
"""
``name_index`` builds an inverted index mapping words to sets of Unicode
characters which contain that word in their names. For example::
>>> index = name_index(32, 65)
>>> sorted(index['SIGN'])
['#', '$', '%', '+', '<', '=', '>']
>>> sorted(index['DIGIT'])
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
>>> index['DIGIT'] & index['EIGHT']
{'8'}
"""
# tag::CHARINDEX[]
import sys
import re
import unicodedata
from typing import Dict, Set, Iterator
RE_WORD = re.compile('\w+')
STOP_CODE = sys.maxunicode + 1
def tokenize(text: str) -> Iterator[str]: # <1>
"""return iterable of uppercased words"""
for match in RE_WORD.finditer(text):
yield match.group().upper()
def name_index(start: int = 32, end: int = STOP_CODE) -> Dict[str, Set[str]]:
index: Dict[str, Set[str]] = {} # <2>
for char in (chr(i) for i in range(start, end)):
if name := unicodedata.name(char, ''): # <3>
for word in tokenize(name):
index.setdefault(word, set()).add(char)
return index
# end::CHARINDEX[]

View File

@ -0,0 +1,38 @@
"""
>>> clip('banana ', 6)
'banana'
>>> clip('banana ', 7)
'banana'
>>> clip('banana ', 5)
'banana'
>>> clip('banana split', 6)
'banana'
>>> clip('banana split', 7)
'banana'
>>> clip('banana split', 10)
'banana'
>>> clip('banana split', 11)
'banana'
>>> clip('banana split', 12)
'banana split'
"""
# tag::CLIP_ANNOT[]
def clip(text: str, max_len: int = 80) -> str:
"""Return new ``str`` clipped at last space before or after ``max_len``.
Return full ``text`` if no space found.
"""
end = None
if len(text) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None:
end = len(text)
return text[:end].rstrip()
# end::CLIP_ANNOT[]

View File

@ -0,0 +1,38 @@
"""
>>> clip('banana ', 6)
'banana'
>>> clip('banana ', 7)
'banana'
>>> clip('banana ', 5)
'banana'
>>> clip('banana split', 6)
'banana'
>>> clip('banana split', 7)
'banana'
>>> clip('banana split', 10)
'banana'
>>> clip('banana split', 11)
'banana'
>>> clip('banana split', 12)
'banana split'
"""
# tag::CLIP_ANNOT[]
def clip(text:str, max_len:'int > 0'=80) -> str: # <1>
"""Return text clipped at the last space before or after max_len
"""
end = None
if len(text) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None: # no spaces were found
end = len(text)
return text[:end].rstrip()
# end::CLIP_ANNOT[]

View File

@ -0,0 +1,10 @@
>>> from clip_annot import clip
>>> from inspect import signature
>>> sig = signature(clip)
>>> sig.return_annotation
<class 'str'>
>>> for param in sig.parameters.values():
... note = repr(param.annotation).ljust(13)
... print(note, ':', param.name, '=', param.default)
<class 'str'> : text = <class 'inspect._empty'>
'int > 0' : max_len = 80

View File

@ -0,0 +1,81 @@
from typing import Tuple, Mapping
NAMES = {
'aqua': 65535,
'black': 0,
'blue': 255,
'fuchsia': 16711935,
'gray': 8421504,
'green': 32768,
'lime': 65280,
'maroon': 8388608,
'navy': 128,
'olive': 8421376,
'purple': 8388736,
'red': 16711680,
'silver': 12632256,
'teal': 32896,
'white': 16777215,
'yellow': 16776960,
}
def rgb2hex(color=Tuple[int, int, int]) -> str:
if any(c not in range(256) for c in color):
raise ValueError('Color components must be in range(256)')
values = (f'{n % 256:02x}' for n in color)
return '#' + ''.join(values)
HEX_ERROR = "Color must use format '#0099ff', got: {!r}"
def hex2rgb(color=str) -> Tuple[int, int, int]:
if len(color) != 7 or color[0] != '#':
raise ValueError(HEX_ERROR.format(color))
try:
r, g, b = (int(color[i:i+2], 16) for i in range(1, 6, 2))
except ValueError as exc:
raise ValueError(HEX_ERROR.format(color)) from exc
return r, g, b
def name2hex(name: str, color_map: Mapping[str, int]) -> str:
try:
code = color_map[name]
except KeyError as exc:
raise KeyError(f'Color {name!r} not found.') from exc
return f'#{code:06x}'
def demo():
c = (255, 255, 0)
h = rgb2hex(c)
r = hex2rgb(h)
print(c, h, r)
c = (255, 165, 0)
h = rgb2hex(c)
r = hex2rgb(h)
print(c, h, r)
c = (512, 165, 0)
try:
h = rgb2hex(c)
except ValueError as exc:
print(c, repr(exc))
try:
r = hex2rgb('bla')
except ValueError as exc:
print(c, repr(exc))
try:
r = hex2rgb('#nonono')
except ValueError as exc:
print(c, repr(exc))
n = 'yellow'
print(n, name2hex(n, NAMES))
n = 'blue'
print(n, name2hex(n, NAMES))
from collections import OrderedDict
o = OrderedDict(black=0, white=0xffffff)
n = 'white'
print(n, name2hex(n, o))
if __name__ == '__main__':
demo()

View File

@ -0,0 +1,26 @@
# tag::COLUMNIZE[]
from typing import Sequence, List, Tuple
def columnize(sequence: Sequence[str], num_columns: int = 0) -> List[Tuple[str, ...]]:
if num_columns == 0:
num_columns = round(len(sequence) ** .5)
num_rows, reminder = divmod(len(sequence), num_columns)
num_rows += bool(reminder)
return [tuple(sequence[i::num_rows]) for i in range(num_rows)]
# end::COLUMNIZE[]
def demo() -> None:
nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India'
' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo'
' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu'
).split()
for row in columnize(nato, 4):
for word in row:
print(f'{word:15}', end='')
print()
if __name__ == '__main__':
demo()

View File

@ -0,0 +1,37 @@
from typing import Sequence, List, Tuple, TypeVar
T = TypeVar('T')
def columnize(sequence: Sequence[T], num_columns: int = 0) -> List[Tuple[T, ...]]:
if num_columns == 0:
num_columns = round(len(sequence) ** .5)
num_rows, reminder = divmod(len(sequence), num_columns)
num_rows += bool(reminder)
return [tuple(sequence[i::num_rows]) for i in range(num_rows)]
def demo() -> None:
nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India'
' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo'
' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu'
).split()
for line in columnize(nato):
for word in line:
print(f'{word:15}', end='')
print()
print()
for length in range(2, 21, 6):
values = list(range(1, length + 1))
for row in columnize(values):
for cell in row:
print(f'{cell:5}', end='')
print()
print()
if __name__ == '__main__':
demo()

View File

@ -0,0 +1,27 @@
from typing import Sequence, List, Tuple
Row = Tuple[str, ...]
def columnize(sequence: Sequence[str], num_columns: int) -> List[Row]:
if num_columns == 0:
num_columns = round(len(sequence) ** .5)
num_rows, reminder = divmod(len(sequence), num_columns)
num_rows += bool(reminder)
return [tuple(sequence[i::num_rows]) for i in range(num_rows)]
def demo() -> None:
nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India'
' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo'
' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu'
).split()
for row in columnize(nato, 4):
for word in row:
print(f'{word:15}', end='')
print()
if __name__ == '__main__':
demo()

View File

@ -0,0 +1,79 @@
from columnize import columnize
def test_columnize_8_in_2():
sequence = 'ABCDEFGH'
expected = [
('A', 'E'),
('B', 'F'),
('C', 'G'),
('D', 'H'),
]
result = columnize(sequence, 2)
assert expected == result
def test_columnize_8_in_4():
sequence = 'ABCDEFGH'
expected = [
('A', 'C', 'E', 'G'),
('B', 'D', 'F', 'H'),
]
result = columnize(sequence, 4)
assert expected == result
def test_columnize_7_in_2():
sequence = 'ABCDEFG'
expected = [
('A', 'E'),
('B', 'F'),
('C', 'G'),
('D',),
]
result = columnize(sequence, 2)
assert expected == result
def test_columnize_8_in_3():
sequence = 'ABCDEFGH'
expected = [
('A', 'D', 'G',),
('B', 'E', 'H',),
('C', 'F'),
]
result = columnize(sequence, 3)
assert expected == result
def test_columnize_8_in_5():
# Not the right number of columns, but the right number of rows.
# This acually looks better, so it's OK!
sequence = 'ABCDEFGH'
expected = [
('A', 'C', 'E', 'G'),
('B', 'D', 'F', 'H'),
]
result = columnize(sequence, 5)
assert expected == result
def test_columnize_7_in_5():
# Not the right number of columns, but the right number of rows.
# This acually looks better, so it's OK!
sequence = 'ABCDEFG'
expected = [
('A', 'C', 'E', 'G'),
('B', 'D', 'F'),
]
result = columnize(sequence, 5)
assert expected == result
def test_columnize_not_enough_items():
sequence = 'AB'
expected = [
('A', 'B'),
]
result = columnize(sequence, 3)
assert expected == result

View File

@ -0,0 +1,4 @@
from typing import Protocol, Any
class Comparable(Protocol): # <1>
def __lt__(self, other: Any) -> bool: ... # <2>

View File

@ -0,0 +1,60 @@
# tag::MYMAX_TYPES[]
from typing import Protocol, Any, TypeVar, overload, Callable, Iterable, Union
class _Comparable(Protocol):
def __lt__(self, other: Any) -> bool: ...
_T = TypeVar('_T')
_CT = TypeVar('_CT', bound=_Comparable)
_DT = TypeVar('_DT')
MISSING = object()
EMPTY_MSG = 'max() arg is an empty sequence'
@overload
def max(__arg1: _CT, __arg2: _CT, *_args: _CT, key: None = ...) -> _CT:
...
@overload
def max(__arg1: _T, __arg2: _T, *_args: _T, key: Callable[[_T], _CT]) -> _T:
...
@overload
def max(__iterable: Iterable[_CT], *, key: None = ...) -> _CT:
...
@overload
def max(__iterable: Iterable[_T], *, key: Callable[[_T], _CT]) -> _T:
...
@overload
def max(__iterable: Iterable[_CT], *, key: None = ...,
default: _DT) -> Union[_CT, _DT]:
...
@overload
def max(__iterable: Iterable[_T], *, key: Callable[[_T], _CT],
default: _DT) -> Union[_T, _DT]:
...
# end::MYMAX_TYPES[]
# tag::MYMAX[]
def max(first, *args, key=None, default=MISSING):
if args:
series = args
candidate = first
else:
series = iter(first)
try:
candidate = next(series)
except StopIteration:
if default is not MISSING:
return default
raise ValueError(EMPTY_MSG) from None
if key is None:
for current in series:
if candidate < current:
candidate = current
else:
candidate_key = key(candidate)
for current in series:
current_key = key(current)
if candidate_key < current_key:
candidate = current
candidate_key = current_key
return candidate
# end::MYMAX[]

View File

@ -0,0 +1,127 @@
from typing import TYPE_CHECKING, List, Optional
import mymax as my
def demo_args_list_float() -> None:
args = [2.5, 3.5, 1.5]
expected = 3.5
result = my.max(*args)
print(args, expected, result, sep='\n')
assert result == expected
if TYPE_CHECKING:
reveal_type(args)
reveal_type(expected)
reveal_type(result)
def demo_args_iter_int() -> None:
args = [30, 10, 20]
expected = 30
result = my.max(args)
print(args, expected, result, sep='\n')
assert result == expected
if TYPE_CHECKING:
reveal_type(args)
reveal_type(expected)
reveal_type(result)
def demo_args_iter_str() -> None:
args = iter('banana kiwi mango apple'.split())
expected = 'mango'
result = my.max(args)
print(args, expected, result, sep='\n')
assert result == expected
if TYPE_CHECKING:
reveal_type(args)
reveal_type(expected)
reveal_type(result)
def demo_args_iter_not_comparable_with_key() -> None:
args = [object(), object(), object()]
key = id
expected = max(args, key=id)
result = my.max(args, key=key)
print(args, key, expected, result, sep='\n')
assert result == expected
if TYPE_CHECKING:
reveal_type(args)
reveal_type(key)
reveal_type(expected)
reveal_type(result)
def demo_empty_iterable_with_default() -> None:
args: List[float] = []
default = None
expected = None
result = my.max(args, default=default)
print(args, default, expected, result, sep='\n')
assert result == expected
if TYPE_CHECKING:
reveal_type(args)
reveal_type(default)
reveal_type(expected)
reveal_type(result)
def demo_different_key_return_type() -> None:
args = iter('banana kiwi mango apple'.split())
key = len
expected = 'banana'
result = my.max(args, key=key)
print(args, key, expected, result, sep='\n')
assert result == expected
if TYPE_CHECKING:
reveal_type(args)
reveal_type(key)
reveal_type(expected)
reveal_type(result)
def demo_different_key_none() -> None:
args = iter('banana kiwi mango apple'.split())
key = None
expected = 'mango'
result = my.max(args, key=key)
print(args, key, expected, result, sep='\n')
assert result == expected
if TYPE_CHECKING:
reveal_type(args)
reveal_type(key)
reveal_type(expected)
reveal_type(result)
###################################### intentional type errors
def error_reported_bug() -> None:
# example from https://github.com/python/typeshed/issues/4051
top: Optional[int] = None
try:
my.max(5, top)
except TypeError as exc:
print(exc)
def error_args_iter_not_comparable() -> None:
try:
my.max([None, None])
except TypeError as exc:
print(exc)
def error_single_arg_not_iterable() -> None:
try:
my.max(1)
except TypeError as exc:
print(exc)
def main():
for name, val in globals().items():
if name.startswith('demo') or name.startswith('error'):
print('_' * 20, name)
val()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,69 @@
from typing import List, Callable, TypeVar
import pytest # type: ignore
import mymax as my
@pytest.fixture
def fruits():
return 'banana kiwi mango apple'.split()
@pytest.mark.parametrize('args, expected', [
([1, 3], 3),
([3, 1], 3),
([30, 10, 20], 30),
])
def test_max_args(args, expected):
result = my.max(*args)
assert result == expected
@pytest.mark.parametrize('iterable, expected', [
([7], 7),
([1, 3], 3),
([3, 1], 3),
([30, 10, 20], 30),
])
def test_max_iterable(iterable, expected):
result = my.max(iterable)
assert result == expected
def test_max_single_arg_not_iterable():
msg = "'int' object is not iterable"
with pytest.raises(TypeError) as exc:
my.max(1)
assert exc.value.args[0] == msg
def test_max_empty_iterable_no_default():
with pytest.raises(ValueError) as exc:
my.max([])
assert exc.value.args[0] == my.EMPTY_MSG
@pytest.mark.parametrize('iterable, default, expected', [
([7], -1, 7),
([], -1, -1),
([], None, None),
])
def test_max_empty_iterable_with_default(iterable, default, expected):
result = my.max(iterable, default=default)
assert result == expected
@pytest.mark.parametrize('key, expected', [
(None, 'mango'),
(lambda x: x, 'mango'),
(len, 'banana'),
(lambda s: -len(s), 'kiwi'),
(lambda s: -ord(s[0]), 'apple'),
(lambda s: ord(s[-1]), 'mango'),
])
def test_max_iterable_with_key(
fruits: List[str],
key: Callable[[str], str],
expected: str
) -> None:
result = my.max(fruits, key=key)
assert result == expected

View File

@ -0,0 +1,31 @@
"""
``top(it, n)`` returns the "greatest" ``n`` elements of the iterable ``t``.
Example:
# tag::TOP_DOCTEST[]
>>> top([4, 1, 5, 2, 6, 7, 3], 3)
[7, 6, 5]
>>> l = 'mango pear apple kiwi banana'.split()
>>> top(l, 3)
['pear', 'mango', 'kiwi']
>>>
>>> l2 = [(len(s), s) for s in l]
>>> l2
[(5, 'mango'), (4, 'pear'), (5, 'apple'), (4, 'kiwi'), (6, 'banana')]
>>> top(l2, 3)
[(6, 'banana'), (5, 'mango'), (5, 'apple')]
# end::TOP_DOCTEST[]
"""
# tag::TOP[]
from typing import TypeVar, Iterable, List
from comparable import Comparable
CT = TypeVar('CT', bound=Comparable)
def top(series: Iterable[CT], length: int) -> List[CT]:
ordered = sorted(series, reverse=True)
return ordered[:length]
# end::TOP[]

View File

@ -0,0 +1,39 @@
from typing import Tuple, List, Iterator, TYPE_CHECKING
import pytest # type: ignore
from top import top
@pytest.mark.parametrize('series, length, expected', [
((1, 2, 3), 2, [3, 2]),
((1, 2, 3), 3, [3, 2, 1]),
((3, 3, 3), 1, [3]),
])
def test_top(
series: Tuple[float, ...],
length: int,
expected: List[float],
) -> None:
result = top(series, length)
assert expected == result
# tag::TOP_TEST[]
def test_top_tuples() -> None:
fruit = 'mango pear apple kiwi banana'.split()
series: Iterator[Tuple[int, str]] = (
(len(s), s) for s in fruit)
length = 3
expected = [(6, 'banana'), (5, 'mango'), (5, 'apple')]
result = top(series, length)
if TYPE_CHECKING:
reveal_type(series)
reveal_type(expected)
reveal_type(result)
assert result == expected
def test_top_objects_error() -> None:
series = [object() for _ in range(4)]
if TYPE_CHECKING:
reveal_type(series)
with pytest.raises(TypeError) as exc:
top(series, 3)
assert "'<' not supported" in str(exc)
# end::TOP_TEST[]

View File

@ -0,0 +1,19 @@
# This example uses the geolib library:
# https://pypi.org/project/geolib/
"""
>>> shanghai = 31.2304, 121.4737
>>> geohash(shanghai)
'wtw3sjq6q'
"""
# tag::GEOHASH[]
from typing import Tuple
from geolib import geohash as gh # type: ignore
PRECISION = 9
def geohash(lat_lon = Tuple[float, float]) -> str:
return gh.encode(*lat_lon, PRECISION)
# end::GEOHASH[]

View File

@ -0,0 +1,36 @@
# This example requires the geolib library:
# https://pypi.org/project/geolib/
"""
>>> shanghai = 31.2304, 121.4737
>>> geohash(shanghai)
'wtw3sjq6q'
"""
# tag::GEOHASH[]
from typing import Tuple, NamedTuple
from geolib import geohash as gh # type: ignore
PRECISION = 9
class Coordinate(NamedTuple):
lat: float
lon: float
def geohash(lat_lon: Coordinate) -> str:
return gh.encode(*lat_lon, PRECISION)
def display(lat_lon: Tuple[float, float]) -> str:
lat, lon = lat_lon
ns = 'N' if lat >= 0 else 'S'
ew = 'E' if lon >= 0 else 'W'
return f'{abs(lat):0.1f}°{ns}, {abs(lon):0.1f}°{ew}'
# end::GEOHASH[]
def demo():
shanghai = 31.2304, 121.4737
s = geohash(shanghai)
print(s)

View File

@ -0,0 +1,12 @@
from coordinates_named import geohash, Coordinate, display
def test_geohash_max_precision() -> None:
sao_paulo = -23.5505, -46.6339
result = geohash(Coordinate(*sao_paulo))
assert '6gyf4bf0r' == result
def test_display() -> None:
sao_paulo = -23.5505, -46.6339
assert display(sao_paulo) == '23.6°S, 46.6°W'
shanghai = 31.2304, 121.4737
assert display(shanghai) == '31.2°N, 121.5°E'

View File

@ -0,0 +1,6 @@
from coordinates import geohash
def test_geohash_max_precision() -> None:
sao_paulo = -23.5505, -46.6339
result = geohash(sao_paulo)
assert '6gyf4bf0r' == result

View File

@ -0,0 +1,2 @@
geolib==1.0.7
future==0.18.2

View File

@ -0,0 +1,5 @@
import time
from typing import Optional
def ctime(secs: Optional[float] = None, /) -> str:
return time.ctime(secs)

View File

@ -0,0 +1,2 @@
def double(n: object) -> object:
return n * 2

View File

@ -0,0 +1,11 @@
from typing import TypeVar, Protocol
T = TypeVar('T') # <1>
class Repeatable(Protocol):
def __mul__(self: T, other: int) -> T: ... # <2>
RT = TypeVar('RT', bound=Repeatable) # <3>
def double(n: RT) -> RT: # <4>
return n * 2

View File

@ -0,0 +1,6 @@
from collections import abc
from typing import Any
def double(n: abc.Sequence) -> Any:
return n * 2

View File

@ -0,0 +1,56 @@
from typing import TYPE_CHECKING
import pytest
from double_protocol import double
def test_double_int() -> None:
given = 2
result = double(given)
assert result == given * 2
if TYPE_CHECKING:
reveal_type(given)
reveal_type(result)
def test_double_str() -> None:
given = 'A'
result = double(given)
assert result == given * 2
if TYPE_CHECKING:
reveal_type(given)
reveal_type(result)
def test_double_fraction() -> None:
from fractions import Fraction
given = Fraction(2, 5)
result = double(given)
assert result == given * 2
if TYPE_CHECKING:
reveal_type(given)
reveal_type(result)
def test_double_array() -> None:
from array import array
given = array('d', [1.0, 2.0, 3.14])
result = double(given)
if TYPE_CHECKING:
reveal_type(given)
reveal_type(result)
def test_double_nparray() -> None:
import numpy as np # type: ignore
given = np.array([[1, 2], [3, 4]])
result = double(given)
comparison = result == given * 2
assert comparison.all()
if TYPE_CHECKING:
reveal_type(given)
reveal_type(result)
def test_double_none() -> None:
given = None
with pytest.raises(TypeError):
result = double(given)

10
08-def-type-hints/list.py Normal file
View File

@ -0,0 +1,10 @@
from typing import List, Tuple
def tokenize(text: str) -> List[str]:
return text.upper().split()
l: List[str] = []
l.append(1)
print(l)
t: Tuple[str, float] = ('São Paulo', 12_176_866)

View File

@ -0,0 +1,20 @@
"""
# tag::SHOW_COUNT_DOCTEST[]
>>> show_count(99, 'bird')
'99 birds'
>>> show_count(1, 'bird')
'1 bird'
>>> show_count(0, 'bird')
'no bird'
# end::SHOW_COUNT_DOCTEST[]
"""
# tag::SHOW_COUNT[]
def show_count(count: int, word: str) -> str:
if count == 0:
return f'no {word}'
elif count == 1:
return f'{count} {word}'
return f'{count} {word}s'
# end::SHOW_COUNT[]

View File

@ -0,0 +1,17 @@
from pytest import mark
from messages import show_count
@mark.parametrize('qty, expected', [
(1, '1 part'),
(2, '2 parts'),
])
def test_show_count(qty, expected):
got = show_count(qty, 'part')
assert got == expected
def test_show_count_zero():
got = show_count(0, 'part')
assert got == 'no part'

View File

@ -0,0 +1,6 @@
[mypy]
python_version = 3.8
warn_unused_configs = True
disallow_incomplete_defs = True
[mypy-pytest]
ignore_missing_imports = True

View File

@ -0,0 +1,24 @@
"""
>>> show_count(99, 'bird')
'99 birds'
>>> show_count(1, 'bird')
'1 bird'
>>> show_count(0, 'bird')
'no bird'
>>> show_count(3, 'virus', 'viruses')
'3 viruses'
"""
# tag::SHOW_COUNT[]
def show_count(count: int, singular: str, plural: str = '') -> str:
if count == 0:
return f'no {singular}'
elif count == 1:
return f'1 {singular}'
else:
if plural:
return f'{count} {plural}'
else:
return f'{count} {singular}s'
# end::SHOW_COUNT[]

View File

@ -0,0 +1,25 @@
from pytest import mark # type: ignore
from messages import show_count
@mark.parametrize('qty, expected', [
(1, '1 part'),
(2, '2 parts'),
(0, 'no part'),
])
def test_show_count(qty, expected):
got = show_count(qty, 'part')
assert got == expected
# tag::TEST_IRREGULAR[]
@mark.parametrize('qty, expected', [
(1, '1 child'),
(2, '2 children'),
(0, 'no child'),
])
def test_irregular(qty, expected) -> None:
got = show_count(qty, 'child', 'children')
assert got == expected
# end::TEST_IRREGULAR[]

View File

@ -0,0 +1,20 @@
"""
# tag::SHOW_COUNT_DOCTEST[]
>>> show_count(99, 'bird')
'99 birds'
>>> show_count(1, 'bird')
'1 bird'
>>> show_count(0, 'bird')
'no bird'
# end::SHOW_COUNT_DOCTEST[]
"""
# tag::SHOW_COUNT[]
def show_count(count, word):
if count == 0:
return f'no {word}'
elif count == 1:
return f'{count} {word}'
return f'{count} {word}s'
# end::SHOW_COUNT[]

View File

@ -0,0 +1,15 @@
from pytest import mark
from messages import show_count
@mark.parametrize('qty, expected', [
(1, '1 part'),
(2, '2 parts'),
])
def test_show_count(qty, expected):
got = show_count(qty, 'part')
assert got == expected
def test_show_count_zero():
got = show_count(0, 'part')
assert got == 'no part'

View File

@ -0,0 +1,25 @@
from collections import Counter
from typing import Iterable, TypeVar
T = TypeVar('T')
def mode(data: Iterable[T]) -> T:
data = iter(data)
pairs = Counter(data).most_common(1)
if len(pairs) == 0:
raise ValueError('no mode for empty data')
return pairs[0][0]
def demo() -> None:
from typing import List, Set, TYPE_CHECKING
pop:List[Set] = [set(), set()]
m = mode(pop)
if TYPE_CHECKING:
reveal_type(pop)
reveal_type(m)
print(pop)
print(repr(m), type(m))
if __name__ == '__main__':
demo()

View File

@ -0,0 +1,23 @@
# tag::MODE_FLOAT[]
from collections import Counter
from typing import Iterable
def mode(data: Iterable[float]) -> float:
pairs = Counter(data).most_common(1)
if len(pairs) == 0:
raise ValueError('no mode for empty data')
return pairs[0][0]
# end::MODE_FLOAT[]
def demo() -> None:
import typing
pop = [1, 1, 2, 3, 3, 3, 3, 4]
m = mode(pop)
if typing.TYPE_CHECKING:
reveal_type(pop)
reveal_type(m)
print(pop)
print(repr(m), type(m))
if __name__ == '__main__':
demo()

View File

@ -0,0 +1,28 @@
# tag::MODE_HASHABLE_T[]
from collections import Counter
from typing import Iterable, Hashable, TypeVar
HashableT = TypeVar('HashableT', bound=Hashable)
def mode(data: Iterable[HashableT]) -> HashableT:
pairs = Counter(data).most_common(1)
if len(pairs) == 0:
raise ValueError('no mode for empty data')
return pairs[0][0]
# end::MODE_HASHABLE_T[]
def demo() -> None:
import typing
pop = 'abracadabra'
m = mode(pop)
if typing.TYPE_CHECKING:
reveal_type(pop)
reveal_type(m)
print(pop)
print(m.upper(), type(m))
if __name__ == '__main__':
demo()

View File

@ -0,0 +1,24 @@
# tag::MODE_FLOAT[]
from collections import Counter
from typing import Iterable, Hashable
def mode(data: Iterable[Hashable]) -> Hashable:
data = iter(data)
pairs = Counter(data).most_common(1)
if len(pairs) == 0:
raise ValueError('no mode for empty data')
return pairs[0][0]
# end::MODE_FLOAT[]
def demo() -> None:
import typing
pop = 'abracadabra'
m = mode(pop)
if typing.TYPE_CHECKING:
reveal_type(pop)
reveal_type(m)
print(pop)
print(m.upper(), type(m))
if __name__ == '__main__':
demo()

View File

@ -0,0 +1,26 @@
from collections import Counter
from typing import Iterable, TypeVar
from decimal import Decimal
from fractions import Fraction
NumberT = TypeVar('NumberT', float, Decimal, Fraction)
def mode(data: Iterable[NumberT]) -> NumberT:
pairs = Counter(data).most_common(1)
if len(pairs) == 0:
raise ValueError('no mode for empty data')
return pairs[0][0]
def demo() -> None:
from typing import List, Set, TYPE_CHECKING
pop = [Fraction(1, 2), Fraction(1, 3), Fraction(1, 4), Fraction(1, 2)]
m = mode(pop)
if TYPE_CHECKING:
reveal_type(pop)
reveal_type(m)
print(pop)
print(repr(m), type(m))
if __name__ == '__main__':
demo()

View File

@ -0,0 +1,13 @@
from functools import reduce # <1>
from operator import add
from typing import overload, Iterable, Union, TypeVar
T = TypeVar('T')
S = TypeVar('S') # <2>
@overload
def sum(it: Iterable[T]) -> Union[T, int]: ... # <3>
@overload
def sum(it: Iterable[T], /, start: S) -> Union[T, S]: ... # <4>
def sum(it, /, start=0): # <5>
return reduce(add, it, start)

101
08-def-type-hints/passdrill.py Executable file
View File

@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""passdrill: typing drills for practicing passphrases
"""
import sys
import os
from getpass import getpass
from hashlib import scrypt
from base64 import b64encode, b64decode
from typing import Sequence, Tuple
HASH_FILENAME = 'passdrill.hash'
HELP = 'Use -s to save passphrase hash for practice.'
def prompt() -> str:
print('WARNING: the passphrase WILL BE SHOWN so that you can check it!')
confirmed = ''
while confirmed != 'y':
passphrase = input('Type passphrase to hash (it will be echoed): ')
if passphrase == '' or passphrase == 'q':
print('ERROR: the passphrase cannot be empty or "q".')
continue
print(f'Passphrase to be hashed -> {passphrase}')
confirmed = input('Confirm (y/n): ').lower()
return passphrase
def crypto_hash(salt: bytes, passphrase: str) -> bytes:
octets = passphrase.encode('utf-8')
# Recommended parameters for interactive logins as of 2017:
# N=32768, r=8 and p=1 (https://godoc.org/golang.org/x/crypto/scrypt)
return scrypt(octets, salt=salt, n=32768, r=8, p=1, maxmem=2 ** 26)
def build_hash(passphrase: str) -> bytes:
salt = os.urandom(32)
payload = crypto_hash(salt, passphrase)
return b64encode(salt) + b':' + b64encode(payload)
def save_hash() -> None:
salted_hash = build_hash(prompt())
with open(HASH_FILENAME, 'wb') as fp:
fp.write(salted_hash)
print(f'Passphrase hash saved to', HASH_FILENAME)
def load_hash() -> Tuple[bytes, bytes]:
try:
with open(HASH_FILENAME, 'rb') as fp:
salted_hash = fp.read()
except FileNotFoundError:
print('ERROR: passphrase hash file not found.', HELP)
sys.exit(2)
salt, stored_hash = salted_hash.split(b':')
return (b64decode(salt), b64decode(stored_hash))
def practice() -> None:
salt, stored_hash = load_hash()
print('Type q to end practice.')
turn = 0
correct = 0
while True:
turn += 1
response = getpass(f'{turn}:')
if response == '':
print('Type q to quit.')
turn -= 1 # don't count this response
continue
elif response == 'q':
turn -= 1 # don't count this response
break
if crypto_hash(salt, response) == stored_hash:
correct += 1
answer = 'OK'
else:
answer = 'wrong'
print(f' {answer}\thits={correct}\tmisses={turn-correct}')
if turn:
pct = correct / turn * 100
print(f'\n{turn} turns. {pct:0.1f}% correct.')
def main(argv: Sequence[str]) -> None:
if len(argv) < 2:
practice()
elif len(argv) == 2 and argv[1] == '-s':
save_hash()
else:
print('ERROR: invalid argument.', HELP)
sys.exit(1)
if __name__ == '__main__':
main(sys.argv)

View File

@ -0,0 +1,36 @@
"""
``zip_replace`` replaces multiple calls to ``str.replace``::
>>> changes = [
... ('(', ' ( '),
... (')', ' ) '),
... (' ', ' '),
... ]
>>> expr = '(+ 2 (* 3 7))'
>>> zip_replace(expr, changes)
' ( + 2 ( * 3 7 ) ) '
"""
# tag::ZIP_REPLACE[]
from typing import Iterable, Tuple
FromTo = Tuple[str, str] # <1>
def zip_replace(text: str, changes: Iterable[FromTo]) -> str: # <2>
for from_, to in changes:
text = text.replace(from_, to)
return text
# end::ZIP_REPLACE[]
def demo() -> None:
import doctest
failed, count = doctest.testmod()
print(f'{count-failed} of {count} doctests OK')
l33t = [(p[0], p[1]) for p in 'a4 e3 i1 o0'.split()]
text = 'mad skilled noob powned leet'
print(zip_replace(text, l33t))
if __name__ == '__main__':
demo()

View File

@ -0,0 +1,39 @@
"""
``zip_replace`` replaces multiple calls to ``str.replace``::
>>> changes = [
... ('(', ' ( '),
... (')', ' ) '),
... (' ', ' '),
... ]
>>> expr = '(+ 2 (* 3 7))'
>>> zip_replace(expr, changes)
' ( + 2 ( * 3 7 ) ) '
"""
from typing import Iterable, NamedTuple
class FromTo(NamedTuple):
from_: str
to: str
def zip_replace(text: str, changes: Iterable[FromTo], count:int = -1) -> str:
for from_, to in changes:
text = text.replace(from_, to, count)
return text
def demo() -> None:
import doctest
failed, count = doctest.testmod()
print(f'{count-failed} of {count} doctests OK')
l33t = [FromTo(*p) for p in 'a4 e3 i1 o0'.split()]
text = 'mad skilled noob powned leet'
print(zip_replace(text, l33t))
if __name__ == '__main__':
demo()

View File

@ -0,0 +1,8 @@
from array import array
from typing import MutableSequence
a = array('d')
reveal_type(a)
b: MutableSequence[float] = array('b')
reveal_type(b)

View File

@ -0,0 +1,16 @@
values_map = [
(1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1),
( 'M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV','I')
]
def to_roman(arabic: int) -> str:
""" Convert an integer to a Roman numeral. """
if not 0 < arabic < 4000:
raise ValueError('Argument must be between 1 and 3999')
result = []
for value, numeral in zip(*values_map):
repeat = arabic // value
result.append(numeral * repeat)
arabic -= value * repeat
return ''.join(result)

View File

@ -0,0 +1,18 @@
import pytest
from romans import to_roman
def test_to_roman_1():
assert to_roman(1) == 'I'
@pytest.mark.parametrize('arabic, roman', [
(3, 'III'),
(4, 'IV'),
(1009, 'MIX'),
(1969, 'MCMLXIX'),
(3999, 'MMMCMXCIX')
])
def test_to_roman(arabic, roman):
assert to_roman(arabic) == roman

View File

@ -0,0 +1,34 @@
# tag::SAMPLE[]
from random import shuffle
from typing import Sequence, List, TypeVar
T = TypeVar('T')
def sample(population: Sequence[T], size: int) -> List[T]:
if size < 1:
raise ValueError('size must be >= 1')
result = list(population)
shuffle(result)
return result[:size]
# end::SAMPLE[]
def demo() -> None:
import typing
p1 = tuple(range(10))
s1 = sample(p1, 3)
if typing.TYPE_CHECKING:
reveal_type(p1)
reveal_type(s1)
print(p1)
print(s1)
p2 = 'The quick brown fox jumps over the lazy dog'
s2 = sample(p2, 10)
if typing.TYPE_CHECKING:
reveal_type(p2)
reveal_type(s2)
print(p2)
print(s2)
if __name__ == '__main__':
demo()

View File

@ -0,0 +1,32 @@
# tag::BOOKDICT[]
from typing import TypedDict, List
import json
class BookDict(TypedDict):
isbn: str
title: str
authors: List[str]
pagecount: int
# end::BOOKDICT[]
# tag::TOXML[]
AUTHOR_EL = '<AUTHOR>{}</AUTHOR>'
def to_xml(book: BookDict) -> str: # <1>
elements: List[str] = [] # <2>
for key, value in book.items():
if isinstance(value, list): # <3>
elements.extend(
AUTHOR_EL.format(n) for n in value) # <4>
else:
tag = key.upper()
elements.append(f'<{tag}>{value}</{tag}>')
xml = '\n\t'.join(elements)
return f'<BOOK>\n\t{xml}\n</BOOK>'
# end::TOXML[]
# tag::FROMJSON[]
def from_json(data: str) -> BookDict:
whatever: BookDict = json.loads(data) # <1>
return whatever # <2>
# end::FROMJSON[]

View File

@ -0,0 +1,32 @@
# tag::BOOKDICT[]
from typing import TypedDict, List
import json
class BookDict(TypedDict):
isbn: str
title: str
authors: List[str]
pagecount: int
# end::BOOKDICT[]
# tag::TOXML[]
AUTHOR_EL = '<AUTHOR>{}</AUTHOR>'
def to_xml(book: BookDict) -> str: # <1>
elements: List[str] = [] # <2>
for key, value in book.items():
if isinstance(value, list): # <3>
elements.extend(AUTHOR_EL.format(n)
for n in value)
else:
tag = key.upper()
elements.append(f'<{tag}>{value}</{tag}>')
xml = '\n\t'.join(elements)
return f'<BOOK>\n\t{xml}\n</BOOK>'
# end::TOXML[]
# tag::FROMJSON[]
def from_json(data: str) -> BookDict:
whatever = json.loads(data) # <1>
return whatever # <2>
# end::FROMJSON[]

View File

@ -0,0 +1,20 @@
from books import BookDict
from typing import TYPE_CHECKING
def demo() -> None: # <1>
book = BookDict( # <2>
isbn='0134757599',
title='Refactoring, 2e',
authors=['Martin Fowler', 'Kent Beck'],
pagecount=478
)
authors = book['authors'] # <3>
if TYPE_CHECKING: # <4>
reveal_type(authors) # <5>
authors = 'Bob' # <6>
book['weight'] = 4.2
del book['title']
if __name__ == '__main__':
demo()

View File

@ -0,0 +1,23 @@
from books import to_xml, from_json
from typing import TYPE_CHECKING
def demo() -> None:
NOT_BOOK_JSON = """
{"title": "Andromeda Strain",
"flavor": "pistachio",
"authors": true}
"""
not_book = from_json(NOT_BOOK_JSON) # <1>
if TYPE_CHECKING: # <2>
reveal_type(not_book)
reveal_type(not_book['authors'])
print(not_book) # <3>
print(not_book['flavor']) # <4>
xml = to_xml(not_book) # <5>
print(xml) # <6>
if __name__ == '__main__':
demo()

View File

@ -0,0 +1,112 @@
import json
from typing import cast
from books import BookDict, to_xml, from_json
XML_SAMPLE = """
<BOOK>
\t<ISBN>0134757599</ISBN>
\t<TITLE>Refactoring, 2e</TITLE>
\t<AUTHOR>Martin Fowler</AUTHOR>
\t<AUTHOR>Kent Beck</AUTHOR>
\t<PAGECOUNT>478</PAGECOUNT>
</BOOK>
""".strip()
# using plain dicts
def test_1() -> None:
xml = to_xml({
'isbn': '0134757599',
'title': 'Refactoring, 2e',
'authors': ['Martin Fowler', 'Kent Beck'],
'pagecount': 478,
})
assert xml == XML_SAMPLE
def test_2() -> None:
xml = to_xml(dict(
isbn='0134757599',
title='Refactoring, 2e',
authors=['Martin Fowler', 'Kent Beck'],
pagecount=478))
assert xml == XML_SAMPLE
def test_5() -> None:
book_data: BookDict = dict(
isbn='0134757599',
title='Refactoring, 2e',
authors=['Martin Fowler', 'Kent Beck'],
pagecount=478
)
xml = to_xml(book_data)
assert xml == XML_SAMPLE
def test_6() -> None:
book_data = dict(
isbn='0134757599',
title='Refactoring, 2e',
authors=['Martin Fowler', 'Kent Beck'],
pagecount=478
)
xml = to_xml(cast(BookDict, book_data)) # cast needed
assert xml == XML_SAMPLE
def test_4() -> None:
xml = to_xml(BookDict(
isbn='0134757599',
title='Refactoring, 2e',
authors=['Martin Fowler', 'Kent Beck'],
pagecount=478))
assert xml == XML_SAMPLE
def test_7() -> None:
book_data = BookDict(
isbn='0134757599',
title='Refactoring, 2e',
authors=['Martin Fowler', 'Kent Beck'],
pagecount=478
)
xml = to_xml(book_data)
assert xml == XML_SAMPLE
def test_8() -> None:
book_data: BookDict = {
'isbn': '0134757599',
'title': 'Refactoring, 2e',
'authors': ['Martin Fowler', 'Kent Beck'],
'pagecount': 478,
}
xml = to_xml(book_data)
assert xml == XML_SAMPLE
BOOK_JSON = """
{"isbn": "0134757599",
"title": "Refactoring, 2e",
"authors": ["Martin Fowler", "Kent Beck"],
"pagecount": 478}
"""
def test_load_book_0() -> None:
book_data: BookDict = json.loads(BOOK_JSON) # typed var
xml = to_xml(book_data)
assert xml == XML_SAMPLE
def test_load_book() -> None:
book_data = from_json(BOOK_JSON)
xml = to_xml(book_data)
assert xml == XML_SAMPLE
NOT_BOOK_JSON = """
{"isbn": 3.141592653589793
"title": [1, 2, 3],
"authors": ["Martin Fowler", "Kent Beck"],
"flavor": "strawberry"}
"""
def test_load_not_book() -> None:
book_data: BookDict = json.loads(BOOK_JSON) # typed var
xml = to_xml(book_data)
assert xml == XML_SAMPLE

View File

@ -0,0 +1,23 @@
import json
from typing import cast
from books import BookDict, to_xml
XML_SAMPLE = """
<BOOK>
\t<ISBN>0134757599</ISBN>
\t<TITLE>Refactoring, 2e</TITLE>
\t<AUTHOR>Martin Fowler</AUTHOR>
\t<AUTHOR>Kent Beck</AUTHOR>
\t<PAGECOUNT>478</PAGECOUNT>
</BOOK>
""".strip()
def test_3() -> None:
xml = to_xml(BookDict(dict([ # Expected keyword arguments, {...}, or dict(...) in TypedDict constructor
('isbn', '0134757599'),
('title', 'Refactoring, 2e'),
('authors', ['Martin Fowler', 'Kent Beck']),
('pagecount', 478),
])))
assert xml == XML_SAMPLE

View File

@ -0,0 +1,4 @@
Sample code for Chapter 8 - "Closures and decorators"
From the book "Fluent Python, Second Edition" by Luciano Ramalho (O'Reilly, 2020)
http://shop.oreilly.com/product/0636920273196.do

View File

@ -0,0 +1,33 @@
"""
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__ # doctest: +ELLIPSIS
(<cell at 0x...: list object at 0x...>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]
"""
DEMO = """
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
"""
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager

View File

@ -0,0 +1,21 @@
"""
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
"""
class Averager():
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total/len(self.series)

View File

@ -0,0 +1,21 @@
import time
import functools
def clock(func):
@functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_lst = []
if args:
arg_lst.append(', '.join(repr(arg) for arg in args))
if kwargs:
pairs = [f'{k}={v!r}' for k, v in kwargs.items()]
arg_lst.append(', '.join(pairs))
arg_str = ', '.join(arg_lst)
print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
return result
return clocked

View File

@ -0,0 +1,13 @@
import time
def clock(func):
def clocked(*args): # <1>
t0 = time.perf_counter()
result = func(*args) # <2>
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
return result
return clocked # <3>

View File

@ -0,0 +1,44 @@
# clockdeco_class.py
"""
>>> snooze(.1) # doctest: +ELLIPSIS
[0.101...s] snooze(0.1) -> None
>>> clock('{name}: {elapsed}')(time.sleep)(.2) # doctest: +ELLIPSIS
sleep: 0.20...
>>> clock('{name}({args}) dt={elapsed:0.3f}s')(time.sleep)(.2)
sleep(0.2) dt=0.201s
"""
# tag::CLOCKDECO_CLS[]
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
class clock: # <1>
def __init__(self, fmt=DEFAULT_FMT): # <2>
self.fmt = fmt
def __call__(self, func): # <3>
def clocked(*_args):
t0 = time.perf_counter()
_result = func(*_args) # <4>
elapsed = time.perf_counter() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args)
result = repr(_result)
print(self.fmt.format(**locals()))
return _result
return clocked
# end::CLOCKDECO_CLS[]
if __name__ == '__main__':
@clock()
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)

View File

@ -0,0 +1,19 @@
import time
from clockdeco import clock
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
if __name__ == '__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))

View File

@ -0,0 +1,40 @@
# clockdeco_param.py
"""
>>> snooze(.1) # doctest: +ELLIPSIS
[0.101...s] snooze(0.1) -> None
>>> clock('{name}: {elapsed}')(time.sleep)(.2) # doctest: +ELLIPSIS
sleep: 0.20...
>>> clock('{name}({args}) dt={elapsed:0.3f}s')(time.sleep)(.2)
sleep(0.2) dt=0.201s
"""
# tag::CLOCKDECO_PARAM[]
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FMT): # <1>
def decorate(func): # <2>
def clocked(*_args): # <3>
t0 = time.perf_counter()
_result = func(*_args) # <4>
elapsed = time.perf_counter() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args) # <5>
result = repr(_result) # <6>
print(fmt.format(**locals())) # <7>
return _result # <8>
return clocked # <9>
return decorate # <10>
if __name__ == '__main__':
@clock() # <11>
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
# end::CLOCKDECO_PARAM[]

View File

@ -0,0 +1,9 @@
import time
from clockdeco_param import clock
@clock('{name}: {elapsed}s')
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)

View File

@ -0,0 +1,9 @@
import time
from clockdeco_param import clock
@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)

View File

@ -0,0 +1,17 @@
from clockdeco import clock
import fibo_demo
import fibo_demo_lru
@clock
def demo1():
fibo_demo.fibonacci(30)
@clock
def demo2():
fibo_demo_lru.fibonacci(30)
demo1()
demo2()

View File

@ -0,0 +1,12 @@
from clockdeco import clock
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
if __name__ == '__main__':
print(fibonacci(6))

View File

@ -0,0 +1,15 @@
import functools
from clockdeco import clock
@functools.lru_cache # <1>
@clock # <2>
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
if __name__ == '__main__':
print(fibonacci(6))

View File

@ -0,0 +1,124 @@
>>> def f1(a):
... print(a)
... print(b)
...
>>> f1(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f1
NameError: name 'b' is not defined
>>> b = 6
>>> f1(3)
3
6
>>> def f2(a):
... print(a)
... print(b)
... b = 9
...
>>> f2(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment
# tag::F1_DIS[]
>>> from dis import dis
>>> dis(f1)
2 0 LOAD_GLOBAL 0 (print) <1>
3 LOAD_FAST 0 (a) <2>
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP
3 10 LOAD_GLOBAL 0 (print)
13 LOAD_GLOBAL 1 (b) <3>
16 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
19 POP_TOP
20 LOAD_CONST 0 (None)
23 RETURN_VALUE
# end::F1_DIS[]
# tag::F2_DIS[]
>>> dis(f2)
2 0 LOAD_GLOBAL 0 (print)
3 LOAD_FAST 0 (a)
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP
3 10 LOAD_GLOBAL 0 (print)
13 LOAD_FAST 1 (b) <1>
16 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
19 POP_TOP
4 20 LOAD_CONST 1 (9)
23 STORE_FAST 1 (b)
26 LOAD_CONST 0 (None)
29 RETURN_VALUE
# end::F2_DIS[]
>>> def f3(a):
... global b
... print(a)
... print(b)
... b = 9
...
>>> f3(3)
3
6
>>> b
9
# tag::F3_DIS[]
>>> dis(f3)
3 0 LOAD_GLOBAL 0 (print)
3 LOAD_FAST 0 (a)
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP
4 10 LOAD_GLOBAL 0 (print)
13 LOAD_GLOBAL 1 (b)
16 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
19 POP_TOP
5 20 LOAD_CONST 1 (9)
23 STORE_GLOBAL 1 (b)
26 LOAD_CONST 0 (None)
29 RETURN_VALUE
# end::F3_DIS[]
>>> def f4(b):
... def f5(a):
... nonlocal b
... print(a)
... print(b)
... b = 7
... return f5
...
>>> f5 = f4(8)
>>> f5(2)
2
8
>>> b
9
>>> f5(3)
3
7????
>>> dis(f5)
4 0 LOAD_GLOBAL 0 (print)
3 LOAD_FAST 0 (a)
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP
5 10 LOAD_GLOBAL 0 (print)
13 LOAD_DEREF 0 (b)
16 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
19 POP_TOP
6 20 LOAD_CONST 1 (7)
23 STORE_DEREF 0 (b)
26 LOAD_CONST 0 (None)
29 RETURN_VALUE

View File

@ -0,0 +1,76 @@
r"""
htmlize(): generic function example
# tag::HTMLIZE_DEMO[]
>>> htmlize({1, 2, 3}) # <1>
'<pre>{1, 2, 3}</pre>'
>>> htmlize(abs)
'<pre>&lt;built-in function abs&gt;</pre>'
>>> htmlize('Heimlich & Co.\n- a game') # <2>
'<p>Heimlich &amp; Co.<br>\n- a game</p>'
>>> htmlize(42) # <3>
'<pre>42 (0x2a)</pre>'
>>> print(htmlize(['alpha', 66, {3, 2, 1}])) # <4>
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>
>>> htmlize(True) # <5>
'<pre>True</pre>'
>>> htmlize(fractions.Fraction(2, 3)) # <6>
'<pre>2/3</pre>'
>>> htmlize(2/3) # <7>
'<pre>0.6666666666666666 (2/3)</pre>'
>>> htmlize(decimal.Decimal('0.02380952'))
'<pre>0.02380952 (1/42)</pre>'
# end::HTMLIZE_DEMO[]
"""
# tag::HTMLIZE[]
from functools import singledispatch
from collections import abc
import fractions
import decimal
import html
import numbers
@singledispatch # <1>
def htmlize(obj: object) -> str:
content = html.escape(repr(obj))
return f'<pre>{content}</pre>'
@htmlize.register # <2>
def _(text: str) -> str: # <3>
content = html.escape(text).replace('\n', '<br>\n')
return f'<p>{content}</p>'
@htmlize.register # <4>
def _(seq: abc.Sequence) -> str:
inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n</ul>'
@htmlize.register # <5>
def _(n: numbers.Integral) -> str:
return f'<pre>{n} (0x{n:x})</pre>'
@htmlize.register # <6>
def _(n: bool) -> str:
return f'<pre>{n}</pre>'
@htmlize.register(fractions.Fraction) # <7>
def _(x) -> str:
frac = fractions.Fraction(x)
return f'<pre>{frac.numerator}/{frac.denominator}</pre>'
@htmlize.register(decimal.Decimal) # <8>
@htmlize.register(float)
def _(x) -> str:
frac = fractions.Fraction(x).limit_denominator()
return f'<pre>{x} ({frac.numerator}/{frac.denominator})</pre>'
# end::HTMLIZE[]

View File

@ -0,0 +1,31 @@
# tag::REGISTRATION[]
registry = [] # <1>
def register(func): # <2>
print(f'running register({func})') # <3>
registry.append(func) # <4>
return func # <5>
@register # <6>
def f1():
print('running f1()')
@register
def f2():
print('running f2()')
def f3(): # <7>
print('running f3()')
def main(): # <8>
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()
if __name__=='__main__':
main() # <9>
# end::REGISTRATION[]

View File

@ -0,0 +1,16 @@
# tag::REGISTRATION_ABRIDGED[]
registry = []
def register(func):
print(f'running register({func})')
registry.append(func)
return func
@register
def f1():
print('running f1()')
print('running main()')
print('registry ->', registry)
f1()
# end::REGISTRATION_ABRIDGED[]

View File

@ -0,0 +1,28 @@
# tag::REGISTRATION_PARAM[]
registry = set() # <1>
def register(active=True): # <2>
def decorate(func): # <3>
print('running register'
f'(active={active})->decorate({func})')
if active: # <4>
registry.add(func)
else:
registry.discard(func) # <5>
return func # <6>
return decorate # <7>
@register(active=False) # <8>
def f1():
print('running f1()')
@register() # <9>
def f2():
print('running f2()')
def f3():
print('running f3()')
# end::REGISTRATION_PARAM[]

View File

@ -0,0 +1,36 @@
def first(f):
print(f'apply first({f.__name__})')
def inner1st(n):
result = f(n)
print(f'inner1({n}): called {f.__name__}({n}) -> {result}')
return result
return inner1st
def second(f):
print(f'apply second({f.__name__})')
def inner2nd(n):
result = f(n)
print(f'inner2({n}): called {f.__name__}({n}) -> {result}')
return result
return inner2nd
@first
@second
def double(n):
return n * 2
print(double(3))
def double_(n):
return n * 2
double_ = first(second(double_))
print(double_(3))

View File

@ -0,0 +1,77 @@
Sample code for Chapter 10 - "Design patterns with first class functions"
From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015)
http://shop.oreilly.com/product/0636920032519.do
Notes
=====
No issues on file with zero type hints
--------------------------------------
Running Mypy on ``classic_strategy.py`` from the first edition, with no
type hints::
$ mypy classic_strategy.py
Success: no issues found in 1 source file
Type inference at play
----------------------
When the ``Order.due`` method made first assignment to discount as ``discount = 0``,
Mypy complained::
mypy classic_strategy.py
classic_strategy.py:68: error: Incompatible types in assignment (expression has type "float", variable has type "int")
Found 1 error in 1 file (checked 1 source file)
To fix it, I made the first assigment as ``discount = 0``.
I never explicitly declared a type for ``discount``.
Mypy ignores functions with no annotations
------------------------------------------
Mypy did not raise any issues with this test case::
def test_bulk_item_promo_with_discount(customer_fidelity_0):
cart = [LineItem('banana', 30, .5),
LineItem('apple', 10, 1.5)]
order = Order(customer_fidelity_0, 10, BulkItemPromo())
assert order.total() == 30.0
assert order.due() == 28.5
The second argument to ``Order`` is declared as ``Sequence[LineItem]``.
Mypy only checks the body of a function the signature as at least one annotation,
like this::
def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None:
cart = [LineItem('banana', 30, .5),
LineItem('apple', 10, 1.5)]
order = Order(customer_fidelity_0, 10, BulkItemPromo())
assert order.total() == 30.0
assert order.due() == 28.5
Now Mypy complains that "Argument 2 of Order has incompatible type".
However, even with the annotation in the test function signature,
Mypy did not find any problem when I mistyped the name of the ``cart`` argument.
Here, ``cart_plain`` should be ``cart``::
def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None:
cart = [LineItem('banana', 30, .5),
LineItem('apple', 10, 1.5)]
order = Order(customer_fidelity_0, cart_plain, BulkItemPromo())
assert order.total() == 30.0
assert order.due() == 28.5
Hypotesis: ``cart_plain`` is a function decorated with ``@pytest.fixture``,
and at the top of the test file I told Mypy to ignore the Pytest import::
import pytest # type: ignore

View File

@ -0,0 +1,113 @@
# classic_strategy.py
# Strategy pattern -- classic implementation
"""
# tag::CLASSIC_STRATEGY_TESTS[]
>>> joe = Customer('John Doe', 0) # <1>
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5), # <2>
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 5, 5.0)]
>>> Order(joe, cart, FidelityPromo()) # <3>
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, FidelityPromo()) # <4>
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5), # <5>
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
<Order total: 30.00 due: 28.50>
>>> big_cart = [LineItem(str(item_code), 1, 1.0) # <7>
... for item_code in range(10)]
>>> Order(joe, big_cart, LargeOrderPromo()) # <8>
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo())
<Order total: 42.00 due: 42.00>
# end::CLASSIC_STRATEGY_TESTS[]
"""
# tag::CLASSIC_STRATEGY[]
from abc import ABC, abstractmethod
import typing
from typing import Sequence, Optional
class Customer(typing.NamedTuple):
name: str
fidelity: int
class LineItem:
def __init__(self, product: str, quantity: int, price: float):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
class Order: # the Context
def __init__(
self,
customer: Customer,
cart: Sequence[LineItem],
promotion: Optional['Promotion'] = None,
):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self) -> float:
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self) -> float:
if self.promotion is None:
discount = 0.0
else:
discount = self.promotion.discount(self)
return self.total() - discount
def __repr__(self):
fmt = '<Order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
class Promotion(ABC): # the Strategy: an abstract base class
@abstractmethod
def discount(self, order: Order) -> float:
"""Return discount as a positive dollar amount"""
class FidelityPromo(Promotion): # first Concrete Strategy
"""5% discount for customers with 1000 or more fidelity points"""
def discount(self, order: Order) -> float:
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
class BulkItemPromo(Promotion): # second Concrete Strategy
"""10% discount for each LineItem with 20 or more units"""
def discount(self, order: Order) -> float:
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * 0.1
return discount
class LargeOrderPromo(Promotion): # third Concrete Strategy
"""7% discount for orders with 10 or more distinct items"""
def discount(self, order: Order) -> float:
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * 0.07
return 0
# end::CLASSIC_STRATEGY[]

View File

@ -0,0 +1,63 @@
from typing import List
import pytest # type: ignore
from classic_strategy import Customer, LineItem, Order
from classic_strategy import FidelityPromo, BulkItemPromo, LargeOrderPromo
@pytest.fixture
def customer_fidelity_0() -> Customer:
return Customer('John Doe', 0)
@pytest.fixture
def customer_fidelity_1100() -> Customer:
return Customer('Ann Smith', 1100)
@pytest.fixture
def cart_plain() -> List[LineItem]:
return [
LineItem('banana', 4, 0.5),
LineItem('apple', 10, 1.5),
LineItem('watermellon', 5, 5.0),
]
def test_fidelity_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, FidelityPromo())
assert order.total() == 42.0
assert order.due() == 42.0
def test_fidelity_promo_with_discount(customer_fidelity_1100, cart_plain) -> None:
order = Order(customer_fidelity_1100, cart_plain, FidelityPromo())
assert order.total() == 42.0
assert order.due() == 39.9
def test_bulk_item_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, BulkItemPromo())
assert order.total() == 42.0
assert order.due() == 42.0
def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None:
cart = [LineItem('banana', 30, 0.5), LineItem('apple', 10, 1.5)]
order = Order(customer_fidelity_0, cart, BulkItemPromo())
assert order.total() == 30.0
assert order.due() == 28.5
def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, LargeOrderPromo())
assert order.total() == 42.0
assert order.due() == 42.0
def test_large_order_promo_with_discount(customer_fidelity_0) -> None:
cart = [LineItem(str(item_code), 1, 1.0) for item_code in range(10)]
order = Order(customer_fidelity_0, cart, LargeOrderPromo())
assert order.total() == 10.0
assert order.due() == 9.3

View File

@ -0,0 +1,110 @@
# classic_strategy.py
# Strategy pattern -- classic implementation
"""
# tag::CLASSIC_STRATEGY_TESTS[]
>>> joe = Customer('John Doe', 0) # <1>
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5), # <2>
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 5, 5.0)]
>>> Order(joe, cart, FidelityPromo()) # <3>
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, FidelityPromo()) # <4>
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5), # <5>
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0) # <7>
... for item_code in range(10)]
>>> Order(joe, long_order, LargeOrderPromo()) # <8>
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo())
<Order total: 42.00 due: 42.00>
# end::CLASSIC_STRATEGY_TESTS[]
"""
# tag::CLASSIC_STRATEGY[]
from abc import ABC, abstractmethod
from collections import namedtuple
import typing
class Customer(typing.NamedTuple):
name: str
fidelity: int
class LineItem:
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
class Order: # the Context
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion.discount(self)
return self.total() - discount
def __repr__(self):
fmt = '<Order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
class Promotion(ABC): # the Strategy: an abstract base class
@abstractmethod
def discount(self, order):
"""Return discount as a positive dollar amount"""
class FidelityPromo(Promotion): # first Concrete Strategy
"""5% discount for customers with 1000 or more fidelity points"""
def discount(self, order):
return order.total() * .05 if order.customer.fidelity >= 1000 else 0
class BulkItemPromo(Promotion): # second Concrete Strategy
"""10% discount for each LineItem with 20 or more units"""
def discount(self, order):
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount
class LargeOrderPromo(Promotion): # third Concrete Strategy
"""7% discount for orders with 10 or more distinct items"""
def discount(self, order):
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0
# end::CLASSIC_STRATEGY[]

View File

@ -0,0 +1,33 @@
from typing import (
List,
Optional,
Union,
)
class BulkItemPromo:
def discount(self, order: Order) -> Union[float, int]: ...
class FidelityPromo:
def discount(self, order: Order) -> Union[float, int]: ...
class LargeOrderPromo:
def discount(self, order: Order) -> Union[float, int]: ...
class LineItem:
def __init__(self, product: str, quantity: int, price: float) -> None: ...
def total(self) -> float: ...
class Order:
def __init__(
self,
customer: Customer,
cart: List[LineItem],
promotion: Optional[Union[BulkItemPromo, LargeOrderPromo, FidelityPromo]] = ...
) -> None: ...
def due(self) -> float: ...
def total(self) -> float: ...

View File

@ -0,0 +1,63 @@
from typing import List
import pytest # type: ignore
from classic_strategy import Customer, LineItem, Order
from classic_strategy import FidelityPromo, BulkItemPromo, LargeOrderPromo
@pytest.fixture
def customer_fidelity_0() -> Customer:
return Customer('John Doe', 0)
@pytest.fixture
def customer_fidelity_1100() -> Customer:
return Customer('Ann Smith', 1100)
@pytest.fixture
def cart_plain() -> List[LineItem]:
return [LineItem('banana', 4, .5),
LineItem('apple', 10, 1.5),
LineItem('watermellon', 5, 5.0)]
def test_fidelity_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, FidelityPromo())
assert order.total() == 42.0
assert order.due() == 42.0
def test_fidelity_promo_with_discount(customer_fidelity_1100, cart_plain) -> None:
order = Order(customer_fidelity_1100, cart_plain, FidelityPromo())
assert order.total() == 42.0
assert order.due() == 39.9
def test_bulk_item_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, BulkItemPromo())
assert order.total() == 42.0
assert order.due() == 42.0
def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None:
cart = [LineItem('banana', 30, .5),
LineItem('apple', 10, 1.5)]
order = Order(customer_fidelity_0, cart, BulkItemPromo())
assert order.total() == 30.0
assert order.due() == 28.5
def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, LargeOrderPromo())
assert order.total() == 42.0
assert order.due() == 42.0
def test_large_order_promo_with_discount(customer_fidelity_0) -> None:
cart = [LineItem(str(item_code), 1, 1.0)
for item_code in range(10)]
order = Order(customer_fidelity_0, cart, LargeOrderPromo())
assert order.total() == 10.0
assert order.due() == 9.3

View File

@ -0,0 +1,2 @@
import pytest
pytest.main(['.'])

View File

@ -0,0 +1,20 @@
def fidelity_promo(order):
"""5% discount for customers with 1000 or more fidelity points"""
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
def bulk_item_promo(order):
"""10% discount for each LineItem with 20 or more units"""
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * 0.1
return discount
def large_order_promo(order):
"""7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * 0.07
return 0

View File

@ -0,0 +1,116 @@
# classic_strategy.py
# Strategy pattern -- classic implementation
"""
# tag::CLASSIC_STRATEGY_TESTS[]
>>> joe = Customer('John Doe', 0) # <1>
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5), # <2>
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 5, 5.0)]
>>> Order(joe, cart, FidelityPromo()) # <3>
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, FidelityPromo()) # <4>
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5), # <5>
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0) # <7>
... for item_code in range(10)]
>>> Order(joe, long_order, LargeOrderPromo()) # <8>
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo())
<Order total: 42.00 due: 42.00>
# end::CLASSIC_STRATEGY_TESTS[]
"""
# tag::CLASSIC_STRATEGY[]
from abc import ABC, abstractmethod
from collections import namedtuple
import typing
from pytypes import typelogged
class Customer(typing.NamedTuple):
name: str
fidelity: int
@typelogged
class LineItem:
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
@typelogged
class Order: # the Context
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion.discount(self)
return self.total() - discount
def __repr__(self):
fmt = '<Order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
@typelogged
class Promotion(ABC): # the Strategy: an abstract base class
@abstractmethod
def discount(self, order):
"""Return discount as a positive dollar amount"""
@typelogged
class FidelityPromo(Promotion): # first Concrete Strategy
"""5% discount for customers with 1000 or more fidelity points"""
def discount(self, order):
return order.total() * .05 if order.customer.fidelity >= 1000 else 0
@typelogged
class BulkItemPromo(Promotion): # second Concrete Strategy
"""10% discount for each LineItem with 20 or more units"""
def discount(self, order):
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount
@typelogged
class LargeOrderPromo(Promotion): # third Concrete Strategy
"""7% discount for orders with 10 or more distinct items"""
def discount(self, order):
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0
# end::CLASSIC_STRATEGY[]

View File

@ -0,0 +1,63 @@
from typing import List
import pytest # type: ignore
from classic_strategy import Customer, LineItem, Order
from classic_strategy import FidelityPromo, BulkItemPromo, LargeOrderPromo
@pytest.fixture
def customer_fidelity_0() -> Customer:
return Customer('John Doe', 0)
@pytest.fixture
def customer_fidelity_1100() -> Customer:
return Customer('Ann Smith', 1100)
@pytest.fixture
def cart_plain() -> List[LineItem]:
return [LineItem('banana', 4, .5),
LineItem('apple', 10, 1.5),
LineItem('watermellon', 5, 5.0)]
def test_fidelity_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, FidelityPromo())
assert order.total() == 42.0
assert order.due() == 42.0
def test_fidelity_promo_with_discount(customer_fidelity_1100, cart_plain) -> None:
order = Order(customer_fidelity_1100, cart_plain, FidelityPromo())
assert order.total() == 42.0
assert order.due() == 39.9
def test_bulk_item_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, BulkItemPromo())
assert order.total() == 42.0
assert order.due() == 42.0
def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None:
cart = [LineItem('banana', 30, .5),
LineItem('apple', 10, 1.5)]
order = Order(customer_fidelity_0, cart, BulkItemPromo())
assert order.total() == 30.0
assert order.due() == 28.5
def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, LargeOrderPromo())
assert order.total() == 42.0
assert order.due() == 42.0
def test_large_order_promo_with_discount(customer_fidelity_0) -> None:
cart = [LineItem(str(item_code), 1, 1.0)
for item_code in range(10)]
order = Order(customer_fidelity_0, cart, LargeOrderPromo())
assert order.total() == 10.0
assert order.due() == 9.3

View File

@ -0,0 +1,48 @@
"""
Automatically generated stubfile of
/home/luciano/flupy/priv/2e-atlas/code/10-dp-1class-func/pytypes/classic_strategy.py
MD5-Checksum: a02fa3b98639f84a81b87d4f46007d51
This file was generated by pytypes.typelogger v1.0b5
at 2020-05-02T15:50:59.987984.
Type information is based on runtime observations while running
CPython 3.8.2 final 0
/home/luciano/flupy/venv-3.8/bin/python3
/home/luciano/flupy/venv-3.8/bin/pytest
WARNING:
If you edit this file, be aware that it was automatically generated.
Save your customized version to a distinct place;
this file might be overwritten without notice.
"""
from classic_strategy import Customer, Promotion
from typing import Union
class LineItem(object):
def __init__(self, product: str, quantity: int, price: float) -> None: ...
def total(self) -> float: ...
class Order(object):
def __init__(self, customer: Customer, cart: List[LineItem], promotion: Union[BulkItemPromo, FidelityPromo, LargeOrderPromo]) -> None: ...
def total(self) -> float: ...
def due(self) -> float: ...
class FidelityPromo(Promotion):
def discount(self, order: Order) -> float: ...
class BulkItemPromo(Promotion):
def discount(self, order: Order) -> float: ...
class LargeOrderPromo(Promotion):
def discount(self, order: Order) -> float: ...

View File

@ -0,0 +1,13 @@
mypy==0.770
mypy-extensions==0.4.3
typed-ast==1.4.1
typing-extensions==3.7.4.1
attrs==19.3.0
more-itertools==8.2.0
packaging==20.3
pluggy==0.13.1
py==1.8.1
pyparsing==2.4.6
pytest==5.4.1
six==1.14.0
wcwidth==0.1.9

View File

@ -0,0 +1,103 @@
# strategy.py
# Strategy pattern -- function-based implementation
"""
# tag::STRATEGY_TESTS[]
>>> joe = Customer('John Doe', 0) # <1>
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5),
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 5, 5.0)]
>>> Order(joe, cart, fidelity_promo) # <2>
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity_promo)
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5),
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo) # <3>
<Order total: 30.00 due: 28.50>
>>> big_cart = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, big_cart, large_order_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>
# end::STRATEGY_TESTS[]
"""
# tag::STRATEGY[]
import typing
from typing import Sequence, Optional, Callable
class Customer(typing.NamedTuple):
name: str
fidelity: int
class LineItem:
def __init__(self, product: str, quantity: int, price: float):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
class Order: # the Context
def __init__(
self,
customer: Customer,
cart: Sequence[LineItem],
promotion: Optional[Callable[['Order'], float]] = None,
) -> None:
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self) -> float:
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self) -> float:
if self.promotion is None:
discount = 0.0
else:
discount = self.promotion(self) # <1>
return self.total() - discount
def __repr__(self):
fmt = '<Order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
# <2>
def fidelity_promo(order: Order) -> float: # <3>
"""5% discount for customers with 1000 or more fidelity points"""
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
def bulk_item_promo(order: Order):
"""10% discount for each LineItem with 20 or more units"""
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * 0.1
return discount
def large_order_promo(order: Order):
"""7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * 0.07
return 0
# end::STRATEGY[]

View File

@ -0,0 +1,42 @@
# strategy_best.py
# Strategy pattern -- function-based implementation
# selecting best promotion from static list of functions
"""
>>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5),
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 5, 5.0)]
>>> banana_cart = [LineItem('banana', 30, .5),
... LineItem('apple', 10, 1.5)]
>>> big_cart = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
# tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, big_cart, best_promo) # <1>
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo) # <2>
<Order total: 30.00 due: 28.50>
>>> Order(ann, cart, best_promo) # <3>
<Order total: 42.00 due: 39.90>
# end::STRATEGY_BEST_TESTS[]
"""
from strategy import Customer, LineItem, Order
from strategy import fidelity_promo, bulk_item_promo, large_order_promo
# tag::STRATEGY_BEST[]
promos = [fidelity_promo, bulk_item_promo, large_order_promo] # <1>
def best_promo(order) -> float: # <2>
"""Select best discount available
"""
return max(promo(order) for promo in promos) # <3>
# end::STRATEGY_BEST[]

View File

@ -0,0 +1,113 @@
# strategy_best2.py
# Strategy pattern -- function-based implementation
# selecting best promotion from current module globals
"""
>>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5),
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 5, 5.0)]
>>> Order(joe, cart, fidelity_promo)
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity_promo)
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5),
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, long_order, large_order_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50>
>>> Order(ann, cart, best_promo)
<Order total: 42.00 due: 39.90>
# end::STRATEGY_BEST_TESTS[]
"""
from collections import namedtuple
Customer = namedtuple('Customer', 'name fidelity')
class LineItem:
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
class Order: # the Context
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion(self)
return self.total() - discount
def __repr__(self):
fmt = '<Order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
def fidelity_promo(order):
"""5% discount for customers with 1000 or more fidelity points"""
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
def bulk_item_promo(order):
"""10% discount for each LineItem with 20 or more units"""
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * 0.1
return discount
def large_order_promo(order):
"""7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * 0.07
return 0
# tag::STRATEGY_BEST2[]
promos = [
globals()[name]
for name in globals() # <1>
if name.endswith('_promo') and name != 'best_promo' # <2>
] # <3>
def best_promo(order):
"""Select best discount available
"""
return max(promo(order) for promo in promos) # <4>
# end::STRATEGY_BEST2[]

View File

@ -0,0 +1,91 @@
# strategy_best3.py
# Strategy pattern -- function-based implementation
# selecting best promotion from imported module
"""
>>> from promotions import *
>>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5),
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 5, 5.0)]
>>> Order(joe, cart, fidelity_promo)
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity_promo)
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5),
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, long_order, large_order_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50>
>>> Order(ann, cart, best_promo)
<Order total: 42.00 due: 39.90>
# end::STRATEGY_BEST_TESTS[]
"""
from collections import namedtuple
import inspect
import promotions
Customer = namedtuple('Customer', 'name fidelity')
class LineItem:
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
class Order: # the Context
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion(self)
return self.total() - discount
def __repr__(self):
fmt = '<Order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
# tag::STRATEGY_BEST3[]
promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)]
def best_promo(order):
"""Select best discount available
"""
return max(promo(order) for promo in promos)
# end::STRATEGY_BEST3[]

View File

@ -0,0 +1,122 @@
# strategy_best4.py
# Strategy pattern -- function-based implementation
# selecting best promotion from list of functions
# registered by a decorator
"""
>>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5),
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 5, 5.0)]
>>> Order(joe, cart, fidelity)
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity)
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5),
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item)
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, long_order, large_order)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order)
<Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50>
>>> Order(ann, cart, best_promo)
<Order total: 42.00 due: 39.90>
# end::STRATEGY_BEST_TESTS[]
"""
from collections import namedtuple
from typing import Callable, List
Customer = namedtuple('Customer', 'name fidelity')
class LineItem:
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
class Order: # the Context
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion(self)
return self.total() - discount
def __repr__(self):
fmt = '<Order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
# tag::STRATEGY_BEST4[]
Promotion = Callable[[Order], float] # <2>
def promotion(promo: Promotion) -> Promotion: # <2>
promos.append(promo)
return promo
promos: List[Promotion] = [] # <1>
@promotion # <3>
def fidelity(order: Order) -> float:
"""5% discount for customers with 1000 or more fidelity points"""
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
@promotion
def bulk_item(order: Order) -> float:
"""10% discount for each LineItem with 20 or more units"""
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * 0.1
return discount
@promotion
def large_order(order: Order) -> float:
"""7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * 0.07
return 0
def best_promo(order: Order) -> float: # <4>
"""Select best discount available
"""
return max(promo(order) for promo in promos)
# end::STRATEGY_BEST4[]

View File

@ -0,0 +1,123 @@
# strategy_param.py
# Strategy pattern -- parametrized with closure
"""
>>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5),
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 5, 5.0)]
>>> Order(joe, cart, fidelity_promo(10))
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity_promo(10))
<Order total: 42.00 due: 37.80>
>>> banana_cart = [LineItem('banana', 30, .5),
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo(10))
<Order total: 30.00 due: 28.50>
>>> big_cart = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, big_cart, LargeOrderPromo(7))
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo(7))
<Order total: 42.00 due: 42.00>
Using ``partial`` to build a parametrized discounter on the fly::
>>> from functools import partial
>>> Order(joe, cart, partial(general_discount, 5))
<Order total: 42.00 due: 39.90>
"""
import typing
from typing import Sequence, Optional, Callable
class Customer(typing.NamedTuple):
name: str
fidelity: int
class LineItem:
def __init__(self, product: str, quantity: int, price: float):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
class Order: # the Context
def __init__(
self,
customer: Customer,
cart: Sequence[LineItem],
promotion: Optional['Promotion'] = None,
):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self) -> float:
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self) -> float:
if self.promotion is None:
discount = 0.0
else:
discount = self.promotion(self) # <1>
return self.total() - discount
def __repr__(self):
fmt = '<Order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
# tag::STRATEGY_PARAM[]
Promotion = Callable[[Order], float] # <2>
def fidelity_promo(percent: float) -> Promotion:
"""discount for customers with 1000 or more fidelity points"""
return lambda order: (
order.total() * percent / 100.0 if order.customer.fidelity >= 1000 else 0
)
def bulk_item_promo(percent: float) -> Promotion:
"""discount for each LineItem with 20 or more units"""
def discounter(order: Order) -> float:
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * percent / 100.0
return discount
return discounter
class LargeOrderPromo:
"""discount for orders with 10 or more distinct items"""
def __init__(self, percent: float):
self.percent = percent
def __call__(self, order: Order) -> float:
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * self.percent / 100.0
return 0
def general_discount(percent: float, order: Order) -> float:
"""unrestricted discount; usage: ``partial(general_discount, 5)``"""
return order.total() * percent / 100.0
# end::STRATEGY[]

Some files were not shown because too many files have changed in this diff Show More