updated from Atlas

This commit is contained in:
Luciano Ramalho 2021-08-07 00:44:01 -03:00
parent cbd13885fc
commit 01e717b60a
96 changed files with 580 additions and 1021 deletions

View File

@ -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). [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_ _Luciano Ramalho<br/>June 29, 2021_

View File

@ -58,7 +58,7 @@ Test ``find`` with single result::
Test ``find`` with two results:: 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+2655 ♕ WHITE CHESS QUEEN
U+265B ♛ BLACK CHESS QUEEN U+265B ♛ BLACK CHESS QUEEN

View File

@ -2,11 +2,11 @@
import sys import sys
import unicodedata 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> 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> char = chr(code) # <4>
name = unicodedata.name(char, None) # <5> name = unicodedata.name(char, None) # <5>
if name and query.issubset(name.split()): # <6> if name and query.issubset(name.split()): # <6>

View File

@ -1,9 +1,7 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
@dataclass @dataclass
class ClubMember: class ClubMember:
name: str name: str
guests: list = field(default_factory=list) guests: list = field(default_factory=list)

View File

@ -3,7 +3,6 @@ from dataclasses import dataclass
# tag::CLUBMEMBER[] # tag::CLUBMEMBER[]
@dataclass @dataclass
class ClubMember: class ClubMember:
name: str name: str
guests: list = [] guests: list = []
# end::CLUBMEMBER[] # end::CLUBMEMBER[]

View File

@ -34,9 +34,7 @@ from club import ClubMember
@dataclass @dataclass
class HackerClubMember(ClubMember): # <1> class HackerClubMember(ClubMember): # <1>
all_handles = set() # <2> all_handles = set() # <2>
handle: str = '' # <3> handle: str = '' # <3>
def __post_init__(self): def __post_init__(self):

View File

@ -35,9 +35,7 @@ from club import ClubMember
@dataclass @dataclass
class HackerClubMember(ClubMember): class HackerClubMember(ClubMember):
all_handles: ClassVar[set[str]] = set() all_handles: ClassVar[set[str]] = set()
handle: str = '' handle: str = ''
def __post_init__(self): def __post_init__(self):

View File

@ -32,7 +32,7 @@ from enum import Enum, auto
from datetime import date from datetime import date
class ResourceType(Enum): # <1> class ResourceType(Enum): # <1>
BOOK = auto() BOOK = auto()
EBOOK = auto() EBOOK = auto()
VIDEO = auto() VIDEO = auto()

View File

@ -2,7 +2,6 @@ from dataclasses import dataclass
@dataclass @dataclass
class DemoDataClass: class DemoDataClass:
a: int # <1> a: int # <1>
b: float = 1.1 # <2> b: float = 1.1 # <2>
c = 'spam' # <3> c = 'spam' # <3>

View File

@ -1,7 +1,6 @@
import typing import typing
class DemoNTClass(typing.NamedTuple): class DemoNTClass(typing.NamedTuple):
a: int # <1> a: int # <1>
b: float = 1.1 # <2> b: float = 1.1 # <2>
c = 'spam' # <3> c = 'spam' # <3>

View File

@ -1,5 +1,4 @@
class DemoPlainClass: class DemoPlainClass:
a: int # <1> a: int # <1>
b: float = 1.1 # <2> b: float = 1.1 # <2>
c = 'spam' # <3> c = 'spam' # <3>

View File

@ -11,7 +11,6 @@
from typing import NamedTuple from typing import NamedTuple
class Coordinate(NamedTuple): class Coordinate(NamedTuple):
lat: float lat: float
lon: float lon: float

View File

@ -13,8 +13,7 @@ This version has a field with a default value::
from typing import NamedTuple from typing import NamedTuple
class Coordinate(NamedTuple): class Coordinate(NamedTuple):
lat: float # <1> lat: float # <1>
lon: float lon: float
reference: str = 'WGS84' # <2> reference: str = 'WGS84' # <2>
# end::COORDINATE[] # end::COORDINATE[]

View File

@ -1,7 +1,6 @@
import typing import typing
class Coordinate(typing.NamedTuple): class Coordinate(typing.NamedTuple):
lat: float lat: float
lon: float lon: float

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ characters which contain that word in their names. For example::
import sys import sys
import re import re
import unicodedata import unicodedata
from typing import Dict, Set, Iterator from collections.abc import Iterator
RE_WORD = re.compile(r'\w+') RE_WORD = re.compile(r'\w+')
STOP_CODE = sys.maxunicode + 1 STOP_CODE = sys.maxunicode + 1
@ -25,8 +25,8 @@ def tokenize(text: str) -> Iterator[str]: # <1>
for match in RE_WORD.finditer(text): for match in RE_WORD.finditer(text):
yield match.group().upper() yield match.group().upper()
def name_index(start: int = 32, end: int = STOP_CODE) -> Dict[str, Set[str]]: def name_index(start: int = 32, end: int = STOP_CODE) -> dict[str, set[str]]:
index: Dict[str, Set[str]] = {} # <2> index: dict[str, set[str]] = {} # <2>
for char in (chr(i) for i in range(start, end)): for char in (chr(i) for i in range(start, end)):
if name := unicodedata.name(char, ''): # <3> if name := unicodedata.name(char, ''): # <3>
for word in tokenize(name): for word in tokenize(name):

View File

@ -1,4 +1,4 @@
from typing import Tuple, Mapping from collections.abc import Mapping
NAMES = { NAMES = {
'aqua': 65535, 'aqua': 65535,
@ -19,7 +19,7 @@ NAMES = {
'yellow': 16776960, '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): if any(c not in range(256) for c in color):
raise ValueError('Color components must be in range(256)') raise ValueError('Color components must be in range(256)')
values = (f'{n % 256:02x}' for n in color) 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}" 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] != '#': if len(color) != 7 or color[0] != '#':
raise ValueError(HEX_ERROR.format(color)) raise ValueError(HEX_ERROR.format(color))
try: try:

View File

@ -1,7 +1,7 @@
# tag::COLUMNIZE[] # 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: if num_columns == 0:
num_columns = round(len(sequence) ** .5) num_columns = round(len(sequence) ** .5)
num_rows, reminder = divmod(len(sequence), num_columns) num_rows, reminder = divmod(len(sequence), num_columns)

View File

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

View File

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

View File

@ -20,11 +20,14 @@ Example:
""" """
# tag::TOP[] # tag::TOP[]
from typing import TypeVar, Iterable, List from collections.abc import Iterable
from typing import TypeVar
from comparable import SupportsLessThan from comparable import SupportsLessThan
LT = TypeVar('LT', bound=SupportsLessThan) LT = TypeVar('LT', bound=SupportsLessThan)
def top(series: Iterable[LT], length: int) -> List[LT]: def top(series: Iterable[LT], length: int) -> list[LT]:
return sorted(series, reverse=True)[:length] ordered = sorted(series, reverse=True)
return ordered[:length]
# end::TOP[] # end::TOP[]

View File

@ -1,6 +1,11 @@
from typing import Tuple, List, Iterator, TYPE_CHECKING # tag::TOP_IMPORT[]
import pytest # type: ignore from collections.abc import Iterator
from typing import TYPE_CHECKING # <1>
import pytest
from top import top from top import top
# end::TOP_IMPORT[]
@pytest.mark.parametrize('series, length, expected', [ @pytest.mark.parametrize('series, length, expected', [
((1, 2, 3), 2, [3, 2]), ((1, 2, 3), 2, [3, 2]),
@ -8,9 +13,9 @@ from top import top
((3, 3, 3), 1, [3]), ((3, 3, 3), 1, [3]),
]) ])
def test_top( def test_top(
series: Tuple[float, ...], series: tuple[float, ...],
length: int, length: int,
expected: List[float], expected: list[float],
) -> None: ) -> None:
result = top(series, length) result = top(series, length)
assert expected == result assert expected == result
@ -18,13 +23,13 @@ def test_top(
# tag::TOP_TEST[] # tag::TOP_TEST[]
def test_top_tuples() -> None: def test_top_tuples() -> None:
fruit = 'mango pear apple kiwi banana'.split() 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) (len(s), s) for s in fruit)
length = 3 length = 3
expected = [(6, 'banana'), (5, 'mango'), (5, 'apple')] expected = [(6, 'banana'), (5, 'mango'), (5, 'apple')]
result = top(series, length) result = top(series, length)
if TYPE_CHECKING: if TYPE_CHECKING: # <3>
reveal_type(series) reveal_type(series) # <4>
reveal_type(expected) reveal_type(expected)
reveal_type(result) reveal_type(result)
assert result == expected assert result == expected
@ -34,7 +39,7 @@ def test_top_objects_error() -> None:
series = [object() for _ in range(4)] series = [object() for _ in range(4)]
if TYPE_CHECKING: if TYPE_CHECKING:
reveal_type(series) reveal_type(series)
with pytest.raises(TypeError) as exc: with pytest.raises(TypeError) as excinfo:
top(series, 3) top(series, 3) # <5>
assert "'<' not supported" in str(exc) assert "'<' not supported" in str(excinfo.value)
# end::TOP_TEST[] # end::TOP_TEST[]

View File

@ -8,10 +8,10 @@
""" """
# tag::GEOHASH[] # tag::GEOHASH[]
from geolib import geohash as gh # type: ignore from geolib import geohash as gh # type: ignore # <1>
PRECISION = 9 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) return gh.encode(*lat_lon, PRECISION)
# end::GEOHASH[] # end::GEOHASH[]

View File

@ -9,7 +9,7 @@
""" """
# tag::GEOHASH[] # tag::GEOHASH[]
from typing import Tuple, NamedTuple from typing import NamedTuple
from geolib import geohash as gh # type: ignore from geolib import geohash as gh # type: ignore
@ -21,16 +21,21 @@ class Coordinate(NamedTuple):
def geohash(lat_lon: Coordinate) -> str: def geohash(lat_lon: Coordinate) -> str:
return gh.encode(*lat_lon, PRECISION) 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 lat, lon = lat_lon
ns = 'N' if lat >= 0 else 'S' ns = 'N' if lat >= 0 else 'S'
ew = 'E' if lon >= 0 else 'W' ew = 'E' if lon >= 0 else 'W'
return f'{abs(lat):0.1f}°{ns}, {abs(lon):0.1f}°{ew}' return f'{abs(lat):0.1f}°{ns}, {abs(lon):0.1f}°{ew}'
# end::DISPLAY[]
# end::GEOHASH[]
def demo(): def demo():
shanghai = 31.2304, 121.4737 shanghai = 31.2304, 121.4737
print(display(shanghai))
s = geohash(shanghai) s = geohash(shanghai)
print(s) print(s)
if __name__ == '__main__':
demo()

View File

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

View File

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

View File

@ -7,11 +7,11 @@ from messages import show_count
(1, '1 part'), (1, '1 part'),
(2, '2 parts'), (2, '2 parts'),
]) ])
def test_show_count(qty, expected): def test_show_count(qty: int, expected: str) -> None:
got = show_count(qty, 'part') got = show_count(qty, 'part')
assert got == expected assert got == expected
def test_show_count_zero(): def test_show_count_zero():
got = show_count(0, 'part') got = show_count(0, 'part')
assert got == 'no part' assert got == 'no parts'

View File

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

View File

@ -1,4 +1,4 @@
from pytest import mark # type: ignore from pytest import mark
from messages import show_count from messages import show_count
@ -6,9 +6,9 @@ from messages import show_count
@mark.parametrize('qty, expected', [ @mark.parametrize('qty, expected', [
(1, '1 part'), (1, '1 part'),
(2, '2 parts'), (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') got = show_count(qty, 'part')
assert got == expected assert got == expected
@ -17,9 +17,9 @@ def test_show_count(qty, expected):
@mark.parametrize('qty, expected', [ @mark.parametrize('qty, expected', [
(1, '1 child'), (1, '1 child'),
(2, '2 children'), (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') got = show_count(qty, 'child', 'children')
assert got == expected assert got == expected
# end::TEST_IRREGULAR[] # end::TEST_IRREGULAR[]

View File

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

View File

@ -12,4 +12,4 @@ def test_show_count(qty, expected):
def test_show_count_zero(): def test_show_count_zero():
got = show_count(0, 'part') got = show_count(0, 'part')
assert got == 'no part' assert got == 'no parts'

View File

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

View File

@ -1,6 +1,6 @@
# tag::MODE_FLOAT[] # tag::MODE_FLOAT[]
from collections import Counter from collections import Counter
from typing import Iterable from collections.abc import Iterable
def mode(data: Iterable[float]) -> float: def mode(data: Iterable[float]) -> float:
pairs = Counter(data).most_common(1) pairs = Counter(data).most_common(1)
@ -20,4 +20,4 @@ def demo() -> None:
print(repr(m), type(m)) print(repr(m), type(m))
if __name__ == '__main__': if __name__ == '__main__':
demo() demo()

View File

@ -1,6 +1,7 @@
# tag::MODE_HASHABLE_T[] # tag::MODE_HASHABLE_T[]
from collections import Counter 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) HashableT = TypeVar('HashableT', bound=Hashable)

View File

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

View File

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

View File

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

View File

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

View File

@ -13,9 +13,9 @@
""" """
# tag::ZIP_REPLACE[] # 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> def zip_replace(text: str, changes: Iterable[FromTo]) -> str: # <2>
for from_, to in changes: for from_, to in changes:

View File

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

View File

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

View File

@ -1,10 +1,11 @@
# tag::SAMPLE[] # tag::SAMPLE[]
from collections.abc import Sequence
from random import shuffle from random import shuffle
from typing import Sequence, List, TypeVar from typing import TypeVar
T = TypeVar('T') T = TypeVar('T')
def sample(population: Sequence[T], size: int) -> List[T]: def sample(population: Sequence[T], size: int) -> list[T]:
if size < 1: if size < 1:
raise ValueError('size must be >= 1') raise ValueError('size must be >= 1')
result = list(population) result = list(population)

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

View File

@ -7,7 +7,7 @@ RT = TypeVar('RT', float, Decimal)
def triple1(a: RT) -> RT: def triple1(a: RT) -> RT:
return a * 3 return a * 3
res1 = triple1(1, 2) res1 = triple1(2)
if TYPE_CHECKING: if TYPE_CHECKING:
reveal_type(res1) reveal_type(res1)
@ -19,7 +19,7 @@ BT = TypeVar('BT', bound=float)
def triple2(a: BT) -> BT: def triple2(a: BT) -> BT:
return a * 3 return a * 3
res2 = triple2(1, 2) res2 = triple2(2)
if TYPE_CHECKING: if TYPE_CHECKING:
reveal_type(res2) reveal_type(res2)

View File

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

View File

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

View File

@ -1,4 +1,4 @@
from typing import List from decimal import Decimal
import pytest # type: ignore import pytest # type: ignore
@ -17,47 +17,49 @@ def customer_fidelity_1100() -> Customer:
@pytest.fixture @pytest.fixture
def cart_plain() -> List[LineItem]: def cart_plain() -> tuple[LineItem, ...]:
return [ return (
LineItem('banana', 4, 0.5), LineItem('banana', 4, Decimal('0.5')),
LineItem('apple', 10, 1.5), LineItem('apple', 10, Decimal('1.5')),
LineItem('watermelon', 5, 5.0), LineItem('watermelon', 5, Decimal('5.0')),
] )
def test_fidelity_promo_no_discount(customer_fidelity_0, cart_plain) -> None: def test_fidelity_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, FidelityPromo()) order = Order(customer_fidelity_0, cart_plain, FidelityPromo())
assert order.total() == 42.0 assert order.total() == 42
assert order.due() == 42.0 assert order.due() == 42
def test_fidelity_promo_with_discount(customer_fidelity_1100, cart_plain) -> None: def test_fidelity_promo_with_discount(customer_fidelity_1100, cart_plain) -> None:
order = Order(customer_fidelity_1100, cart_plain, FidelityPromo()) order = Order(customer_fidelity_1100, cart_plain, FidelityPromo())
assert order.total() == 42.0 assert order.total() == 42
assert order.due() == 39.9 assert order.due() == Decimal('39.9')
def test_bulk_item_promo_no_discount(customer_fidelity_0, cart_plain) -> None: def test_bulk_item_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, BulkItemPromo()) order = Order(customer_fidelity_0, cart_plain, BulkItemPromo())
assert order.total() == 42.0 assert order.total() == 42
assert order.due() == 42.0 assert order.due() == 42
def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None: 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()) order = Order(customer_fidelity_0, cart, BulkItemPromo())
assert order.total() == 30.0 assert order.total() == 30
assert order.due() == 28.5 assert order.due() == Decimal('28.5')
def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None: def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, LargeOrderPromo()) order = Order(customer_fidelity_0, cart_plain, LargeOrderPromo())
assert order.total() == 42.0 assert order.total() == 42
assert order.due() == 42.0 assert order.due() == 42
def test_large_order_promo_with_discount(customer_fidelity_0) -> None: 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()) order = Order(customer_fidelity_0, cart, LargeOrderPromo())
assert order.total() == 10.0 assert order.total() == 10
assert order.due() == 9.3 assert order.due() == Decimal('9.3')

View File

@ -17,9 +17,9 @@
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo()) # <6> >>> Order(joe, banana_cart, BulkItemPromo()) # <6>
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo()) >>> Order(joe, cart, LargeOrderPromo())
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>

View File

@ -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""" """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""" """10% discount for each LineItem with 20 or more units"""
discount = 0 discount = Decimal(0)
for item in order.cart: for item in order.cart:
if item.quantity >= 20: if item.quantity >= 20:
discount += item.total() * 0.1 discount += item.total() * Decimal('0.1')
return discount return discount
def large_order_promo(order): def large_order_promo(order: Order) -> Decimal:
"""7% discount for orders with 10 or more distinct items""" """7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart} distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10: if len(distinct_items) >= 10:
return order.total() * 0.07 return order.total() * Decimal('0.07')
return 0 return Decimal(0)

View File

@ -17,9 +17,9 @@
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo()) # <6> >>> Order(joe, banana_cart, BulkItemPromo()) # <6>
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo()) >>> Order(joe, cart, LargeOrderPromo())
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>

View File

@ -1,13 +1,2 @@
mypy==0.770 mypy==0.910
mypy-extensions==0.4.3 pytest==6.2.4
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

View File

@ -6,20 +6,20 @@
>>> joe = Customer('John Doe', 0) # <1> >>> joe = Customer('John Doe', 0) # <1>
>>> ann = Customer('Ann Smith', 1100) >>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5), >>> cart = [LineItem('banana', 4, Decimal('.5')),
... LineItem('apple', 10, 1.5), ... LineItem('apple', 10, Decimal('1.5')),
... LineItem('watermelon', 5, 5.0)] ... LineItem('watermelon', 5, Decimal(5))]
>>> Order(joe, cart, fidelity_promo) # <2> >>> Order(joe, cart, fidelity_promo) # <2>
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity_promo) >>> Order(ann, cart, fidelity_promo)
<Order total: 42.00 due: 39.90> <Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5), >>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, Decimal('1.5'))]
>>> Order(joe, banana_cart, bulk_item_promo) # <3> >>> Order(joe, banana_cart, bulk_item_promo) # <3>
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo) >>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>
@ -28,75 +28,71 @@
""" """
# tag::STRATEGY[] # tag::STRATEGY[]
import typing from collections.abc import Sequence
from typing import Sequence, Optional, Callable from dataclasses import dataclass
from decimal import Decimal
from typing import Optional, Callable, NamedTuple
class Customer(typing.NamedTuple): class Customer(NamedTuple):
name: str name: str
fidelity: int fidelity: int
class LineItem: class LineItem(NamedTuple):
def __init__(self, product: str, quantity: int, price: float): product: str
self.product = product quantity: int
self.quantity = quantity price: Decimal
self.price = price
def total(self): def total(self):
return self.price * self.quantity return self.price * self.quantity
@dataclass(frozen=True)
class Order: # the Context class Order: # the Context
def __init__( customer: Customer
self, cart: Sequence[LineItem]
customer: Customer, promotion: Optional[Callable[['Order'], Decimal]] = None # <1>
cart: Sequence[LineItem],
promotion: Optional[Callable[['Order'], float]] = None,
) -> None:
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self) -> float: def total(self) -> Decimal:
if not hasattr(self, '__total'): totals = (item.total() for item in self.cart)
self.__total = sum(item.total() for item in self.cart) return sum(totals, start=Decimal(0))
return self.__total
def due(self) -> float: def due(self) -> Decimal:
if self.promotion is None: if self.promotion is None:
discount = 0.0 discount = Decimal(0)
else: else:
discount = self.promotion(self) # <1> discount = self.promotion(self) # <2>
return self.total() - discount return self.total() - discount
def __repr__(self): def __repr__(self):
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>' 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""" """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""" """10% discount for each LineItem with 20 or more units"""
discount = 0 discount = Decimal(0)
for item in order.cart: for item in order.cart:
if item.quantity >= 20: if item.quantity >= 20:
discount += item.total() * 0.1 discount += item.total() * Decimal('0.1')
return discount 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""" """7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart} distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10: if len(distinct_items) >= 10:
return order.total() * 0.07 return order.total() * Decimal('0.07')
return 0 return Decimal(0)
# end::STRATEGY[] # end::STRATEGY[]

View File

@ -3,19 +3,20 @@
# selecting best promotion from static list of functions # selecting best promotion from static list of functions
""" """
>>> from strategy import Customer, LineItem
>>> joe = Customer('John Doe', 0) >>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100) >>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5), >>> cart = [LineItem('banana', 4, Decimal('.5')),
... LineItem('apple', 10, 1.5), ... LineItem('apple', 10, Decimal('1.5')),
... LineItem('watermelon', 5, 5.0)] ... LineItem('watermelon', 5, Decimal(5))]
>>> banana_cart = [LineItem('banana', 30, .5), >>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, Decimal('1.5'))]
>>> big_cart = [LineItem(str(item_code), 1, 1.0) >>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
... for item_code in range(10)] ... for item_code in range(10)]
# tag::STRATEGY_BEST_TESTS[] # 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 total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo) # <2> >>> Order(joe, banana_cart, best_promo) # <2>
<Order total: 30.00 due: 28.50> <Order total: 30.00 due: 28.50>
@ -25,7 +26,9 @@
# end::STRATEGY_BEST_TESTS[] # 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 from strategy import fidelity_promo, bulk_item_promo, large_order_promo
# tag::STRATEGY_BEST[] # 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> promos = [fidelity_promo, bulk_item_promo, large_order_promo] # <1>
def best_promo(order) -> float: # <2> def best_promo(order: Order) -> Decimal: # <2>
"""Select best discount available """Compute the best discount available"""
"""
return max(promo(order) for promo in promos) # <3> return max(promo(order) for promo in promos) # <3>

View File

@ -3,29 +3,31 @@
# selecting best promotion from current module globals # selecting best promotion from current module globals
""" """
>>> from decimal import Decimal
>>> from strategy import Customer, LineItem, Order
>>> joe = Customer('John Doe', 0) >>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100) >>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5), >>> cart = [LineItem('banana', 4, Decimal('.5')),
... LineItem('apple', 10, 1.5), ... LineItem('apple', 10, Decimal('1.5')),
... LineItem('watermelon', 5, 5.0)] ... LineItem('watermelon', 5, Decimal(5))]
>>> Order(joe, cart, fidelity_promo) >>> Order(joe, cart, fidelity_promo)
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity_promo) >>> Order(ann, cart, fidelity_promo)
<Order total: 42.00 due: 39.90> <Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5), >>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, Decimal('1.5'))]
>>> Order(joe, banana_cart, bulk_item_promo) >>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo) >>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[] # tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo) >>> Order(joe, long_cart, best_promo)
<Order total: 10.00 due: 9.30> <Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo) >>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50> <Order total: 30.00 due: 28.50>
@ -35,78 +37,21 @@
# end::STRATEGY_BEST_TESTS[] # 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[] # tag::STRATEGY_BEST2[]
from decimal import Decimal
from strategy import Order
from strategy import (
fidelity_promo, bulk_item_promo, large_order_promo # <1>
)
promos = [ promos = [promo for name, promo in globals().items() # <2>
globals()[name] if name.endswith('_promo') and # <3>
for name in globals() # <1> name != 'best_promo' # <4>
if name.endswith('_promo') and name != 'best_promo' # <2> ]
] # <3>
def best_promo(order): def best_promo(order: Order) -> Decimal: # <5>
"""Select best discount available """Compute the best discount available"""
""" return max(promo(order) for promo in promos)
return max(promo(order) for promo in promos) # <4>
# end::STRATEGY_BEST2[] # end::STRATEGY_BEST2[]

View File

@ -3,30 +3,32 @@
# selecting best promotion from imported module # selecting best promotion from imported module
""" """
>>> from decimal import Decimal
>>> from strategy import Customer, LineItem, Order
>>> from promotions import * >>> from promotions import *
>>> joe = Customer('John Doe', 0) >>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100) >>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5), >>> cart = [LineItem('banana', 4, Decimal('.5')),
... LineItem('apple', 10, 1.5), ... LineItem('apple', 10, Decimal('1.5')),
... LineItem('watermelon', 5, 5.0)] ... LineItem('watermelon', 5, Decimal(5))]
>>> Order(joe, cart, fidelity_promo) >>> Order(joe, cart, fidelity_promo)
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity_promo) >>> Order(ann, cart, fidelity_promo)
<Order total: 42.00 due: 39.90> <Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5), >>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, Decimal('1.5'))]
>>> Order(joe, banana_cart, bulk_item_promo) >>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo) >>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[] # tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo) >>> Order(joe, long_cart, best_promo)
<Order total: 10.00 due: 9.30> <Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo) >>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50> <Order total: 30.00 due: 28.50>
@ -36,55 +38,20 @@
# end::STRATEGY_BEST_TESTS[] # 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[] # 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): promos = [func for _, func in inspect.getmembers(promotions, inspect.isfunction)]
"""Select best discount available
"""
def best_promo(order: Order) -> Decimal:
"""Compute the best discount available"""
return max(promo(order) for promo in promos) return max(promo(order) for promo in promos)
# end::STRATEGY_BEST3[] # end::STRATEGY_BEST3[]

View File

@ -4,29 +4,32 @@
# registered by a decorator # registered by a decorator
""" """
>>> from decimal import Decimal
>>> from strategy import Customer, LineItem, Order
>>> from promotions import *
>>> joe = Customer('John Doe', 0) >>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100) >>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5), >>> cart = [LineItem('banana', 4, Decimal('.5')),
... LineItem('apple', 10, 1.5), ... LineItem('apple', 10, Decimal('1.5')),
... LineItem('watermelon', 5, 5.0)] ... LineItem('watermelon', 5, Decimal(5))]
>>> Order(joe, cart, fidelity) >>> Order(joe, cart, fidelity_promo)
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity) >>> Order(ann, cart, fidelity_promo)
<Order total: 42.00 due: 39.90> <Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5), >>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, Decimal('1.5'))]
>>> Order(joe, banana_cart, bulk_item) >>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order) >>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[] # tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo) >>> Order(joe, long_cart, best_promo)
<Order total: 10.00 due: 9.30> <Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo) >>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50> <Order total: 30.00 due: 28.50>
@ -36,47 +39,16 @@
# end::STRATEGY_BEST_TESTS[] # end::STRATEGY_BEST_TESTS[]
""" """
from collections import namedtuple from decimal import Decimal
from typing import Callable, List from typing import Callable
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 strategy import Order
# tag::STRATEGY_BEST4[] # tag::STRATEGY_BEST4[]
Promotion = Callable[[Order], float] # <2> Promotion = Callable[[Order], Decimal]
promos: list[Promotion] = [] # <1>
def promotion(promo: Promotion) -> Promotion: # <2> def promotion(promo: Promotion) -> Promotion: # <2>
@ -84,38 +56,35 @@ def promotion(promo: Promotion) -> Promotion: # <2>
return promo 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> @promotion # <4>
def fidelity(order: Order) -> float: def fidelity(order: Order) -> Decimal:
"""5% discount for customers with 1000 or more fidelity points""" """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 @promotion
def bulk_item(order: Order) -> float: def bulk_item(order: Order) -> Decimal:
"""10% discount for each LineItem with 20 or more units""" """10% discount for each LineItem with 20 or more units"""
discount = 0 discount = Decimal(0)
for item in order.cart: for item in order.cart:
if item.quantity >= 20: if item.quantity >= 20:
discount += item.total() * 0.1 discount += item.total() * Decimal('0.1')
return discount return discount
@promotion @promotion
def large_order(order: Order) -> float: def large_order(order: Order) -> Decimal:
"""7% discount for orders with 10 or more distinct items""" """7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart} distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10: if len(distinct_items) >= 10:
return order.total() * 0.07 return order.total() * Decimal('0.07')
return 0 return Decimal(0)
def best_promo(order: Order) -> float: # <4>
"""Select best discount available
"""
return max(promo(order) for promo in promos)
# end::STRATEGY_BEST4[] # end::STRATEGY_BEST4[]

View File

@ -15,9 +15,9 @@
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo(10)) >>> Order(joe, banana_cart, bulk_item_promo(10))
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo(7)) >>> Order(joe, cart, LargeOrderPromo(7))
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>

View File

@ -47,7 +47,7 @@ def test_large_order_promo_with_discount(customer_fidelity_0) -> None:
assert order.due() == 9.3 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) general_promo: Promotion = functools.partial(general_discount, 5)
order = Order(customer_fidelity_1100, cart_plain, general_promo) order = Order(customer_fidelity_1100, cart_plain, general_promo)
assert order.total() == 42.0 assert order.total() == 42.0

View File

@ -1,4 +1,4 @@
from typing import List from decimal import Decimal
import pytest # type: ignore import pytest # type: ignore
@ -17,47 +17,50 @@ def customer_fidelity_1100() -> Customer:
@pytest.fixture @pytest.fixture
def cart_plain() -> List[LineItem]: def cart_plain() -> tuple[LineItem, ...]:
return [ return (
LineItem('banana', 4, 0.5), LineItem('banana', 4, Decimal('0.5')),
LineItem('apple', 10, 1.5), LineItem('apple', 10, Decimal('1.5')),
LineItem('watermelon', 5, 5.0), LineItem('watermelon', 5, Decimal('5.0')),
] )
def test_fidelity_promo_no_discount(customer_fidelity_0, cart_plain) -> None: def test_fidelity_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, fidelity_promo) order = Order(customer_fidelity_0, cart_plain, fidelity_promo)
assert order.total() == 42.0 assert order.total() == 42
assert order.due() == 42.0 assert order.due() == 42
def test_fidelity_promo_with_discount(customer_fidelity_1100, cart_plain) -> None: def test_fidelity_promo_with_discount(customer_fidelity_1100, cart_plain) -> None:
order = Order(customer_fidelity_1100, cart_plain, fidelity_promo) order = Order(customer_fidelity_1100, cart_plain, fidelity_promo)
assert order.total() == 42.0 assert order.total() == 42
assert order.due() == 39.9 assert order.due() == Decimal('39.9')
def test_bulk_item_promo_no_discount(customer_fidelity_0, cart_plain) -> None: def test_bulk_item_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, bulk_item_promo) order = Order(customer_fidelity_0, cart_plain, bulk_item_promo)
assert order.total() == 42.0 assert order.total() == 42
assert order.due() == 42.0 assert order.due() == 42
def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None: 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) order = Order(customer_fidelity_0, cart, bulk_item_promo)
assert order.total() == 30.0 assert order.total() == 30
assert order.due() == 28.5 assert order.due() == Decimal('28.5')
def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None: def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
order = Order(customer_fidelity_0, cart_plain, large_order_promo) order = Order(customer_fidelity_0, cart_plain, large_order_promo)
assert order.total() == 42.0 assert order.total() == 42
assert order.due() == 42.0 assert order.due() == 42
def test_large_order_promo_with_discount(customer_fidelity_0) -> None: 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) order = Order(customer_fidelity_0, cart, large_order_promo)
assert order.total() == 10.0 assert order.total() == 10
assert order.due() == 9.3 assert order.due() == Decimal('9.3')

View File

@ -17,9 +17,9 @@
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo()) # <6> >>> Order(joe, banana_cart, BulkItemPromo()) # <6>
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo()) >>> Order(joe, cart, LargeOrderPromo())
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>

View File

@ -17,9 +17,9 @@
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo) # <3> >>> Order(joe, banana_cart, bulk_item_promo) # <3>
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo) >>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>

View File

@ -16,16 +16,16 @@
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo) >>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo) >>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[] # 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 total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo) # <2> >>> Order(joe, banana_cart, best_promo) # <2>
<Order total: 30.00 due: 28.50> <Order total: 30.00 due: 28.50>

View File

@ -16,16 +16,16 @@
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo) >>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo) >>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[] # tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo) >>> Order(joe, long_cart, best_promo)
<Order total: 10.00 due: 9.30> <Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo) >>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50> <Order total: 30.00 due: 28.50>

View File

@ -17,16 +17,16 @@
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo) >>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo) >>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[] # tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo) >>> Order(joe, long_cart, best_promo)
<Order total: 10.00 due: 9.30> <Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo) >>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50> <Order total: 30.00 due: 28.50>

View File

@ -17,16 +17,16 @@
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item) >>> Order(joe, banana_cart, bulk_item)
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order) >>> Order(joe, cart, large_order)
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[] # tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo) >>> Order(joe, long_cart, best_promo)
<Order total: 10.00 due: 9.30> <Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo) >>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50> <Order total: 30.00 due: 28.50>

View File

@ -15,9 +15,9 @@
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo(10)) >>> Order(joe, banana_cart, bulk_item_promo(10))
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo(7)) >>> Order(joe, cart, large_order_promo(7))
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>

View File

@ -15,9 +15,9 @@
... LineItem('apple', 10, 1.5)] ... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo(10)) >>> Order(joe, banana_cart, BulkItemPromo(10))
<Order total: 30.00 due: 28.50> <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)] ... 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 total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo(7)) >>> Order(joe, cart, LargeOrderPromo(7))
<Order total: 42.00 due: 42.00> <Order total: 42.00 due: 42.00>

View File

@ -4,20 +4,25 @@ import resource
NUM_VECTORS = 10**7 NUM_VECTORS = 10**7
module = None
if len(sys.argv) == 2: if len(sys.argv) == 2:
module_name = sys.argv[1].replace('.py', '') module_name = sys.argv[1].replace('.py', '')
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
else: else:
print(f'Usage: {sys.argv[0]} <vector-module-to-test>') print(f'Usage: {sys.argv[0]} <vector-module-to-test>')
sys.exit(2) # command line usage error
fmt = 'Selected Vector2d type: {.__name__}.{.__name__}' if module is None:
print(fmt.format(module, module.Vector2d)) 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 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 mem_final = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
print(f'Initial RAM usage: {mem_init:14,}') print(f'Initial RAM usage: {mem_init:14,}')

View 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
View 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[]

View File

@ -72,7 +72,7 @@ Tests of `x` and `y` read-only properties:
>>> v1.x = 123 >>> v1.x = 123
Traceback (most recent call last): Traceback (most recent call last):
... ...
AttributeError: can't set attribute AttributeError: can't set attribute 'x'
Tests of hashing: Tests of hashing:
@ -90,6 +90,8 @@ from array import array
import math import math
class Vector2d: class Vector2d:
__match_args__ = ('x', 'y')
typecode = 'd' typecode = 'd'
def __init__(self, x, y): def __init__(self, x, y):

View File

@ -72,7 +72,7 @@ Tests of `x` and `y` read-only properties:
>>> v1.x = 123 >>> v1.x = 123
Traceback (most recent call last): Traceback (most recent call last):
... ...
AttributeError: can't set attribute AttributeError: can't set attribute 'x'
# end::VECTOR2D_V3_HASH_DEMO[] # end::VECTOR2D_V3_HASH_DEMO[]
@ -112,7 +112,7 @@ class Vector2d:
def __iter__(self): def __iter__(self):
return (i for i in (self.x, self.y)) # <6> 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[] # end::VECTOR2D_V3_PROP[]
def __repr__(self): def __repr__(self):

View File

@ -72,7 +72,7 @@ Tests of `x` and `y` read-only properties:
>>> v1.x = 123 >>> v1.x = 123
Traceback (most recent call last): Traceback (most recent call last):
... ...
AttributeError: can't set attribute AttributeError: can't set attribute 'x'
Tests of hashing: Tests of hashing:
@ -90,11 +90,10 @@ import math
# tag::VECTOR2D_V3_SLOTS[] # tag::VECTOR2D_V3_SLOTS[]
class Vector2d: class Vector2d:
__slots__ = ('__x', '__y') __match_args__ = ('x', 'y') # <1>
__slots__ = ('__x', '__y') # <2>
typecode = 'd' typecode = 'd'
# methods follow (omitted in book listing)
# end::VECTOR2D_V3_SLOTS[] # end::VECTOR2D_V3_SLOTS[]
def __init__(self, x, y): def __init__(self, x, y):

View File

@ -199,15 +199,17 @@ class Vector:
return self._components[index] return self._components[index]
# tag::VECTOR_V3_GETATTR[] # tag::VECTOR_V3_GETATTR[]
shortcut_names = 'xyzt' __match_args__ = ('x', 'y', 'z', 't') # <1>
def __getattr__(self, name): def __getattr__(self, name):
cls = type(self) # <1> cls = type(self) # <2>
if len(name) == 1: # <2> try:
pos = cls.shortcut_names.find(name) # <3> pos = cls.__match_args__.index(name) # <3>
if 0 <= pos < len(self._components): # <4> except ValueError: # <4>
return self._components[pos] pos = -1
msg = f'{cls.__name__!r} object has no attribute {name!r}' # <5> 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) raise AttributeError(msg)
# end::VECTOR_V3_GETATTR[] # end::VECTOR_V3_GETATTR[]
@ -215,8 +217,8 @@ class Vector:
def __setattr__(self, name, value): def __setattr__(self, name, value):
cls = type(self) cls = type(self)
if len(name) == 1: # <1> if len(name) == 1: # <1>
if name in cls.shortcut_names: # <2> if name in cls.__match_args__: # <2>
error = 'read-only attribute {attr_name!r}' error = 'readonly attribute {attr_name!r}'
elif name.islower(): # <3> elif name.islower(): # <3>
error = "can't set attributes 'a' to 'z' in {cls_name!r}" error = "can't set attributes 'a' to 'z' in {cls_name!r}"
else: else:

View File

@ -199,14 +199,16 @@ class Vector:
index = operator.index(key) index = operator.index(key)
return self._components[index] return self._components[index]
shortcut_names = 'xyzt' __match_args__ = ('x', 'y', 'z', 't')
def __getattr__(self, name): def __getattr__(self, name):
cls = type(self) cls = type(self)
if len(name) == 1: try:
pos = cls.shortcut_names.find(name) pos = cls.__match_args__.index(name)
if 0 <= pos < len(self._components): except ValueError:
return self._components[pos] pos = -1
if 0 <= pos < len(self._components):
return self._components[pos]
msg = f'{cls.__name__!r} object has no attribute {name!r}' msg = f'{cls.__name__!r} object has no attribute {name!r}'
raise AttributeError(msg) raise AttributeError(msg)

View File

@ -242,14 +242,16 @@ class Vector:
index = operator.index(key) index = operator.index(key)
return self._components[index] return self._components[index]
shortcut_names = 'xyzt' __match_args__ = ('x', 'y', 'z', 't')
def __getattr__(self, name): def __getattr__(self, name):
cls = type(self) cls = type(self)
if len(name) == 1: try:
pos = cls.shortcut_names.find(name) pos = cls.__match_args__.index(name)
if 0 <= pos < len(self._components): except ValueError:
return self._components[pos] pos = -1
if 0 <= pos < len(self._components):
return self._components[pos]
msg = f'{cls.__name__!r} object has no attribute {name!r}' msg = f'{cls.__name__!r} object has no attribute {name!r}'
raise AttributeError(msg) raise AttributeError(msg)

View File

@ -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') ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split() suits = 'spades diamonds clubs hearts'.split()

View File

@ -12,14 +12,14 @@ class SimplePicker: # <2>
return self._items.pop() return self._items.pop()
def test_isinstance() -> None: # <4> def test_isinstance() -> None: # <4>
popper = SimplePicker([1]) popper: RandomPicker = SimplePicker([1]) # <5>
assert isinstance(popper, RandomPicker) assert isinstance(popper, RandomPicker) # <6>
def test_item_type() -> None: # <5> def test_item_type() -> None: # <7>
items = [1, 2] items = [1, 2]
popper = SimplePicker(items) popper = SimplePicker(items)
item = popper.pick() item = popper.pick()
assert item in items assert item in items
if TYPE_CHECKING: if TYPE_CHECKING:
reveal_type(item) # <6> reveal_type(item) # <8>
assert isinstance(item, int) assert isinstance(item, int)

View File

@ -168,5 +168,5 @@ class Vector2d:
@classmethod @classmethod
def fromcomplex(cls, datum): def fromcomplex(cls, datum):
return Vector2d(datum.real, datum.imag) # <1> return cls(datum.real, datum.imag) # <1>
# end::VECTOR2D_V4_COMPLEX[] # end::VECTOR2D_V4_COMPLEX[]

View File

@ -170,5 +170,5 @@ class Vector2d:
@classmethod @classmethod
def fromcomplex(cls, datum: SupportsComplex) -> Vector2d: # <3> def fromcomplex(cls, datum: SupportsComplex) -> Vector2d: # <3>
c = complex(datum) # <4> c = complex(datum) # <4>
return Vector2d(c.real, c.imag) return cls(c.real, c.imag)
# end::VECTOR2D_V5_COMPLEX[] # end::VECTOR2D_V5_COMPLEX[]

View File

@ -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')]) >>> 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:: Tests for item retrieval using `d[key]` notation::
@ -82,14 +111,12 @@ Tests for count retrieval using `d[key]` notation::
# tag::UPPERCASE_MIXIN[] # tag::UPPERCASE_MIXIN[]
import collections import collections
def _upper(key): # <1> def _upper(key): # <1>
try: try:
return key.upper() return key.upper()
except AttributeError: except AttributeError:
return key return key
class UpperCaseMixin: # <2> class UpperCaseMixin: # <2>
def __setitem__(self, key, item): def __setitem__(self, key, item):
super().__setitem__(_upper(key), item) super().__setitem__(_upper(key), item)
@ -108,8 +135,6 @@ class UpperCaseMixin: # <2>
class UpperDict(UpperCaseMixin, collections.UserDict): # <1> class UpperDict(UpperCaseMixin, collections.UserDict): # <1>
pass pass
class UpperCounter(UpperCaseMixin, collections.Counter): # <2> class UpperCounter(UpperCaseMixin, collections.Counter): # <2>
"""Specialized 'Counter' that uppercases string keys""" # <3> """Specialized 'Counter' that uppercases string keys""" # <3>
# end::UPPERDICT[] # end::UPPERDICT[]

View File

@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# tag::ABS_DEMO[]
import math import math
from typing import NamedTuple, SupportsAbs from typing import NamedTuple, SupportsAbs
@ -30,3 +31,4 @@ assert is_unit(v3)
assert is_unit(v4) assert is_unit(v4)
print('OK') print('OK')
# end::ABS_DEMO[]

View File

@ -1,5 +1,5 @@
""" """
A 2-dimensional vector class A two-dimensional vector class
>>> v1 = Vector2d(3, 4) >>> v1 = Vector2d(3, 4)
>>> print(v1.x, v1.y) >>> print(v1.x, v1.y)
@ -72,7 +72,7 @@ Tests of `x` and `y` read-only properties:
>>> v1.x = 123 >>> v1.x = 123
Traceback (most recent call last): Traceback (most recent call last):
... ...
AttributeError: can't set attribute AttributeError: can't set attribute 'x'
Tests of hashing: Tests of hashing:
@ -81,7 +81,7 @@ Tests of hashing:
>>> v2 = Vector2d(3.1, 4.2) >>> v2 = Vector2d(3.1, 4.2)
>>> hash(v1), hash(v2) >>> hash(v1), hash(v2)
(7, 384307168202284039) (7, 384307168202284039)
>>> len(set([v1, v2])) >>> len({v1, v2})
2 2
""" """
@ -90,6 +90,8 @@ from array import array
import math import math
class Vector2d: class Vector2d:
__match_args__ = ('x', 'y')
typecode = 'd' typecode = 'd'
def __init__(self, x, y): def __init__(self, x, y):

View File

@ -305,14 +305,16 @@ class Vector:
index = operator.index(key) index = operator.index(key)
return self._components[index] return self._components[index]
shortcut_names = 'xyzt' __match_args__ = ('x', 'y', 'z', 't')
def __getattr__(self, name): def __getattr__(self, name):
cls = type(self) cls = type(self)
if len(name) == 1: try:
pos = cls.shortcut_names.find(name) pos = cls.__match_args__.index(name)
if 0 <= pos < len(self._components): except ValueError:
return self._components[pos] pos = -1
if 0 <= pos < len(self._components):
return self._components[pos]
msg = f'{cls.__name__!r} object has no attribute {name!r}' msg = f'{cls.__name__!r} object has no attribute {name!r}'
raise AttributeError(msg) raise AttributeError(msg)

View File

@ -355,14 +355,16 @@ class Vector:
index = operator.index(key) index = operator.index(key)
return self._components[index] return self._components[index]
shortcut_names = 'xyzt' __match_args__ = ('x', 'y', 'z', 't')
def __getattr__(self, name): def __getattr__(self, name):
cls = type(self) cls = type(self)
if len(name) == 1: try:
pos = cls.shortcut_names.find(name) pos = cls.__match_args__.index(name)
if 0 <= pos < len(self._components): except ValueError:
return self._components[pos] pos = -1
if 0 <= pos < len(self._components):
return self._components[pos]
msg = f'{cls.__name__!r} object has no attribute {name!r}' msg = f'{cls.__name__!r} object has no attribute {name!r}'
raise AttributeError(msg) raise AttributeError(msg)

View File

@ -361,14 +361,16 @@ class Vector:
index = operator.index(key) index = operator.index(key)
return self._components[index] return self._components[index]
shortcut_names = 'xyzt' __match_args__ = ('x', 'y', 'z', 't')
def __getattr__(self, name): def __getattr__(self, name):
cls = type(self) cls = type(self)
if len(name) == 1: try:
pos = cls.shortcut_names.find(name) pos = cls.__match_args__.index(name)
if 0 <= pos < len(self._components): except ValueError:
return self._components[pos] pos = -1
if 0 <= pos < len(self._components):
return self._components[pos]
msg = f'{cls.__name__!r} object has no attribute {name!r}' msg = f'{cls.__name__!r} object has no attribute {name!r}'
raise AttributeError(msg) raise AttributeError(msg)