ch08, 09, 10: example files
This commit is contained in:
parent
42861b64d8
commit
bf4a2be8b9
1
08-def-type-hints/README.asciidoc
Normal file
1
08-def-type-hints/README.asciidoc
Normal file
@ -0,0 +1 @@
|
||||
== Type Hints in Function Definitions
|
67
08-def-type-hints/RPN_calc/calc.py
Executable file
67
08-def-type-hints/RPN_calc/calc.py
Executable 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()
|
51
08-def-type-hints/RPN_calc/calc_test.py
Normal file
51
08-def-type-hints/RPN_calc/calc_test.py
Normal 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()
|
36
08-def-type-hints/arg_lab.py
Normal file
36
08-def-type-hints/arg_lab.py
Normal 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'))
|
15
08-def-type-hints/birds/birds.py
Normal file
15
08-def-type-hints/birds/birds.py
Normal 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()
|
6
08-def-type-hints/birds/daffy.py
Normal file
6
08-def-type-hints/birds/daffy.py
Normal file
@ -0,0 +1,6 @@
|
||||
from birds import *
|
||||
|
||||
daffy = Duck()
|
||||
alert(daffy) # <1>
|
||||
alert_duck(daffy) # <2>
|
||||
alert_bird(daffy) # <3>
|
9
08-def-type-hints/birds/protocol/lake.py
Normal file
9
08-def-type-hints/birds/protocol/lake.py
Normal 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)
|
10
08-def-type-hints/birds/protocol/parrot.py
Normal file
10
08-def-type-hints/birds/protocol/parrot.py
Normal 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>
|
13
08-def-type-hints/birds/protocol/swan.py
Normal file
13
08-def-type-hints/birds/protocol/swan.py
Normal 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>
|
6
08-def-type-hints/birds/woody.py
Normal file
6
08-def-type-hints/birds/woody.py
Normal 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
29
08-def-type-hints/bus.py
Normal 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[]
|
35
08-def-type-hints/charindex.py
Normal file
35
08-def-type-hints/charindex.py
Normal 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[]
|
38
08-def-type-hints/clip_annot.py
Normal file
38
08-def-type-hints/clip_annot.py
Normal 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[]
|
38
08-def-type-hints/clip_annot_1ed.py
Normal file
38
08-def-type-hints/clip_annot_1ed.py
Normal 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[]
|
10
08-def-type-hints/clip_annot_signature.rst
Normal file
10
08-def-type-hints/clip_annot_signature.rst
Normal 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
|
81
08-def-type-hints/colors.py
Normal file
81
08-def-type-hints/colors.py
Normal 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()
|
26
08-def-type-hints/columnize.py
Normal file
26
08-def-type-hints/columnize.py
Normal 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()
|
37
08-def-type-hints/columnize2.py
Normal file
37
08-def-type-hints/columnize2.py
Normal 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()
|
27
08-def-type-hints/columnize_alias.py
Normal file
27
08-def-type-hints/columnize_alias.py
Normal 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()
|
79
08-def-type-hints/columnize_test.py
Normal file
79
08-def-type-hints/columnize_test.py
Normal 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
|
4
08-def-type-hints/comparable/comparable.py
Normal file
4
08-def-type-hints/comparable/comparable.py
Normal file
@ -0,0 +1,4 @@
|
||||
from typing import Protocol, Any
|
||||
|
||||
class Comparable(Protocol): # <1>
|
||||
def __lt__(self, other: Any) -> bool: ... # <2>
|
60
08-def-type-hints/comparable/mymax.py
Normal file
60
08-def-type-hints/comparable/mymax.py
Normal 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[]
|
127
08-def-type-hints/comparable/mymax_demo.py
Normal file
127
08-def-type-hints/comparable/mymax_demo.py
Normal 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()
|
69
08-def-type-hints/comparable/mymax_test.py
Normal file
69
08-def-type-hints/comparable/mymax_test.py
Normal 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
|
31
08-def-type-hints/comparable/top.py
Normal file
31
08-def-type-hints/comparable/top.py
Normal 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[]
|
39
08-def-type-hints/comparable/top_test.py
Normal file
39
08-def-type-hints/comparable/top_test.py
Normal 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[]
|
19
08-def-type-hints/coordinates/coordinates.py
Normal file
19
08-def-type-hints/coordinates/coordinates.py
Normal 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[]
|
36
08-def-type-hints/coordinates/coordinates_named.py
Normal file
36
08-def-type-hints/coordinates/coordinates_named.py
Normal 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)
|
12
08-def-type-hints/coordinates/coordinates_named_test.py
Normal file
12
08-def-type-hints/coordinates/coordinates_named_test.py
Normal 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'
|
6
08-def-type-hints/coordinates/coordinates_test.py
Normal file
6
08-def-type-hints/coordinates/coordinates_test.py
Normal 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
|
2
08-def-type-hints/coordinates/requirements.txt
Normal file
2
08-def-type-hints/coordinates/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
geolib==1.0.7
|
||||
future==0.18.2
|
5
08-def-type-hints/ctime.py
Normal file
5
08-def-type-hints/ctime.py
Normal file
@ -0,0 +1,5 @@
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
def ctime(secs: Optional[float] = None, /) -> str:
|
||||
return time.ctime(secs)
|
2
08-def-type-hints/double/double_object.py
Normal file
2
08-def-type-hints/double/double_object.py
Normal file
@ -0,0 +1,2 @@
|
||||
def double(n: object) -> object:
|
||||
return n * 2
|
11
08-def-type-hints/double/double_protocol.py
Normal file
11
08-def-type-hints/double/double_protocol.py
Normal 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
|
6
08-def-type-hints/double/double_sequence.py
Normal file
6
08-def-type-hints/double/double_sequence.py
Normal file
@ -0,0 +1,6 @@
|
||||
from collections import abc
|
||||
from typing import Any
|
||||
|
||||
def double(n: abc.Sequence) -> Any:
|
||||
return n * 2
|
||||
|
56
08-def-type-hints/double/double_test.py
Normal file
56
08-def-type-hints/double/double_test.py
Normal 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
10
08-def-type-hints/list.py
Normal 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)
|
20
08-def-type-hints/messages/hints_1/messages.py
Normal file
20
08-def-type-hints/messages/hints_1/messages.py
Normal 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[]
|
17
08-def-type-hints/messages/hints_1/messages_test.py
Normal file
17
08-def-type-hints/messages/hints_1/messages_test.py
Normal 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'
|
6
08-def-type-hints/messages/hints_1/mypy.ini
Normal file
6
08-def-type-hints/messages/hints_1/mypy.ini
Normal file
@ -0,0 +1,6 @@
|
||||
[mypy]
|
||||
python_version = 3.8
|
||||
warn_unused_configs = True
|
||||
disallow_incomplete_defs = True
|
||||
[mypy-pytest]
|
||||
ignore_missing_imports = True
|
24
08-def-type-hints/messages/hints_2/messages.py
Normal file
24
08-def-type-hints/messages/hints_2/messages.py
Normal 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[]
|
25
08-def-type-hints/messages/hints_2/messages_test.py
Normal file
25
08-def-type-hints/messages/hints_2/messages_test.py
Normal 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[]
|
20
08-def-type-hints/messages/no_hints/messages.py
Normal file
20
08-def-type-hints/messages/no_hints/messages.py
Normal 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[]
|
15
08-def-type-hints/messages/no_hints/messages_test.py
Normal file
15
08-def-type-hints/messages/no_hints/messages_test.py
Normal 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'
|
25
08-def-type-hints/mode/mode_T.py
Normal file
25
08-def-type-hints/mode/mode_T.py
Normal 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()
|
23
08-def-type-hints/mode/mode_float.py
Normal file
23
08-def-type-hints/mode/mode_float.py
Normal 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()
|
28
08-def-type-hints/mode/mode_hashable.py
Normal file
28
08-def-type-hints/mode/mode_hashable.py
Normal 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()
|
24
08-def-type-hints/mode/mode_hashable_wrong.py
Normal file
24
08-def-type-hints/mode/mode_hashable_wrong.py
Normal 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()
|
26
08-def-type-hints/mode/mode_number.py
Normal file
26
08-def-type-hints/mode/mode_number.py
Normal 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()
|
13
08-def-type-hints/mysum.py
Normal file
13
08-def-type-hints/mysum.py
Normal 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
101
08-def-type-hints/passdrill.py
Executable 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)
|
36
08-def-type-hints/replacer.py
Normal file
36
08-def-type-hints/replacer.py
Normal 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()
|
39
08-def-type-hints/replacer2.py
Normal file
39
08-def-type-hints/replacer2.py
Normal 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()
|
8
08-def-type-hints/reveal_array.py
Normal file
8
08-def-type-hints/reveal_array.py
Normal 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)
|
||||
|
16
08-def-type-hints/romans.py
Normal file
16
08-def-type-hints/romans.py
Normal 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)
|
18
08-def-type-hints/romans_test.py
Normal file
18
08-def-type-hints/romans_test.py
Normal 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
|
34
08-def-type-hints/sample.py
Normal file
34
08-def-type-hints/sample.py
Normal 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()
|
32
08-def-type-hints/typeddict/books.py
Normal file
32
08-def-type-hints/typeddict/books.py
Normal 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[]
|
32
08-def-type-hints/typeddict/books_any.py
Normal file
32
08-def-type-hints/typeddict/books_any.py
Normal 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[]
|
20
08-def-type-hints/typeddict/demo_books.py
Normal file
20
08-def-type-hints/typeddict/demo_books.py
Normal 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()
|
23
08-def-type-hints/typeddict/demo_not_book.py
Normal file
23
08-def-type-hints/typeddict/demo_not_book.py
Normal 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()
|
112
08-def-type-hints/typeddict/test_books.py
Normal file
112
08-def-type-hints/typeddict/test_books.py
Normal 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
|
23
08-def-type-hints/typeddict/test_books_check_fails.py
Normal file
23
08-def-type-hints/typeddict/test_books_check_fails.py
Normal 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
|
4
09-closure-deco/README.rst
Normal file
4
09-closure-deco/README.rst
Normal 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
|
33
09-closure-deco/average.py
Normal file
33
09-closure-deco/average.py
Normal 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
|
21
09-closure-deco/average_oo.py
Normal file
21
09-closure-deco/average_oo.py
Normal 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)
|
21
09-closure-deco/clockdeco.py
Normal file
21
09-closure-deco/clockdeco.py
Normal 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
|
13
09-closure-deco/clockdeco0.py
Normal file
13
09-closure-deco/clockdeco0.py
Normal 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>
|
44
09-closure-deco/clockdeco_cls.py
Normal file
44
09-closure-deco/clockdeco_cls.py
Normal 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)
|
||||
|
||||
|
19
09-closure-deco/clockdeco_demo.py
Normal file
19
09-closure-deco/clockdeco_demo.py
Normal 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))
|
40
09-closure-deco/clockdeco_param.py
Normal file
40
09-closure-deco/clockdeco_param.py
Normal 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[]
|
9
09-closure-deco/clockdeco_param_demo1.py
Normal file
9
09-closure-deco/clockdeco_param_demo1.py
Normal 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)
|
9
09-closure-deco/clockdeco_param_demo2.py
Normal file
9
09-closure-deco/clockdeco_param_demo2.py
Normal 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)
|
17
09-closure-deco/fibo_compare.py
Normal file
17
09-closure-deco/fibo_compare.py
Normal 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()
|
12
09-closure-deco/fibo_demo.py
Normal file
12
09-closure-deco/fibo_demo.py
Normal 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))
|
15
09-closure-deco/fibo_demo_lru.py
Normal file
15
09-closure-deco/fibo_demo_lru.py
Normal 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))
|
124
09-closure-deco/global_x_local.rst
Normal file
124
09-closure-deco/global_x_local.rst
Normal 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
|
||||
|
||||
|
76
09-closure-deco/htmlizer.py
Normal file
76
09-closure-deco/htmlizer.py
Normal 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><built-in function abs></pre>'
|
||||
>>> htmlize('Heimlich & Co.\n- a game') # <2>
|
||||
'<p>Heimlich & 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[]
|
||||
|
31
09-closure-deco/registration.py
Normal file
31
09-closure-deco/registration.py
Normal 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[]
|
16
09-closure-deco/registration_abridged.py
Normal file
16
09-closure-deco/registration_abridged.py
Normal 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[]
|
28
09-closure-deco/registration_param.py
Normal file
28
09-closure-deco/registration_param.py
Normal 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[]
|
36
09-closure-deco/stacked.py
Normal file
36
09-closure-deco/stacked.py
Normal 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))
|
77
10-dp-1class-func/README.rst
Normal file
77
10-dp-1class-func/README.rst
Normal 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
|
113
10-dp-1class-func/classic_strategy.py
Normal file
113
10-dp-1class-func/classic_strategy.py
Normal 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[]
|
63
10-dp-1class-func/classic_strategy_test.py
Normal file
63
10-dp-1class-func/classic_strategy_test.py
Normal 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
|
110
10-dp-1class-func/monkeytype/classic_strategy.py
Normal file
110
10-dp-1class-func/monkeytype/classic_strategy.py
Normal 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[]
|
33
10-dp-1class-func/monkeytype/classic_strategy.pyi
Normal file
33
10-dp-1class-func/monkeytype/classic_strategy.pyi
Normal 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: ...
|
63
10-dp-1class-func/monkeytype/classic_strategy_test.py
Normal file
63
10-dp-1class-func/monkeytype/classic_strategy_test.py
Normal 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
|
2
10-dp-1class-func/monkeytype/run.py
Normal file
2
10-dp-1class-func/monkeytype/run.py
Normal file
@ -0,0 +1,2 @@
|
||||
import pytest
|
||||
pytest.main(['.'])
|
20
10-dp-1class-func/promotions.py
Normal file
20
10-dp-1class-func/promotions.py
Normal 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
|
116
10-dp-1class-func/pytypes/classic_strategy.py
Normal file
116
10-dp-1class-func/pytypes/classic_strategy.py
Normal 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[]
|
63
10-dp-1class-func/pytypes/classic_strategy_test.py
Normal file
63
10-dp-1class-func/pytypes/classic_strategy_test.py
Normal 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
|
@ -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: ...
|
||||
|
13
10-dp-1class-func/requirements.txt
Normal file
13
10-dp-1class-func/requirements.txt
Normal 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
|
103
10-dp-1class-func/strategy.py
Normal file
103
10-dp-1class-func/strategy.py
Normal 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[]
|
42
10-dp-1class-func/strategy_best.py
Normal file
42
10-dp-1class-func/strategy_best.py
Normal 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[]
|
113
10-dp-1class-func/strategy_best2.py
Normal file
113
10-dp-1class-func/strategy_best2.py
Normal 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[]
|
91
10-dp-1class-func/strategy_best3.py
Normal file
91
10-dp-1class-func/strategy_best3.py
Normal 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[]
|
122
10-dp-1class-func/strategy_best4.py
Normal file
122
10-dp-1class-func/strategy_best4.py
Normal 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[]
|
123
10-dp-1class-func/strategy_param.py
Normal file
123
10-dp-1class-func/strategy_param.py
Normal 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
Loading…
Reference in New Issue
Block a user