updated from Atlas
This commit is contained in:
parent
cbd13885fc
commit
01e717b60a
@ -23,4 +23,14 @@ The copyright holder is Peter Norvig and the code is licensed under the
|
||||
[MIT license](https://github.com/norvig/pytudes/blob/60168bce8cdfacf57c92a5b2979f0b2e95367753/LICENSE).
|
||||
|
||||
|
||||
## Changes to Norvig's code
|
||||
|
||||
I made small changes to the programs in `original/`:
|
||||
|
||||
* In `lis.py`:
|
||||
* The `Procedure` class accepts a list of expressions as the `body`, and `__call__` evaluates those expressions in order, and returns the value of the last. This is consistent with Scheme's `lambda` syntax and provided a useful example for pattern matching.
|
||||
* In the `elif` block for `'lambda'`, I added the `*` in front of the `*body` variable in the tuple unpacking to capture the expressions as a list, before calling the `Procedure` constructor.
|
||||
|
||||
* In `lispy.py` I made [changes and a pull request](https://github.com/norvig/pytudes/pull/106) to make it run on Python 3.
|
||||
|
||||
_Luciano Ramalho<br/>June 29, 2021_
|
||||
|
@ -58,7 +58,7 @@ Test ``find`` with single result::
|
||||
|
||||
Test ``find`` with two results::
|
||||
|
||||
>>> find('chess', 'queen', last=0xFFFF) # doctest:+NORMALIZE_WHITESPACE
|
||||
>>> find('chess', 'queen', end=0xFFFF) # doctest:+NORMALIZE_WHITESPACE
|
||||
U+2655 ♕ WHITE CHESS QUEEN
|
||||
U+265B ♛ BLACK CHESS QUEEN
|
||||
|
||||
|
@ -2,11 +2,11 @@
|
||||
import sys
|
||||
import unicodedata
|
||||
|
||||
FIRST, LAST = ord(' '), sys.maxunicode # <1>
|
||||
START, END = ord(' '), sys.maxunicode + 1 # <1>
|
||||
|
||||
def find(*query_words, first=FIRST, last=LAST): # <2>
|
||||
def find(*query_words, start=START, end=END): # <2>
|
||||
query = {w.upper() for w in query_words} # <3>
|
||||
for code in range(first, last + 1):
|
||||
for code in range(start, end):
|
||||
char = chr(code) # <4>
|
||||
name = unicodedata.name(char, None) # <5>
|
||||
if name and query.issubset(name.split()): # <6>
|
||||
|
@ -1,9 +1,7 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClubMember:
|
||||
|
||||
name: str
|
||||
guests: list = field(default_factory=list)
|
||||
|
||||
|
@ -3,7 +3,6 @@ from dataclasses import dataclass
|
||||
# tag::CLUBMEMBER[]
|
||||
@dataclass
|
||||
class ClubMember:
|
||||
|
||||
name: str
|
||||
guests: list = []
|
||||
# end::CLUBMEMBER[]
|
||||
|
@ -34,9 +34,7 @@ from club import ClubMember
|
||||
|
||||
@dataclass
|
||||
class HackerClubMember(ClubMember): # <1>
|
||||
|
||||
all_handles = set() # <2>
|
||||
|
||||
handle: str = '' # <3>
|
||||
|
||||
def __post_init__(self):
|
||||
|
@ -35,9 +35,7 @@ from club import ClubMember
|
||||
|
||||
@dataclass
|
||||
class HackerClubMember(ClubMember):
|
||||
|
||||
all_handles: ClassVar[set[str]] = set()
|
||||
|
||||
handle: str = ''
|
||||
|
||||
def __post_init__(self):
|
||||
|
@ -32,7 +32,7 @@ from enum import Enum, auto
|
||||
from datetime import date
|
||||
|
||||
|
||||
class ResourceType(Enum): # <1>
|
||||
class ResourceType(Enum): # <1>
|
||||
BOOK = auto()
|
||||
EBOOK = auto()
|
||||
VIDEO = auto()
|
||||
|
@ -2,7 +2,6 @@ from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class DemoDataClass:
|
||||
|
||||
a: int # <1>
|
||||
b: float = 1.1 # <2>
|
||||
c = 'spam' # <3>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import typing
|
||||
|
||||
class DemoNTClass(typing.NamedTuple):
|
||||
|
||||
a: int # <1>
|
||||
b: float = 1.1 # <2>
|
||||
c = 'spam' # <3>
|
||||
|
@ -1,5 +1,4 @@
|
||||
class DemoPlainClass:
|
||||
|
||||
a: int # <1>
|
||||
b: float = 1.1 # <2>
|
||||
c = 'spam' # <3>
|
||||
|
@ -11,7 +11,6 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
class Coordinate(NamedTuple):
|
||||
|
||||
lat: float
|
||||
lon: float
|
||||
|
||||
|
@ -13,8 +13,7 @@ This version has a field with a default value::
|
||||
from typing import NamedTuple
|
||||
|
||||
class Coordinate(NamedTuple):
|
||||
|
||||
lat: float # <1>
|
||||
lon: float
|
||||
reference: str = 'WGS84' # <2>
|
||||
# end::COORDINATE[]
|
||||
# end::COORDINATE[]
|
||||
|
@ -1,7 +1,6 @@
|
||||
import typing
|
||||
|
||||
class Coordinate(typing.NamedTuple):
|
||||
|
||||
lat: float
|
||||
lon: float
|
||||
|
||||
|
@ -1,48 +0,0 @@
|
||||
"""
|
||||
>>> clip('banana split', 5)
|
||||
'banana'
|
||||
>>> clip('banana split', 6)
|
||||
'banana'
|
||||
>>> clip('banana split', 7)
|
||||
'banana'
|
||||
>>> clip('banana split', 8)
|
||||
'banana'
|
||||
>>> clip('banana split', 11)
|
||||
'banana'
|
||||
>>> clip('banana split', 12)
|
||||
'banana split'
|
||||
>>> clip('banana-split', 3)
|
||||
'banana-split'
|
||||
|
||||
Jess' tests:
|
||||
|
||||
>>> text = 'The quick brown fox jumps over the lazy dog.'
|
||||
>>> clip14 = clip(text, max_len=14)
|
||||
>>> clip14
|
||||
'The quick'
|
||||
>>> len(clip14)
|
||||
9
|
||||
>>> clip15 = clip(text, max_len=15)
|
||||
>>> clip15
|
||||
'The quick brown'
|
||||
>>> len(clip15)
|
||||
15
|
||||
|
||||
"""
|
||||
|
||||
# tag::CLIP[]
|
||||
def clip(text, max_len=80):
|
||||
"""Return max_len characters clipped at space if possible"""
|
||||
text = text.rstrip()
|
||||
if len(text) <= max_len or ' ' not in text:
|
||||
return text
|
||||
end = len(text)
|
||||
space_at = text.rfind(' ', 0, max_len + 1)
|
||||
if space_at >= 0:
|
||||
end = space_at
|
||||
else:
|
||||
space_at = text.find(' ', max_len)
|
||||
if space_at >= 0:
|
||||
end = space_at
|
||||
return text[:end].rstrip()
|
||||
# end::CLIP[]
|
@ -1,9 +0,0 @@
|
||||
>>> from clip import clip
|
||||
>>> clip.__defaults__
|
||||
(80,)
|
||||
>>> clip.__code__ # doctest: +ELLIPSIS
|
||||
<code object clip at 0x...>
|
||||
>>> clip.__code__.co_varnames
|
||||
('text', 'max_len', 'end', 'space_at')
|
||||
>>> clip.__code__.co_argcount
|
||||
2
|
@ -1,12 +0,0 @@
|
||||
>>> from clip import clip
|
||||
>>> from inspect import signature
|
||||
>>> sig = signature(clip)
|
||||
>>> sig
|
||||
<Signature (text, max_len=80)>
|
||||
>>> str(sig)
|
||||
'(text, max_len=80)'
|
||||
>>> for name, param in sig.parameters.items():
|
||||
... print(param.kind, ':', name, '=', param.default)
|
||||
...
|
||||
POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
|
||||
POSITIONAL_OR_KEYWORD : max_len = 80
|
@ -1,67 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
from array import array
|
||||
from typing import Mapping, MutableSequence, Callable, Iterable, 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()
|
@ -1,51 +0,0 @@
|
||||
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()
|
@ -15,7 +15,7 @@ characters which contain that word in their names. For example::
|
||||
import sys
|
||||
import re
|
||||
import unicodedata
|
||||
from typing import Dict, Set, Iterator
|
||||
from collections.abc import Iterator
|
||||
|
||||
RE_WORD = re.compile(r'\w+')
|
||||
STOP_CODE = sys.maxunicode + 1
|
||||
@ -25,8 +25,8 @@ def tokenize(text: str) -> Iterator[str]: # <1>
|
||||
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>
|
||||
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):
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Tuple, Mapping
|
||||
from collections.abc import Mapping
|
||||
|
||||
NAMES = {
|
||||
'aqua': 65535,
|
||||
@ -19,7 +19,7 @@ NAMES = {
|
||||
'yellow': 16776960,
|
||||
}
|
||||
|
||||
def rgb2hex(color=Tuple[int, int, int]) -> str:
|
||||
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)
|
||||
@ -27,7 +27,7 @@ def rgb2hex(color=Tuple[int, int, int]) -> str:
|
||||
|
||||
HEX_ERROR = "Color must use format '#0099ff', got: {!r}"
|
||||
|
||||
def hex2rgb(color=str) -> Tuple[int, int, int]:
|
||||
def hex2rgb(color: str) -> tuple[int, int, int]:
|
||||
if len(color) != 7 or color[0] != '#':
|
||||
raise ValueError(HEX_ERROR.format(color))
|
||||
try:
|
||||
|
@ -1,7 +1,7 @@
|
||||
# tag::COLUMNIZE[]
|
||||
from typing import Sequence, List, Tuple
|
||||
from collections.abc import Sequence
|
||||
|
||||
def columnize(sequence: Sequence[str], num_columns: int = 0) -> List[Tuple[str, ...]]:
|
||||
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)
|
||||
|
@ -1,35 +0,0 @@
|
||||
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()
|
@ -1,26 +0,0 @@
|
||||
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()
|
@ -20,11 +20,14 @@ Example:
|
||||
"""
|
||||
|
||||
# tag::TOP[]
|
||||
from typing import TypeVar, Iterable, List
|
||||
from collections.abc import Iterable
|
||||
from typing import TypeVar
|
||||
|
||||
from comparable import SupportsLessThan
|
||||
|
||||
LT = TypeVar('LT', bound=SupportsLessThan)
|
||||
|
||||
def top(series: Iterable[LT], length: int) -> List[LT]:
|
||||
return sorted(series, reverse=True)[:length]
|
||||
def top(series: Iterable[LT], length: int) -> list[LT]:
|
||||
ordered = sorted(series, reverse=True)
|
||||
return ordered[:length]
|
||||
# end::TOP[]
|
||||
|
@ -1,6 +1,11 @@
|
||||
from typing import Tuple, List, Iterator, TYPE_CHECKING
|
||||
import pytest # type: ignore
|
||||
# tag::TOP_IMPORT[]
|
||||
from collections.abc import Iterator
|
||||
from typing import TYPE_CHECKING # <1>
|
||||
|
||||
import pytest
|
||||
|
||||
from top import top
|
||||
# end::TOP_IMPORT[]
|
||||
|
||||
@pytest.mark.parametrize('series, length, expected', [
|
||||
((1, 2, 3), 2, [3, 2]),
|
||||
@ -8,9 +13,9 @@ from top import top
|
||||
((3, 3, 3), 1, [3]),
|
||||
])
|
||||
def test_top(
|
||||
series: Tuple[float, ...],
|
||||
series: tuple[float, ...],
|
||||
length: int,
|
||||
expected: List[float],
|
||||
expected: list[float],
|
||||
) -> None:
|
||||
result = top(series, length)
|
||||
assert expected == result
|
||||
@ -18,13 +23,13 @@ def test_top(
|
||||
# tag::TOP_TEST[]
|
||||
def test_top_tuples() -> None:
|
||||
fruit = 'mango pear apple kiwi banana'.split()
|
||||
series: Iterator[Tuple[int, str]] = (
|
||||
series: Iterator[tuple[int, str]] = ( # <2>
|
||||
(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)
|
||||
if TYPE_CHECKING: # <3>
|
||||
reveal_type(series) # <4>
|
||||
reveal_type(expected)
|
||||
reveal_type(result)
|
||||
assert result == expected
|
||||
@ -34,7 +39,7 @@ 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)
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
top(series, 3) # <5>
|
||||
assert "'<' not supported" in str(excinfo.value)
|
||||
# end::TOP_TEST[]
|
||||
|
@ -8,10 +8,10 @@
|
||||
"""
|
||||
|
||||
# tag::GEOHASH[]
|
||||
from geolib import geohash as gh # type: ignore
|
||||
from geolib import geohash as gh # type: ignore # <1>
|
||||
|
||||
PRECISION = 9
|
||||
|
||||
def geohash(lat_lon: tuple[float, float]) -> str:
|
||||
def geohash(lat_lon: tuple[float, float]) -> str: # <2>
|
||||
return gh.encode(*lat_lon, PRECISION)
|
||||
# end::GEOHASH[]
|
||||
|
@ -9,7 +9,7 @@
|
||||
"""
|
||||
|
||||
# tag::GEOHASH[]
|
||||
from typing import Tuple, NamedTuple
|
||||
from typing import NamedTuple
|
||||
|
||||
from geolib import geohash as gh # type: ignore
|
||||
|
||||
@ -21,16 +21,21 @@ class Coordinate(NamedTuple):
|
||||
|
||||
def geohash(lat_lon: Coordinate) -> str:
|
||||
return gh.encode(*lat_lon, PRECISION)
|
||||
# end::GEOHASH[]
|
||||
|
||||
def display(lat_lon: Tuple[float, float]) -> str:
|
||||
# tag::DISPLAY[]
|
||||
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[]
|
||||
# end::DISPLAY[]
|
||||
|
||||
def demo():
|
||||
shanghai = 31.2304, 121.4737
|
||||
print(display(shanghai))
|
||||
s = geohash(shanghai)
|
||||
print(s)
|
||||
|
||||
if __name__ == '__main__':
|
||||
demo()
|
||||
|
@ -1,10 +0,0 @@
|
||||
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)
|
@ -5,16 +5,15 @@
|
||||
>>> show_count(1, 'bird')
|
||||
'1 bird'
|
||||
>>> show_count(0, 'bird')
|
||||
'no bird'
|
||||
'no birds'
|
||||
|
||||
# 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'
|
||||
if count == 1:
|
||||
return f'1 {word}'
|
||||
count_str = str(count) if count else 'no'
|
||||
return f'{count_str} {word}s'
|
||||
# end::SHOW_COUNT[]
|
||||
|
@ -7,11 +7,11 @@ from messages import show_count
|
||||
(1, '1 part'),
|
||||
(2, '2 parts'),
|
||||
])
|
||||
def test_show_count(qty, expected):
|
||||
def test_show_count(qty: int, expected: str) -> None:
|
||||
got = show_count(qty, 'part')
|
||||
assert got == expected
|
||||
|
||||
|
||||
def test_show_count_zero():
|
||||
got = show_count(0, 'part')
|
||||
assert got == 'no part'
|
||||
assert got == 'no parts'
|
||||
|
@ -4,21 +4,22 @@
|
||||
>>> show_count(1, 'bird')
|
||||
'1 bird'
|
||||
>>> show_count(0, 'bird')
|
||||
'no bird'
|
||||
'no birds'
|
||||
>>> show_count(3, 'virus', 'viruses')
|
||||
'3 viruses'
|
||||
>>> show_count(1, 'virus', 'viruses')
|
||||
'1 virus'
|
||||
>>> show_count(0, 'virus', 'viruses')
|
||||
'no viruses'
|
||||
"""
|
||||
|
||||
# tag::SHOW_COUNT[]
|
||||
def show_count(count: int, singular: str, plural: str = '') -> str:
|
||||
if count == 0:
|
||||
return f'no {singular}'
|
||||
elif count == 1:
|
||||
if count == 1:
|
||||
return f'1 {singular}'
|
||||
else:
|
||||
if plural:
|
||||
return f'{count} {plural}'
|
||||
else:
|
||||
return f'{count} {singular}s'
|
||||
count_str = str(count) if count else 'no'
|
||||
if not plural:
|
||||
plural = singular + 's'
|
||||
return f'{count_str} {plural}'
|
||||
|
||||
# end::SHOW_COUNT[]
|
||||
|
@ -1,4 +1,4 @@
|
||||
from pytest import mark # type: ignore
|
||||
from pytest import mark
|
||||
|
||||
from messages import show_count
|
||||
|
||||
@ -6,9 +6,9 @@ from messages import show_count
|
||||
@mark.parametrize('qty, expected', [
|
||||
(1, '1 part'),
|
||||
(2, '2 parts'),
|
||||
(0, 'no part'),
|
||||
(0, 'no parts'),
|
||||
])
|
||||
def test_show_count(qty, expected):
|
||||
def test_show_count(qty: int, expected: str) -> None:
|
||||
got = show_count(qty, 'part')
|
||||
assert got == expected
|
||||
|
||||
@ -17,9 +17,9 @@ def test_show_count(qty, expected):
|
||||
@mark.parametrize('qty, expected', [
|
||||
(1, '1 child'),
|
||||
(2, '2 children'),
|
||||
(0, 'no child'),
|
||||
(0, 'no children'),
|
||||
])
|
||||
def test_irregular(qty, expected) -> None:
|
||||
def test_irregular(qty: int, expected: str) -> None:
|
||||
got = show_count(qty, 'child', 'children')
|
||||
assert got == expected
|
||||
# end::TEST_IRREGULAR[]
|
||||
|
@ -5,16 +5,15 @@
|
||||
>>> show_count(1, 'bird')
|
||||
'1 bird'
|
||||
>>> show_count(0, 'bird')
|
||||
'no bird'
|
||||
'no birds'
|
||||
|
||||
# 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'
|
||||
if count == 1:
|
||||
return f'1 {word}'
|
||||
count_str = str(count) if count else 'no'
|
||||
return f'{count_str} {word}s'
|
||||
# end::SHOW_COUNT[]
|
||||
|
@ -12,4 +12,4 @@ def test_show_count(qty, expected):
|
||||
|
||||
def test_show_count_zero():
|
||||
got = show_count(0, 'part')
|
||||
assert got == 'no part'
|
||||
assert got == 'no parts'
|
||||
|
@ -1,26 +0,0 @@
|
||||
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 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()
|
@ -1,6 +1,6 @@
|
||||
# tag::MODE_FLOAT[]
|
||||
from collections import Counter
|
||||
from typing import Iterable
|
||||
from collections.abc import Iterable
|
||||
|
||||
def mode(data: Iterable[float]) -> float:
|
||||
pairs = Counter(data).most_common(1)
|
||||
@ -20,4 +20,4 @@ def demo() -> None:
|
||||
print(repr(m), type(m))
|
||||
|
||||
if __name__ == '__main__':
|
||||
demo()
|
||||
demo()
|
||||
|
@ -1,6 +1,7 @@
|
||||
# tag::MODE_HASHABLE_T[]
|
||||
from collections import Counter
|
||||
from typing import Iterable, Hashable, TypeVar
|
||||
from collections.abc import Iterable, Hashable
|
||||
from typing import TypeVar
|
||||
|
||||
HashableT = TypeVar('HashableT', bound=Hashable)
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
# 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()
|
@ -1,26 +0,0 @@
|
||||
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 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()
|
@ -1,6 +1,4 @@
|
||||
[mypy]
|
||||
python_version = 3.8
|
||||
python_version = 3.9
|
||||
warn_unused_configs = True
|
||||
disallow_incomplete_defs = True
|
||||
[mypy-pytest]
|
||||
ignore_missing_imports = True
|
@ -1,101 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""passdrill: typing drills for practicing passphrases
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from base64 import b64encode, b64decode
|
||||
from getpass import getpass
|
||||
from hashlib import scrypt
|
||||
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 in ('', '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)
|
||||
# "standard" exit status codes:
|
||||
# https://stackoverflow.com/questions/1101957/are-there-any-standard-exit-status-codes-in-linux/40484670#40484670
|
||||
sys.exit(74) # input/output error
|
||||
|
||||
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:
|
||||
print(f'\n{turn} turns. {correct / turn:.1%} 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(2) # command line usage error
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
@ -13,9 +13,9 @@
|
||||
"""
|
||||
|
||||
# tag::ZIP_REPLACE[]
|
||||
from typing import Iterable, Tuple
|
||||
from collections.abc import Iterable
|
||||
|
||||
FromTo = Tuple[str, str] # <1>
|
||||
FromTo = tuple[str, str] # <1>
|
||||
|
||||
def zip_replace(text: str, changes: Iterable[FromTo]) -> str: # <2>
|
||||
for from_, to in changes:
|
||||
|
@ -1,39 +0,0 @@
|
||||
"""
|
||||
``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()
|
@ -1,8 +0,0 @@
|
||||
from array import array
|
||||
from typing import MutableSequence
|
||||
|
||||
a = array('d')
|
||||
reveal_type(a)
|
||||
b: MutableSequence[float] = array('b')
|
||||
reveal_type(b)
|
||||
|
@ -1,10 +1,11 @@
|
||||
# tag::SAMPLE[]
|
||||
from collections.abc import Sequence
|
||||
from random import shuffle
|
||||
from typing import Sequence, List, TypeVar
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def sample(population: Sequence[T], size: int) -> List[T]:
|
||||
def sample(population: Sequence[T], size: int) -> list[T]:
|
||||
if size < 1:
|
||||
raise ValueError('size must be >= 1')
|
||||
result = list(population)
|
||||
|
11
08-def-type-hints/typevar_bounded.py
Normal file
11
08-def-type-hints/typevar_bounded.py
Normal file
@ -0,0 +1,11 @@
|
||||
from typing import TypeVar, TYPE_CHECKING
|
||||
|
||||
BT = TypeVar('BT', bound=float)
|
||||
|
||||
def triple2(a: BT) -> BT:
|
||||
return a * 3
|
||||
|
||||
res2 = triple2(2)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
reveal_type(res2)
|
@ -7,7 +7,7 @@ RT = TypeVar('RT', float, Decimal)
|
||||
def triple1(a: RT) -> RT:
|
||||
return a * 3
|
||||
|
||||
res1 = triple1(1, 2)
|
||||
res1 = triple1(2)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
reveal_type(res1)
|
||||
@ -19,7 +19,7 @@ BT = TypeVar('BT', bound=float)
|
||||
def triple2(a: BT) -> BT:
|
||||
return a * 3
|
||||
|
||||
res2 = triple2(1, 2)
|
||||
res2 = triple2(2)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
reveal_type(res2)
|
||||
|
@ -1,17 +1,14 @@
|
||||
import time
|
||||
from clockdeco import clock
|
||||
|
||||
from clockdeco0 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)
|
@ -6,20 +6,20 @@
|
||||
|
||||
>>> joe = Customer('John Doe', 0) # <1>
|
||||
>>> ann = Customer('Ann Smith', 1100)
|
||||
>>> cart = [LineItem('banana', 4, .5), # <2>
|
||||
... LineItem('apple', 10, 1.5),
|
||||
... LineItem('watermelon', 5, 5.0)]
|
||||
>>> cart = (LineItem('banana', 4, Decimal('.5')), # <2>
|
||||
... LineItem('apple', 10, Decimal('1.5')),
|
||||
... LineItem('watermelon', 5, Decimal(5)))
|
||||
>>> 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)]
|
||||
>>> banana_cart = (LineItem('banana', 30, Decimal('.5')), # <5>
|
||||
... LineItem('apple', 10, Decimal('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>
|
||||
>>> long_cart = tuple(LineItem(str(sku), 1, Decimal(1)) # <7>
|
||||
... for sku in range(10))
|
||||
>>> Order(joe, long_cart, LargeOrderPromo()) # <8>
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, cart, LargeOrderPromo())
|
||||
<Order total: 42.00 due: 42.00>
|
||||
@ -29,44 +29,37 @@
|
||||
# tag::CLASSIC_STRATEGY[]
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import typing
|
||||
from typing import Sequence, Optional
|
||||
from collections.abc import Sequence
|
||||
from decimal import Decimal
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
|
||||
class Customer(typing.NamedTuple):
|
||||
class Customer(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
|
||||
class LineItem(NamedTuple):
|
||||
product: str
|
||||
quantity: int
|
||||
price: Decimal
|
||||
|
||||
def total(self):
|
||||
def total(self) -> Decimal:
|
||||
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
|
||||
class Order(NamedTuple): # the Context
|
||||
customer: Customer
|
||||
cart: Sequence[LineItem]
|
||||
promotion: Optional['Promotion'] = None
|
||||
|
||||
def total(self) -> float:
|
||||
if not hasattr(self, '__total'):
|
||||
self.__total = sum(item.total() for item in self.cart)
|
||||
return self.__total
|
||||
def total(self) -> Decimal:
|
||||
totals = (item.total() for item in self.cart)
|
||||
return sum(totals, start=Decimal(0))
|
||||
|
||||
def due(self) -> float:
|
||||
def due(self) -> Decimal:
|
||||
if self.promotion is None:
|
||||
discount = 0.0
|
||||
discount = Decimal(0)
|
||||
else:
|
||||
discount = self.promotion.discount(self)
|
||||
return self.total() - discount
|
||||
@ -77,36 +70,37 @@ class Order: # the Context
|
||||
|
||||
class Promotion(ABC): # the Strategy: an abstract base class
|
||||
@abstractmethod
|
||||
def discount(self, order: Order) -> float:
|
||||
def discount(self, order: Order) -> Decimal:
|
||||
"""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
|
||||
def discount(self, order: Order) -> Decimal:
|
||||
rate = Decimal('0.05')
|
||||
if order.customer.fidelity >= 1000:
|
||||
return order.total() * rate
|
||||
return Decimal(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
|
||||
def discount(self, order: Order) -> Decimal:
|
||||
discount = Decimal(0)
|
||||
for item in order.cart:
|
||||
if item.quantity >= 20:
|
||||
discount += item.total() * 0.1
|
||||
discount += item.total() * Decimal('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:
|
||||
def discount(self, order: Order) -> Decimal:
|
||||
distinct_items = {item.product for item in order.cart}
|
||||
if len(distinct_items) >= 10:
|
||||
return order.total() * 0.07
|
||||
return 0
|
||||
|
||||
|
||||
return order.total() * Decimal('0.07')
|
||||
return Decimal(0)
|
||||
# end::CLASSIC_STRATEGY[]
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import List
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest # type: ignore
|
||||
|
||||
@ -17,47 +17,49 @@ def customer_fidelity_1100() -> Customer:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cart_plain() -> List[LineItem]:
|
||||
return [
|
||||
LineItem('banana', 4, 0.5),
|
||||
LineItem('apple', 10, 1.5),
|
||||
LineItem('watermelon', 5, 5.0),
|
||||
]
|
||||
def cart_plain() -> tuple[LineItem, ...]:
|
||||
return (
|
||||
LineItem('banana', 4, Decimal('0.5')),
|
||||
LineItem('apple', 10, Decimal('1.5')),
|
||||
LineItem('watermelon', 5, Decimal('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
|
||||
assert order.total() == 42
|
||||
assert order.due() == 42
|
||||
|
||||
|
||||
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
|
||||
assert order.total() == 42
|
||||
assert order.due() == Decimal('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
|
||||
assert order.total() == 42
|
||||
assert order.due() == 42
|
||||
|
||||
|
||||
def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None:
|
||||
cart = [LineItem('banana', 30, 0.5), LineItem('apple', 10, 1.5)]
|
||||
cart = [LineItem('banana', 30, Decimal('0.5')),
|
||||
LineItem('apple', 10, Decimal('1.5'))]
|
||||
order = Order(customer_fidelity_0, cart, BulkItemPromo())
|
||||
assert order.total() == 30.0
|
||||
assert order.due() == 28.5
|
||||
assert order.total() == 30
|
||||
assert order.due() == Decimal('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
|
||||
assert order.total() == 42
|
||||
assert order.due() == 42
|
||||
|
||||
|
||||
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)]
|
||||
cart = [LineItem(str(item_code), 1, Decimal(1))
|
||||
for item_code in range(10)]
|
||||
order = Order(customer_fidelity_0, cart, LargeOrderPromo())
|
||||
assert order.total() == 10.0
|
||||
assert order.due() == 9.3
|
||||
assert order.total() == 10
|
||||
assert order.due() == Decimal('9.3')
|
||||
|
@ -17,9 +17,9 @@
|
||||
... 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>
|
||||
>>> long_cart = [LineItem(str(item_code), 1, 1.0) # <7>
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, long_order, LargeOrderPromo()) # <8>
|
||||
>>> Order(joe, long_cart, LargeOrderPromo()) # <8>
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, cart, LargeOrderPromo())
|
||||
<Order total: 42.00 due: 42.00>
|
||||
|
@ -1,20 +1,25 @@
|
||||
def fidelity_promo(order):
|
||||
from decimal import Decimal
|
||||
from strategy import Order
|
||||
|
||||
def fidelity_promo(order: Order) -> Decimal: # <3>
|
||||
"""5% discount for customers with 1000 or more fidelity points"""
|
||||
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
|
||||
if order.customer.fidelity >= 1000:
|
||||
return order.total() * Decimal('0.05')
|
||||
return Decimal(0)
|
||||
|
||||
|
||||
def bulk_item_promo(order):
|
||||
def bulk_item_promo(order: Order) -> Decimal:
|
||||
"""10% discount for each LineItem with 20 or more units"""
|
||||
discount = 0
|
||||
discount = Decimal(0)
|
||||
for item in order.cart:
|
||||
if item.quantity >= 20:
|
||||
discount += item.total() * 0.1
|
||||
discount += item.total() * Decimal('0.1')
|
||||
return discount
|
||||
|
||||
|
||||
def large_order_promo(order):
|
||||
def large_order_promo(order: Order) -> Decimal:
|
||||
"""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
|
||||
return order.total() * Decimal('0.07')
|
||||
return Decimal(0)
|
||||
|
@ -17,9 +17,9 @@
|
||||
... 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>
|
||||
>>> long_cart = [LineItem(str(item_code), 1, 1.0) # <7>
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, long_order, LargeOrderPromo()) # <8>
|
||||
>>> Order(joe, long_cart, LargeOrderPromo()) # <8>
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, cart, LargeOrderPromo())
|
||||
<Order total: 42.00 due: 42.00>
|
||||
|
@ -1,13 +1,2 @@
|
||||
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.10.0
|
||||
pyparsing==2.4.6
|
||||
pytest==5.4.1
|
||||
six==1.14.0
|
||||
wcwidth==0.1.9
|
||||
mypy==0.910
|
||||
pytest==6.2.4
|
||||
|
@ -6,20 +6,20 @@
|
||||
|
||||
>>> joe = Customer('John Doe', 0) # <1>
|
||||
>>> ann = Customer('Ann Smith', 1100)
|
||||
>>> cart = [LineItem('banana', 4, .5),
|
||||
... LineItem('apple', 10, 1.5),
|
||||
... LineItem('watermelon', 5, 5.0)]
|
||||
>>> cart = [LineItem('banana', 4, Decimal('.5')),
|
||||
... LineItem('apple', 10, Decimal('1.5')),
|
||||
... LineItem('watermelon', 5, Decimal(5))]
|
||||
>>> 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)]
|
||||
>>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
|
||||
... LineItem('apple', 10, Decimal('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)
|
||||
>>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, big_cart, large_order_promo)
|
||||
>>> Order(joe, long_cart, large_order_promo)
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, cart, large_order_promo)
|
||||
<Order total: 42.00 due: 42.00>
|
||||
@ -28,75 +28,71 @@
|
||||
"""
|
||||
# tag::STRATEGY[]
|
||||
|
||||
import typing
|
||||
from typing import Sequence, Optional, Callable
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from typing import Optional, Callable, NamedTuple
|
||||
|
||||
|
||||
class Customer(typing.NamedTuple):
|
||||
class Customer(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
|
||||
class LineItem(NamedTuple):
|
||||
product: str
|
||||
quantity: int
|
||||
price: Decimal
|
||||
|
||||
def total(self):
|
||||
return self.price * self.quantity
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
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
|
||||
customer: Customer
|
||||
cart: Sequence[LineItem]
|
||||
promotion: Optional[Callable[['Order'], Decimal]] = None # <1>
|
||||
|
||||
def total(self) -> float:
|
||||
if not hasattr(self, '__total'):
|
||||
self.__total = sum(item.total() for item in self.cart)
|
||||
return self.__total
|
||||
def total(self) -> Decimal:
|
||||
totals = (item.total() for item in self.cart)
|
||||
return sum(totals, start=Decimal(0))
|
||||
|
||||
def due(self) -> float:
|
||||
def due(self) -> Decimal:
|
||||
if self.promotion is None:
|
||||
discount = 0.0
|
||||
discount = Decimal(0)
|
||||
else:
|
||||
discount = self.promotion(self) # <1>
|
||||
discount = self.promotion(self) # <2>
|
||||
return self.total() - discount
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
|
||||
|
||||
|
||||
# <2>
|
||||
# <3>
|
||||
|
||||
|
||||
def fidelity_promo(order: Order) -> float: # <3>
|
||||
def fidelity_promo(order: Order) -> Decimal: # <4>
|
||||
"""5% discount for customers with 1000 or more fidelity points"""
|
||||
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
|
||||
if order.customer.fidelity >= 1000:
|
||||
return order.total() * Decimal('0.05')
|
||||
return Decimal(0)
|
||||
|
||||
|
||||
def bulk_item_promo(order: Order):
|
||||
def bulk_item_promo(order: Order) -> Decimal:
|
||||
"""10% discount for each LineItem with 20 or more units"""
|
||||
discount = 0
|
||||
discount = Decimal(0)
|
||||
for item in order.cart:
|
||||
if item.quantity >= 20:
|
||||
discount += item.total() * 0.1
|
||||
discount += item.total() * Decimal('0.1')
|
||||
return discount
|
||||
|
||||
|
||||
def large_order_promo(order: Order):
|
||||
def large_order_promo(order: Order) -> Decimal:
|
||||
"""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
|
||||
return order.total() * Decimal('0.07')
|
||||
return Decimal(0)
|
||||
|
||||
|
||||
# end::STRATEGY[]
|
||||
|
@ -3,19 +3,20 @@
|
||||
# selecting best promotion from static list of functions
|
||||
|
||||
"""
|
||||
>>> from strategy import Customer, LineItem
|
||||
>>> joe = Customer('John Doe', 0)
|
||||
>>> ann = Customer('Ann Smith', 1100)
|
||||
>>> cart = [LineItem('banana', 4, .5),
|
||||
... LineItem('apple', 10, 1.5),
|
||||
... LineItem('watermelon', 5, 5.0)]
|
||||
>>> banana_cart = [LineItem('banana', 30, .5),
|
||||
... LineItem('apple', 10, 1.5)]
|
||||
>>> big_cart = [LineItem(str(item_code), 1, 1.0)
|
||||
>>> cart = [LineItem('banana', 4, Decimal('.5')),
|
||||
... LineItem('apple', 10, Decimal('1.5')),
|
||||
... LineItem('watermelon', 5, Decimal(5))]
|
||||
>>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
|
||||
... LineItem('apple', 10, Decimal('1.5'))]
|
||||
>>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
|
||||
... for item_code in range(10)]
|
||||
|
||||
# tag::STRATEGY_BEST_TESTS[]
|
||||
|
||||
>>> Order(joe, big_cart, best_promo) # <1>
|
||||
>>> Order(joe, long_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>
|
||||
@ -25,7 +26,9 @@
|
||||
# end::STRATEGY_BEST_TESTS[]
|
||||
"""
|
||||
|
||||
from strategy import Customer, LineItem, Order
|
||||
from decimal import Decimal
|
||||
|
||||
from strategy import Order
|
||||
from strategy import fidelity_promo, bulk_item_promo, large_order_promo
|
||||
|
||||
# tag::STRATEGY_BEST[]
|
||||
@ -33,9 +36,8 @@ from strategy import fidelity_promo, bulk_item_promo, large_order_promo
|
||||
promos = [fidelity_promo, bulk_item_promo, large_order_promo] # <1>
|
||||
|
||||
|
||||
def best_promo(order) -> float: # <2>
|
||||
"""Select best discount available
|
||||
"""
|
||||
def best_promo(order: Order) -> Decimal: # <2>
|
||||
"""Compute the best discount available"""
|
||||
return max(promo(order) for promo in promos) # <3>
|
||||
|
||||
|
||||
|
@ -3,29 +3,31 @@
|
||||
# selecting best promotion from current module globals
|
||||
|
||||
"""
|
||||
>>> from decimal import Decimal
|
||||
>>> from strategy import Customer, LineItem, Order
|
||||
>>> joe = Customer('John Doe', 0)
|
||||
>>> ann = Customer('Ann Smith', 1100)
|
||||
>>> cart = [LineItem('banana', 4, .5),
|
||||
... LineItem('apple', 10, 1.5),
|
||||
... LineItem('watermelon', 5, 5.0)]
|
||||
>>> cart = [LineItem('banana', 4, Decimal('.5')),
|
||||
... LineItem('apple', 10, Decimal('1.5')),
|
||||
... LineItem('watermelon', 5, Decimal(5))]
|
||||
>>> 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)]
|
||||
>>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
|
||||
... LineItem('apple', 10, Decimal('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)
|
||||
>>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, long_order, large_order_promo)
|
||||
>>> Order(joe, long_cart, 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(joe, long_cart, best_promo)
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, banana_cart, best_promo)
|
||||
<Order total: 30.00 due: 28.50>
|
||||
@ -35,78 +37,21 @@
|
||||
# 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):
|
||||
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
|
||||
|
||||
|
||||
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[]
|
||||
from decimal import Decimal
|
||||
from strategy import Order
|
||||
from strategy import (
|
||||
fidelity_promo, bulk_item_promo, large_order_promo # <1>
|
||||
)
|
||||
|
||||
promos = [
|
||||
globals()[name]
|
||||
for name in globals() # <1>
|
||||
if name.endswith('_promo') and name != 'best_promo' # <2>
|
||||
] # <3>
|
||||
promos = [promo for name, promo in globals().items() # <2>
|
||||
if name.endswith('_promo') and # <3>
|
||||
name != 'best_promo' # <4>
|
||||
]
|
||||
|
||||
|
||||
def best_promo(order):
|
||||
"""Select best discount available
|
||||
"""
|
||||
return max(promo(order) for promo in promos) # <4>
|
||||
|
||||
def best_promo(order: Order) -> Decimal: # <5>
|
||||
"""Compute the best discount available"""
|
||||
return max(promo(order) for promo in promos)
|
||||
|
||||
# end::STRATEGY_BEST2[]
|
||||
|
@ -3,30 +3,32 @@
|
||||
# selecting best promotion from imported module
|
||||
|
||||
"""
|
||||
>>> from decimal import Decimal
|
||||
>>> from strategy import Customer, LineItem, Order
|
||||
>>> from promotions import *
|
||||
>>> joe = Customer('John Doe', 0)
|
||||
>>> ann = Customer('Ann Smith', 1100)
|
||||
>>> cart = [LineItem('banana', 4, .5),
|
||||
... LineItem('apple', 10, 1.5),
|
||||
... LineItem('watermelon', 5, 5.0)]
|
||||
>>> cart = [LineItem('banana', 4, Decimal('.5')),
|
||||
... LineItem('apple', 10, Decimal('1.5')),
|
||||
... LineItem('watermelon', 5, Decimal(5))]
|
||||
>>> 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)]
|
||||
>>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
|
||||
... LineItem('apple', 10, Decimal('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)
|
||||
>>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, long_order, large_order_promo)
|
||||
>>> Order(joe, long_cart, 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(joe, long_cart, best_promo)
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, banana_cart, best_promo)
|
||||
<Order total: 30.00 due: 28.50>
|
||||
@ -36,55 +38,20 @@
|
||||
# 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):
|
||||
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
|
||||
|
||||
|
||||
# tag::STRATEGY_BEST3[]
|
||||
|
||||
promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)]
|
||||
from decimal import Decimal
|
||||
import inspect
|
||||
|
||||
from strategy import Order
|
||||
import promotions
|
||||
|
||||
|
||||
def best_promo(order):
|
||||
"""Select best discount available
|
||||
"""
|
||||
promos = [func for _, func in inspect.getmembers(promotions, inspect.isfunction)]
|
||||
|
||||
|
||||
def best_promo(order: Order) -> Decimal:
|
||||
"""Compute the best discount available"""
|
||||
return max(promo(order) for promo in promos)
|
||||
|
||||
|
||||
# end::STRATEGY_BEST3[]
|
||||
|
@ -4,29 +4,32 @@
|
||||
# registered by a decorator
|
||||
|
||||
"""
|
||||
>>> from decimal import Decimal
|
||||
>>> from strategy import Customer, LineItem, Order
|
||||
>>> from promotions import *
|
||||
>>> joe = Customer('John Doe', 0)
|
||||
>>> ann = Customer('Ann Smith', 1100)
|
||||
>>> cart = [LineItem('banana', 4, .5),
|
||||
... LineItem('apple', 10, 1.5),
|
||||
... LineItem('watermelon', 5, 5.0)]
|
||||
>>> Order(joe, cart, fidelity)
|
||||
>>> cart = [LineItem('banana', 4, Decimal('.5')),
|
||||
... LineItem('apple', 10, Decimal('1.5')),
|
||||
... LineItem('watermelon', 5, Decimal(5))]
|
||||
>>> Order(joe, cart, fidelity_promo)
|
||||
<Order total: 42.00 due: 42.00>
|
||||
>>> Order(ann, cart, fidelity)
|
||||
>>> 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)
|
||||
>>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
|
||||
... LineItem('apple', 10, Decimal('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)
|
||||
>>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, long_order, large_order)
|
||||
>>> Order(joe, long_cart, large_order_promo)
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, cart, large_order)
|
||||
>>> Order(joe, cart, large_order_promo)
|
||||
<Order total: 42.00 due: 42.00>
|
||||
|
||||
# tag::STRATEGY_BEST_TESTS[]
|
||||
|
||||
>>> Order(joe, long_order, best_promo)
|
||||
>>> Order(joe, long_cart, best_promo)
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, banana_cart, best_promo)
|
||||
<Order total: 30.00 due: 28.50>
|
||||
@ -36,47 +39,16 @@
|
||||
# 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):
|
||||
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
|
||||
from decimal import Decimal
|
||||
from typing import Callable
|
||||
|
||||
from strategy import Order
|
||||
|
||||
# tag::STRATEGY_BEST4[]
|
||||
|
||||
Promotion = Callable[[Order], float] # <2>
|
||||
Promotion = Callable[[Order], Decimal]
|
||||
|
||||
promos: list[Promotion] = [] # <1>
|
||||
|
||||
|
||||
def promotion(promo: Promotion) -> Promotion: # <2>
|
||||
@ -84,38 +56,35 @@ def promotion(promo: Promotion) -> Promotion: # <2>
|
||||
return promo
|
||||
|
||||
|
||||
promos: List[Promotion] = [] # <1>
|
||||
def best_promo(order: Order) -> Decimal:
|
||||
"""Compute the best discount available"""
|
||||
return max(promo(order) for promo in promos) # <3>
|
||||
|
||||
|
||||
@promotion # <3>
|
||||
def fidelity(order: Order) -> float:
|
||||
@promotion # <4>
|
||||
def fidelity(order: Order) -> Decimal:
|
||||
"""5% discount for customers with 1000 or more fidelity points"""
|
||||
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
|
||||
if order.customer.fidelity >= 1000:
|
||||
return order.total() * Decimal('0.05')
|
||||
return Decimal(0)
|
||||
|
||||
|
||||
@promotion
|
||||
def bulk_item(order: Order) -> float:
|
||||
def bulk_item(order: Order) -> Decimal:
|
||||
"""10% discount for each LineItem with 20 or more units"""
|
||||
discount = 0
|
||||
discount = Decimal(0)
|
||||
for item in order.cart:
|
||||
if item.quantity >= 20:
|
||||
discount += item.total() * 0.1
|
||||
discount += item.total() * Decimal('0.1')
|
||||
return discount
|
||||
|
||||
|
||||
@promotion
|
||||
def large_order(order: Order) -> float:
|
||||
def large_order(order: Order) -> Decimal:
|
||||
"""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)
|
||||
|
||||
return order.total() * Decimal('0.07')
|
||||
return Decimal(0)
|
||||
|
||||
# end::STRATEGY_BEST4[]
|
||||
|
@ -15,9 +15,9 @@
|
||||
... 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)
|
||||
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, big_cart, LargeOrderPromo(7))
|
||||
>>> Order(joe, long_cart, LargeOrderPromo(7))
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, cart, LargeOrderPromo(7))
|
||||
<Order total: 42.00 due: 42.00>
|
||||
|
@ -47,7 +47,7 @@ def test_large_order_promo_with_discount(customer_fidelity_0) -> None:
|
||||
assert order.due() == 9.3
|
||||
|
||||
|
||||
def test_general_discount(customer_fidelity_0, cart_plain) -> None:
|
||||
def test_general_discount(customer_fidelity_1100, cart_plain) -> None:
|
||||
general_promo: Promotion = functools.partial(general_discount, 5)
|
||||
order = Order(customer_fidelity_1100, cart_plain, general_promo)
|
||||
assert order.total() == 42.0
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import List
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest # type: ignore
|
||||
|
||||
@ -17,47 +17,50 @@ def customer_fidelity_1100() -> Customer:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cart_plain() -> List[LineItem]:
|
||||
return [
|
||||
LineItem('banana', 4, 0.5),
|
||||
LineItem('apple', 10, 1.5),
|
||||
LineItem('watermelon', 5, 5.0),
|
||||
]
|
||||
def cart_plain() -> tuple[LineItem, ...]:
|
||||
return (
|
||||
LineItem('banana', 4, Decimal('0.5')),
|
||||
LineItem('apple', 10, Decimal('1.5')),
|
||||
LineItem('watermelon', 5, Decimal('5.0')),
|
||||
)
|
||||
|
||||
|
||||
def test_fidelity_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
||||
order = Order(customer_fidelity_0, cart_plain, fidelity_promo)
|
||||
assert order.total() == 42.0
|
||||
assert order.due() == 42.0
|
||||
assert order.total() == 42
|
||||
assert order.due() == 42
|
||||
|
||||
|
||||
def test_fidelity_promo_with_discount(customer_fidelity_1100, cart_plain) -> None:
|
||||
order = Order(customer_fidelity_1100, cart_plain, fidelity_promo)
|
||||
assert order.total() == 42.0
|
||||
assert order.due() == 39.9
|
||||
assert order.total() == 42
|
||||
assert order.due() == Decimal('39.9')
|
||||
|
||||
|
||||
def test_bulk_item_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
||||
order = Order(customer_fidelity_0, cart_plain, bulk_item_promo)
|
||||
assert order.total() == 42.0
|
||||
assert order.due() == 42.0
|
||||
assert order.total() == 42
|
||||
assert order.due() == 42
|
||||
|
||||
|
||||
def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None:
|
||||
cart = [LineItem('banana', 30, 0.5), LineItem('apple', 10, 1.5)]
|
||||
cart = [LineItem('banana', 30, Decimal('0.5')),
|
||||
LineItem('apple', 10, Decimal('1.5'))]
|
||||
order = Order(customer_fidelity_0, cart, bulk_item_promo)
|
||||
assert order.total() == 30.0
|
||||
assert order.due() == 28.5
|
||||
assert order.total() == 30
|
||||
assert order.due() == Decimal('28.5')
|
||||
|
||||
|
||||
def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
||||
order = Order(customer_fidelity_0, cart_plain, large_order_promo)
|
||||
assert order.total() == 42.0
|
||||
assert order.due() == 42.0
|
||||
assert order.total() == 42
|
||||
assert order.due() == 42
|
||||
|
||||
|
||||
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)]
|
||||
|
||||
cart = [LineItem(str(item_code), 1, Decimal(1))
|
||||
for item_code in range(10)]
|
||||
order = Order(customer_fidelity_0, cart, large_order_promo)
|
||||
assert order.total() == 10.0
|
||||
assert order.due() == 9.3
|
||||
assert order.total() == 10
|
||||
assert order.due() == Decimal('9.3')
|
||||
|
@ -17,9 +17,9 @@
|
||||
... 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>
|
||||
>>> long_cart = [LineItem(str(item_code), 1, 1.0) # <7>
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, long_order, LargeOrderPromo()) # <8>
|
||||
>>> Order(joe, long_cart, LargeOrderPromo()) # <8>
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, cart, LargeOrderPromo())
|
||||
<Order total: 42.00 due: 42.00>
|
||||
|
@ -17,9 +17,9 @@
|
||||
... LineItem('apple', 10, 1.5)]
|
||||
>>> Order(joe, banana_cart, bulk_item_promo) # <3>
|
||||
<Order total: 30.00 due: 28.50>
|
||||
>>> long_order = [LineItem(str(item_code), 1, 1.0)
|
||||
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, long_order, large_order_promo)
|
||||
>>> Order(joe, long_cart, large_order_promo)
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, cart, large_order_promo)
|
||||
<Order total: 42.00 due: 42.00>
|
||||
|
@ -16,16 +16,16 @@
|
||||
... 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)
|
||||
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, long_order, large_order_promo)
|
||||
>>> Order(joe, long_cart, 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) # <1>
|
||||
>>> Order(joe, long_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>
|
||||
|
@ -16,16 +16,16 @@
|
||||
... 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)
|
||||
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, long_order, large_order_promo)
|
||||
>>> Order(joe, long_cart, 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(joe, long_cart, best_promo)
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, banana_cart, best_promo)
|
||||
<Order total: 30.00 due: 28.50>
|
||||
|
@ -17,16 +17,16 @@
|
||||
... 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)
|
||||
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, long_order, large_order_promo)
|
||||
>>> Order(joe, long_cart, 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(joe, long_cart, best_promo)
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, banana_cart, best_promo)
|
||||
<Order total: 30.00 due: 28.50>
|
||||
|
@ -17,16 +17,16 @@
|
||||
... 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)
|
||||
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, long_order, large_order)
|
||||
>>> Order(joe, long_cart, 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(joe, long_cart, best_promo)
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, banana_cart, best_promo)
|
||||
<Order total: 30.00 due: 28.50>
|
||||
|
@ -15,9 +15,9 @@
|
||||
... LineItem('apple', 10, 1.5)]
|
||||
>>> Order(joe, banana_cart, bulk_item_promo(10))
|
||||
<Order total: 30.00 due: 28.50>
|
||||
>>> long_order = [LineItem(str(item_code), 1, 1.0)
|
||||
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, long_order, large_order_promo(7))
|
||||
>>> Order(joe, long_cart, large_order_promo(7))
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, cart, large_order_promo(7))
|
||||
<Order total: 42.00 due: 42.00>
|
||||
|
@ -15,9 +15,9 @@
|
||||
... LineItem('apple', 10, 1.5)]
|
||||
>>> Order(joe, banana_cart, BulkItemPromo(10))
|
||||
<Order total: 30.00 due: 28.50>
|
||||
>>> long_order = [LineItem(str(item_code), 1, 1.0)
|
||||
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||
... for item_code in range(10)]
|
||||
>>> Order(joe, long_order, LargeOrderPromo(7))
|
||||
>>> Order(joe, long_cart, LargeOrderPromo(7))
|
||||
<Order total: 10.00 due: 9.30>
|
||||
>>> Order(joe, cart, LargeOrderPromo(7))
|
||||
<Order total: 42.00 due: 42.00>
|
||||
|
@ -4,20 +4,25 @@ import resource
|
||||
|
||||
NUM_VECTORS = 10**7
|
||||
|
||||
module = None
|
||||
if len(sys.argv) == 2:
|
||||
module_name = sys.argv[1].replace('.py', '')
|
||||
module = importlib.import_module(module_name)
|
||||
else:
|
||||
print(f'Usage: {sys.argv[0]} <vector-module-to-test>')
|
||||
sys.exit(2) # command line usage error
|
||||
|
||||
fmt = 'Selected Vector2d type: {.__name__}.{.__name__}'
|
||||
print(fmt.format(module, module.Vector2d))
|
||||
if module is None:
|
||||
print('Running test with built-in `complex`')
|
||||
cls = complex
|
||||
else:
|
||||
fmt = 'Selected Vector2d type: {.__name__}.{.__name__}'
|
||||
print(fmt.format(module, module.Vector2d))
|
||||
cls = module.Vector2d
|
||||
|
||||
mem_init = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
||||
print(f'Creating {NUM_VECTORS:,} Vector2d instances')
|
||||
print(f'Creating {NUM_VECTORS:,} {cls.__qualname__!r} instances')
|
||||
|
||||
vectors = [module.Vector2d(3.0, 4.0) for i in range(NUM_VECTORS)]
|
||||
vectors = [cls(3.0, 4.0) for i in range(NUM_VECTORS)]
|
||||
|
||||
mem_final = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
||||
print(f'Initial RAM usage: {mem_init:14,}')
|
||||
|
53
11-pythonic-obj/patterns.py
Normal file
53
11-pythonic-obj/patterns.py
Normal file
@ -0,0 +1,53 @@
|
||||
from vector2d_v3 import Vector2d
|
||||
|
||||
# tag::KEYWORD_PATTERNS[]
|
||||
def keyword_pattern_demo(v: Vector2d) -> None:
|
||||
match v:
|
||||
case Vector2d(x=0, y=0):
|
||||
print(f'{v!r} is null')
|
||||
case Vector2d(x=0):
|
||||
print(f'{v!r} is vertical')
|
||||
case Vector2d(y=0):
|
||||
print(f'{v!r} is horizontal')
|
||||
case Vector2d(x=x, y=y) if x==y:
|
||||
print(f'{v!r} is diagonal')
|
||||
case _:
|
||||
print(f'{v!r} is awesome')
|
||||
# end::KEYWORD_PATTERNS[]
|
||||
|
||||
# tag::POSITIONAL_PATTERNS[]
|
||||
def positional_pattern_demo(v: Vector2d) -> None:
|
||||
match v:
|
||||
case Vector2d(0, 0):
|
||||
print(f'{v!r} is null')
|
||||
case Vector2d(0):
|
||||
print(f'{v!r} is vertical')
|
||||
case Vector2d(_, 0):
|
||||
print(f'{v!r} is horizontal')
|
||||
case Vector2d(x, y) if x==y:
|
||||
print(f'{v!r} is diagonal')
|
||||
case _:
|
||||
print(f'{v!r} is awesome')
|
||||
# end::POSITIONAL_PATTERNS[]
|
||||
|
||||
|
||||
def main():
|
||||
vectors = (
|
||||
Vector2d(1, 1),
|
||||
Vector2d(0, 1),
|
||||
Vector2d(1, 0),
|
||||
Vector2d(1, 2),
|
||||
Vector2d(0, 0),
|
||||
)
|
||||
print('KEYWORD PATTERNS:')
|
||||
for vector in vectors:
|
||||
keyword_pattern_demo(vector)
|
||||
|
||||
print('POSITIONAL PATTERNS:')
|
||||
for vector in vectors:
|
||||
positional_pattern_demo(vector)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
52
11-pythonic-obj/slots.rst
Normal file
52
11-pythonic-obj/slots.rst
Normal file
@ -0,0 +1,52 @@
|
||||
# tag::PIXEL[]
|
||||
>>> class Pixel:
|
||||
... __slots__ = ('x', 'y') # <1>
|
||||
...
|
||||
>>> p = Pixel() # <2>
|
||||
>>> p.__dict__ # <3>
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: 'Pixel' object has no attribute '__dict__'
|
||||
>>> p.x = 10 # <4>
|
||||
>>> p.y = 20
|
||||
>>> p.color = 'red' # <5>
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: 'Pixel' object has no attribute 'color'
|
||||
|
||||
# end::PIXEL[]
|
||||
|
||||
# tag::OPEN_PIXEL[]
|
||||
>>> class OpenPixel(Pixel): # <1>
|
||||
... pass
|
||||
...
|
||||
>>> op = OpenPixel()
|
||||
>>> op.__dict__ # <2>
|
||||
{}
|
||||
>>> op.x = 8 # <3>
|
||||
>>> op.__dict__ # <4>
|
||||
{}
|
||||
>>> op.x # <5>
|
||||
8
|
||||
>>> op.color = 'green' # <6>
|
||||
>>> op.__dict__ # <7>
|
||||
{'color': 'green'}
|
||||
|
||||
# end::OPEN_PIXEL[]
|
||||
|
||||
# tag::COLOR_PIXEL[]
|
||||
>>> class ColorPixel(Pixel):
|
||||
... __slots__ = ('color',) # <1>
|
||||
>>> cp = ColorPixel()
|
||||
>>> cp.__dict__ # <2>
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: 'ColorPixel' object has no attribute '__dict__'
|
||||
>>> cp.x = 2
|
||||
>>> cp.color = 'blue' # <3>
|
||||
>>> cp.flavor = 'banana'
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: 'ColorPixel' object has no attribute 'flavor'
|
||||
|
||||
# end::COLOR_PIXEL[]
|
@ -72,7 +72,7 @@ Tests of `x` and `y` read-only properties:
|
||||
>>> v1.x = 123
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: can't set attribute
|
||||
AttributeError: can't set attribute 'x'
|
||||
|
||||
|
||||
Tests of hashing:
|
||||
@ -90,6 +90,8 @@ from array import array
|
||||
import math
|
||||
|
||||
class Vector2d:
|
||||
__match_args__ = ('x', 'y')
|
||||
|
||||
typecode = 'd'
|
||||
|
||||
def __init__(self, x, y):
|
||||
|
@ -72,7 +72,7 @@ Tests of `x` and `y` read-only properties:
|
||||
>>> v1.x = 123
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: can't set attribute
|
||||
AttributeError: can't set attribute 'x'
|
||||
|
||||
# end::VECTOR2D_V3_HASH_DEMO[]
|
||||
|
||||
@ -112,7 +112,7 @@ class Vector2d:
|
||||
def __iter__(self):
|
||||
return (i for i in (self.x, self.y)) # <6>
|
||||
|
||||
# remaining methods follow (omitted in book listing)
|
||||
# remaining methods: same as previous Vector2d
|
||||
# end::VECTOR2D_V3_PROP[]
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -72,7 +72,7 @@ Tests of `x` and `y` read-only properties:
|
||||
>>> v1.x = 123
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: can't set attribute
|
||||
AttributeError: can't set attribute 'x'
|
||||
|
||||
Tests of hashing:
|
||||
|
||||
@ -90,11 +90,10 @@ import math
|
||||
|
||||
# tag::VECTOR2D_V3_SLOTS[]
|
||||
class Vector2d:
|
||||
__slots__ = ('__x', '__y')
|
||||
__match_args__ = ('x', 'y') # <1>
|
||||
__slots__ = ('__x', '__y') # <2>
|
||||
|
||||
typecode = 'd'
|
||||
|
||||
# methods follow (omitted in book listing)
|
||||
# end::VECTOR2D_V3_SLOTS[]
|
||||
|
||||
def __init__(self, x, y):
|
||||
|
@ -199,15 +199,17 @@ class Vector:
|
||||
return self._components[index]
|
||||
|
||||
# tag::VECTOR_V3_GETATTR[]
|
||||
shortcut_names = 'xyzt'
|
||||
__match_args__ = ('x', 'y', 'z', 't') # <1>
|
||||
|
||||
def __getattr__(self, name):
|
||||
cls = type(self) # <1>
|
||||
if len(name) == 1: # <2>
|
||||
pos = cls.shortcut_names.find(name) # <3>
|
||||
if 0 <= pos < len(self._components): # <4>
|
||||
return self._components[pos]
|
||||
msg = f'{cls.__name__!r} object has no attribute {name!r}' # <5>
|
||||
cls = type(self) # <2>
|
||||
try:
|
||||
pos = cls.__match_args__.index(name) # <3>
|
||||
except ValueError: # <4>
|
||||
pos = -1
|
||||
if 0 <= pos < len(self._components): # <5>
|
||||
return self._components[pos]
|
||||
msg = f'{cls.__name__!r} object has no attribute {name!r}' # <6>
|
||||
raise AttributeError(msg)
|
||||
# end::VECTOR_V3_GETATTR[]
|
||||
|
||||
@ -215,8 +217,8 @@ class Vector:
|
||||
def __setattr__(self, name, value):
|
||||
cls = type(self)
|
||||
if len(name) == 1: # <1>
|
||||
if name in cls.shortcut_names: # <2>
|
||||
error = 'read-only attribute {attr_name!r}'
|
||||
if name in cls.__match_args__: # <2>
|
||||
error = 'readonly attribute {attr_name!r}'
|
||||
elif name.islower(): # <3>
|
||||
error = "can't set attributes 'a' to 'z' in {cls_name!r}"
|
||||
else:
|
||||
|
@ -199,14 +199,16 @@ class Vector:
|
||||
index = operator.index(key)
|
||||
return self._components[index]
|
||||
|
||||
shortcut_names = 'xyzt'
|
||||
__match_args__ = ('x', 'y', 'z', 't')
|
||||
|
||||
def __getattr__(self, name):
|
||||
cls = type(self)
|
||||
if len(name) == 1:
|
||||
pos = cls.shortcut_names.find(name)
|
||||
if 0 <= pos < len(self._components):
|
||||
return self._components[pos]
|
||||
try:
|
||||
pos = cls.__match_args__.index(name)
|
||||
except ValueError:
|
||||
pos = -1
|
||||
if 0 <= pos < len(self._components):
|
||||
return self._components[pos]
|
||||
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||
raise AttributeError(msg)
|
||||
|
||||
|
@ -242,14 +242,16 @@ class Vector:
|
||||
index = operator.index(key)
|
||||
return self._components[index]
|
||||
|
||||
shortcut_names = 'xyzt'
|
||||
__match_args__ = ('x', 'y', 'z', 't')
|
||||
|
||||
def __getattr__(self, name):
|
||||
cls = type(self)
|
||||
if len(name) == 1:
|
||||
pos = cls.shortcut_names.find(name)
|
||||
if 0 <= pos < len(self._components):
|
||||
return self._components[pos]
|
||||
try:
|
||||
pos = cls.__match_args__.index(name)
|
||||
except ValueError:
|
||||
pos = -1
|
||||
if 0 <= pos < len(self._components):
|
||||
return self._components[pos]
|
||||
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||
raise AttributeError(msg)
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import collections
|
||||
from collections import namedtuple, abc
|
||||
|
||||
Card = collections.namedtuple('Card', ['rank', 'suit'])
|
||||
Card = namedtuple('Card', ['rank', 'suit'])
|
||||
|
||||
class FrenchDeck2(collections.MutableSequence):
|
||||
class FrenchDeck2(abc.MutableSequence):
|
||||
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
|
||||
suits = 'spades diamonds clubs hearts'.split()
|
||||
|
||||
|
@ -12,14 +12,14 @@ class SimplePicker: # <2>
|
||||
return self._items.pop()
|
||||
|
||||
def test_isinstance() -> None: # <4>
|
||||
popper = SimplePicker([1])
|
||||
assert isinstance(popper, RandomPicker)
|
||||
popper: RandomPicker = SimplePicker([1]) # <5>
|
||||
assert isinstance(popper, RandomPicker) # <6>
|
||||
|
||||
def test_item_type() -> None: # <5>
|
||||
def test_item_type() -> None: # <7>
|
||||
items = [1, 2]
|
||||
popper = SimplePicker(items)
|
||||
item = popper.pick()
|
||||
assert item in items
|
||||
if TYPE_CHECKING:
|
||||
reveal_type(item) # <6>
|
||||
reveal_type(item) # <8>
|
||||
assert isinstance(item, int)
|
||||
|
@ -168,5 +168,5 @@ class Vector2d:
|
||||
|
||||
@classmethod
|
||||
def fromcomplex(cls, datum):
|
||||
return Vector2d(datum.real, datum.imag) # <1>
|
||||
return cls(datum.real, datum.imag) # <1>
|
||||
# end::VECTOR2D_V4_COMPLEX[]
|
||||
|
@ -170,5 +170,5 @@ class Vector2d:
|
||||
@classmethod
|
||||
def fromcomplex(cls, datum: SupportsComplex) -> Vector2d: # <3>
|
||||
c = complex(datum) # <4>
|
||||
return Vector2d(c.real, c.imag)
|
||||
return cls(c.real, c.imag)
|
||||
# end::VECTOR2D_V5_COMPLEX[]
|
||||
|
@ -1,10 +1,39 @@
|
||||
"""UpperDict uppercases all string keys.
|
||||
"""
|
||||
Short demos
|
||||
===========
|
||||
|
||||
Test for initializer. `str` keys are uppercased::
|
||||
``UpperDict`` behaves like a case-insensitive mapping`::
|
||||
|
||||
# tag::UPPERDICT_DEMO[]
|
||||
>>> d = UpperDict([('a', 'letter A'), (2, 'digit two')])
|
||||
>>> list(d.keys())
|
||||
['A', 2]
|
||||
>>> d['b'] = 'letter B'
|
||||
>>> 'b' in d
|
||||
True
|
||||
>>> d['a'], d.get('B')
|
||||
('letter A', 'letter B')
|
||||
>>> list(d.keys())
|
||||
['A', 2, 'B']
|
||||
|
||||
# end::UPPERDICT_DEMO[]
|
||||
|
||||
And ``UpperCounter`` is also case-insensitive::
|
||||
|
||||
# tag::UPPERCOUNTER_DEMO[]
|
||||
>>> c = UpperCounter('BaNanA')
|
||||
>>> c.most_common()
|
||||
[('A', 3), ('N', 2), ('B', 1)]
|
||||
|
||||
# end::UPPERCOUNTER_DEMO[]
|
||||
|
||||
Detailed tests
|
||||
==============
|
||||
|
||||
UpperDict uppercases all string keys.
|
||||
|
||||
>>> d = UpperDict([('a', 'letter A'), ('B', 'letter B'), (2, 'digit two')])
|
||||
>>> list(d.keys())
|
||||
['A', 'B', 2]
|
||||
|
||||
|
||||
Tests for item retrieval using `d[key]` notation::
|
||||
|
||||
@ -82,14 +111,12 @@ Tests for count retrieval using `d[key]` notation::
|
||||
# tag::UPPERCASE_MIXIN[]
|
||||
import collections
|
||||
|
||||
|
||||
def _upper(key): # <1>
|
||||
try:
|
||||
return key.upper()
|
||||
except AttributeError:
|
||||
return key
|
||||
|
||||
|
||||
class UpperCaseMixin: # <2>
|
||||
def __setitem__(self, key, item):
|
||||
super().__setitem__(_upper(key), item)
|
||||
@ -108,8 +135,6 @@ class UpperCaseMixin: # <2>
|
||||
class UpperDict(UpperCaseMixin, collections.UserDict): # <1>
|
||||
pass
|
||||
|
||||
|
||||
class UpperCounter(UpperCaseMixin, collections.Counter): # <2>
|
||||
"""Specialized 'Counter' that uppercases string keys""" # <3>
|
||||
|
||||
# end::UPPERDICT[]
|
||||
|
@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# tag::ABS_DEMO[]
|
||||
import math
|
||||
from typing import NamedTuple, SupportsAbs
|
||||
|
||||
@ -30,3 +31,4 @@ assert is_unit(v3)
|
||||
assert is_unit(v4)
|
||||
|
||||
print('OK')
|
||||
# end::ABS_DEMO[]
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
A 2-dimensional vector class
|
||||
A two-dimensional vector class
|
||||
|
||||
>>> v1 = Vector2d(3, 4)
|
||||
>>> print(v1.x, v1.y)
|
||||
@ -72,7 +72,7 @@ Tests of `x` and `y` read-only properties:
|
||||
>>> v1.x = 123
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: can't set attribute
|
||||
AttributeError: can't set attribute 'x'
|
||||
|
||||
|
||||
Tests of hashing:
|
||||
@ -81,7 +81,7 @@ Tests of hashing:
|
||||
>>> v2 = Vector2d(3.1, 4.2)
|
||||
>>> hash(v1), hash(v2)
|
||||
(7, 384307168202284039)
|
||||
>>> len(set([v1, v2]))
|
||||
>>> len({v1, v2})
|
||||
2
|
||||
|
||||
"""
|
||||
@ -90,6 +90,8 @@ from array import array
|
||||
import math
|
||||
|
||||
class Vector2d:
|
||||
__match_args__ = ('x', 'y')
|
||||
|
||||
typecode = 'd'
|
||||
|
||||
def __init__(self, x, y):
|
||||
|
@ -305,14 +305,16 @@ class Vector:
|
||||
index = operator.index(key)
|
||||
return self._components[index]
|
||||
|
||||
shortcut_names = 'xyzt'
|
||||
__match_args__ = ('x', 'y', 'z', 't')
|
||||
|
||||
def __getattr__(self, name):
|
||||
cls = type(self)
|
||||
if len(name) == 1:
|
||||
pos = cls.shortcut_names.find(name)
|
||||
if 0 <= pos < len(self._components):
|
||||
return self._components[pos]
|
||||
try:
|
||||
pos = cls.__match_args__.index(name)
|
||||
except ValueError:
|
||||
pos = -1
|
||||
if 0 <= pos < len(self._components):
|
||||
return self._components[pos]
|
||||
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||
raise AttributeError(msg)
|
||||
|
||||
|
@ -355,14 +355,16 @@ class Vector:
|
||||
index = operator.index(key)
|
||||
return self._components[index]
|
||||
|
||||
shortcut_names = 'xyzt'
|
||||
__match_args__ = ('x', 'y', 'z', 't')
|
||||
|
||||
def __getattr__(self, name):
|
||||
cls = type(self)
|
||||
if len(name) == 1:
|
||||
pos = cls.shortcut_names.find(name)
|
||||
if 0 <= pos < len(self._components):
|
||||
return self._components[pos]
|
||||
try:
|
||||
pos = cls.__match_args__.index(name)
|
||||
except ValueError:
|
||||
pos = -1
|
||||
if 0 <= pos < len(self._components):
|
||||
return self._components[pos]
|
||||
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||
raise AttributeError(msg)
|
||||
|
||||
|
@ -361,14 +361,16 @@ class Vector:
|
||||
index = operator.index(key)
|
||||
return self._components[index]
|
||||
|
||||
shortcut_names = 'xyzt'
|
||||
__match_args__ = ('x', 'y', 'z', 't')
|
||||
|
||||
def __getattr__(self, name):
|
||||
cls = type(self)
|
||||
if len(name) == 1:
|
||||
pos = cls.shortcut_names.find(name)
|
||||
if 0 <= pos < len(self._components):
|
||||
return self._components[pos]
|
||||
try:
|
||||
pos = cls.__match_args__.index(name)
|
||||
except ValueError:
|
||||
pos = -1
|
||||
if 0 <= pos < len(self._components):
|
||||
return self._components[pos]
|
||||
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||
raise AttributeError(msg)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user