diff --git a/02-array-seq/lispy/README.md b/02-array-seq/lispy/README.md
index 9ac6f56..38458bb 100644
--- a/02-array-seq/lispy/README.md
+++ b/02-array-seq/lispy/README.md
@@ -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
June 29, 2021_
diff --git a/04-text-byte/charfinder/README.rst b/04-text-byte/charfinder/README.rst
index 46a5d70..15a613d 100644
--- a/04-text-byte/charfinder/README.rst
+++ b/04-text-byte/charfinder/README.rst
@@ -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
diff --git a/04-text-byte/charfinder/cf.py b/04-text-byte/charfinder/cf.py
index 28f20d0..db982bc 100755
--- a/04-text-byte/charfinder/cf.py
+++ b/04-text-byte/charfinder/cf.py
@@ -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>
diff --git a/05-data-classes/dataclass/club.py b/05-data-classes/dataclass/club.py
index cd8ff46..7af49c8 100644
--- a/05-data-classes/dataclass/club.py
+++ b/05-data-classes/dataclass/club.py
@@ -1,9 +1,7 @@
from dataclasses import dataclass, field
-
@dataclass
class ClubMember:
-
name: str
guests: list = field(default_factory=list)
diff --git a/05-data-classes/dataclass/club_wrong.py b/05-data-classes/dataclass/club_wrong.py
index 3d73d6a..8521a9d 100644
--- a/05-data-classes/dataclass/club_wrong.py
+++ b/05-data-classes/dataclass/club_wrong.py
@@ -3,7 +3,6 @@ from dataclasses import dataclass
# tag::CLUBMEMBER[]
@dataclass
class ClubMember:
-
name: str
guests: list = []
# end::CLUBMEMBER[]
diff --git a/05-data-classes/dataclass/hackerclub.py b/05-data-classes/dataclass/hackerclub.py
index 4d9112e..762c2cd 100644
--- a/05-data-classes/dataclass/hackerclub.py
+++ b/05-data-classes/dataclass/hackerclub.py
@@ -34,9 +34,7 @@ from club import ClubMember
@dataclass
class HackerClubMember(ClubMember): # <1>
-
all_handles = set() # <2>
-
handle: str = '' # <3>
def __post_init__(self):
diff --git a/05-data-classes/dataclass/hackerclub_annotated.py b/05-data-classes/dataclass/hackerclub_annotated.py
index 5cf90fc..2394796 100644
--- a/05-data-classes/dataclass/hackerclub_annotated.py
+++ b/05-data-classes/dataclass/hackerclub_annotated.py
@@ -35,9 +35,7 @@ from club import ClubMember
@dataclass
class HackerClubMember(ClubMember):
-
all_handles: ClassVar[set[str]] = set()
-
handle: str = ''
def __post_init__(self):
diff --git a/05-data-classes/dataclass/resource.py b/05-data-classes/dataclass/resource.py
index 5190055..f332a11 100644
--- a/05-data-classes/dataclass/resource.py
+++ b/05-data-classes/dataclass/resource.py
@@ -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()
diff --git a/05-data-classes/meaning/demo_dc.py b/05-data-classes/meaning/demo_dc.py
index 3cc45ce..fa45bb8 100644
--- a/05-data-classes/meaning/demo_dc.py
+++ b/05-data-classes/meaning/demo_dc.py
@@ -2,7 +2,6 @@ from dataclasses import dataclass
@dataclass
class DemoDataClass:
-
a: int # <1>
b: float = 1.1 # <2>
c = 'spam' # <3>
diff --git a/05-data-classes/meaning/demo_nt.py b/05-data-classes/meaning/demo_nt.py
index 317fb82..8f52354 100644
--- a/05-data-classes/meaning/demo_nt.py
+++ b/05-data-classes/meaning/demo_nt.py
@@ -1,7 +1,6 @@
import typing
class DemoNTClass(typing.NamedTuple):
-
a: int # <1>
b: float = 1.1 # <2>
c = 'spam' # <3>
diff --git a/05-data-classes/meaning/demo_plain.py b/05-data-classes/meaning/demo_plain.py
index 6376959..98c3e40 100644
--- a/05-data-classes/meaning/demo_plain.py
+++ b/05-data-classes/meaning/demo_plain.py
@@ -1,5 +1,4 @@
class DemoPlainClass:
-
a: int # <1>
b: float = 1.1 # <2>
c = 'spam' # <3>
diff --git a/05-data-classes/typing_namedtuple/coordinates.py b/05-data-classes/typing_namedtuple/coordinates.py
index 378a430..5e4d879 100644
--- a/05-data-classes/typing_namedtuple/coordinates.py
+++ b/05-data-classes/typing_namedtuple/coordinates.py
@@ -11,7 +11,6 @@
from typing import NamedTuple
class Coordinate(NamedTuple):
-
lat: float
lon: float
diff --git a/05-data-classes/typing_namedtuple/coordinates2.py b/05-data-classes/typing_namedtuple/coordinates2.py
index 2032311..efcd6be 100644
--- a/05-data-classes/typing_namedtuple/coordinates2.py
+++ b/05-data-classes/typing_namedtuple/coordinates2.py
@@ -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[]
\ No newline at end of file
+# end::COORDINATE[]
diff --git a/05-data-classes/typing_namedtuple/nocheck_demo.py b/05-data-classes/typing_namedtuple/nocheck_demo.py
index 8ca5dc1..43c1b96 100644
--- a/05-data-classes/typing_namedtuple/nocheck_demo.py
+++ b/05-data-classes/typing_namedtuple/nocheck_demo.py
@@ -1,7 +1,6 @@
import typing
class Coordinate(typing.NamedTuple):
-
lat: float
lon: float
diff --git a/07-1class-func/clip.py b/07-1class-func/clip.py
deleted file mode 100644
index 2f97c66..0000000
--- a/07-1class-func/clip.py
+++ /dev/null
@@ -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[]
diff --git a/07-1class-func/clip_introspection.rst b/07-1class-func/clip_introspection.rst
deleted file mode 100644
index 0b4334d..0000000
--- a/07-1class-func/clip_introspection.rst
+++ /dev/null
@@ -1,9 +0,0 @@
->>> from clip import clip
->>> clip.__defaults__
-(80,)
->>> clip.__code__ # doctest: +ELLIPSIS
-
->>> clip.__code__.co_varnames
-('text', 'max_len', 'end', 'space_at')
->>> clip.__code__.co_argcount
-2
diff --git a/07-1class-func/clip_signature.rst b/07-1class-func/clip_signature.rst
deleted file mode 100644
index 9bc2dee..0000000
--- a/07-1class-func/clip_signature.rst
+++ /dev/null
@@ -1,12 +0,0 @@
->>> from clip import clip
->>> from inspect import signature
->>> sig = signature(clip)
->>> sig
-
->>> str(sig)
-'(text, max_len=80)'
->>> for name, param in sig.parameters.items():
-... print(param.kind, ':', name, '=', param.default)
-...
-POSITIONAL_OR_KEYWORD : text =
-POSITIONAL_OR_KEYWORD : max_len = 80
diff --git a/08-def-type-hints/RPN_calc/calc.py b/08-def-type-hints/RPN_calc/calc.py
deleted file mode 100755
index 967b094..0000000
--- a/08-def-type-hints/RPN_calc/calc.py
+++ /dev/null
@@ -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()
diff --git a/08-def-type-hints/RPN_calc/calc_test.py b/08-def-type-hints/RPN_calc/calc_test.py
deleted file mode 100644
index c2d144c..0000000
--- a/08-def-type-hints/RPN_calc/calc_test.py
+++ /dev/null
@@ -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()
diff --git a/08-def-type-hints/charindex.py b/08-def-type-hints/charindex.py
index 36d20e3..c368c37 100644
--- a/08-def-type-hints/charindex.py
+++ b/08-def-type-hints/charindex.py
@@ -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):
diff --git a/08-def-type-hints/colors.py b/08-def-type-hints/colors.py
index b57413c..c0d0665 100644
--- a/08-def-type-hints/colors.py
+++ b/08-def-type-hints/colors.py
@@ -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:
diff --git a/08-def-type-hints/columnize.py b/08-def-type-hints/columnize.py
index ef995ae..66b72d8 100644
--- a/08-def-type-hints/columnize.py
+++ b/08-def-type-hints/columnize.py
@@ -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)
diff --git a/08-def-type-hints/columnize2.py b/08-def-type-hints/columnize2.py
deleted file mode 100644
index 2524fd3..0000000
--- a/08-def-type-hints/columnize2.py
+++ /dev/null
@@ -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()
diff --git a/08-def-type-hints/columnize_alias.py b/08-def-type-hints/columnize_alias.py
deleted file mode 100644
index b783469..0000000
--- a/08-def-type-hints/columnize_alias.py
+++ /dev/null
@@ -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()
diff --git a/08-def-type-hints/comparable/top.py b/08-def-type-hints/comparable/top.py
index c552890..2851f62 100644
--- a/08-def-type-hints/comparable/top.py
+++ b/08-def-type-hints/comparable/top.py
@@ -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[]
diff --git a/08-def-type-hints/comparable/top_test.py b/08-def-type-hints/comparable/top_test.py
index baff36b..c8c39ac 100644
--- a/08-def-type-hints/comparable/top_test.py
+++ b/08-def-type-hints/comparable/top_test.py
@@ -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[]
diff --git a/08-def-type-hints/coordinates/coordinates.py b/08-def-type-hints/coordinates/coordinates.py
index d48b390..fb594eb 100644
--- a/08-def-type-hints/coordinates/coordinates.py
+++ b/08-def-type-hints/coordinates/coordinates.py
@@ -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[]
diff --git a/08-def-type-hints/coordinates/coordinates_named.py b/08-def-type-hints/coordinates/coordinates_named.py
index 87cac72..76bd9bb 100644
--- a/08-def-type-hints/coordinates/coordinates_named.py
+++ b/08-def-type-hints/coordinates/coordinates_named.py
@@ -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()
diff --git a/08-def-type-hints/list.py b/08-def-type-hints/list.py
deleted file mode 100644
index 6d122e7..0000000
--- a/08-def-type-hints/list.py
+++ /dev/null
@@ -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)
diff --git a/08-def-type-hints/messages/hints_1/messages.py b/08-def-type-hints/messages/hints_1/messages.py
index c00e706..59b432e 100644
--- a/08-def-type-hints/messages/hints_1/messages.py
+++ b/08-def-type-hints/messages/hints_1/messages.py
@@ -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[]
diff --git a/08-def-type-hints/messages/hints_1/messages_test.py b/08-def-type-hints/messages/hints_1/messages_test.py
index 2a16f25..3688da1 100644
--- a/08-def-type-hints/messages/hints_1/messages_test.py
+++ b/08-def-type-hints/messages/hints_1/messages_test.py
@@ -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'
diff --git a/08-def-type-hints/messages/hints_2/messages.py b/08-def-type-hints/messages/hints_2/messages.py
index c43d85f..fd2a331 100644
--- a/08-def-type-hints/messages/hints_2/messages.py
+++ b/08-def-type-hints/messages/hints_2/messages.py
@@ -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[]
diff --git a/08-def-type-hints/messages/hints_2/messages_test.py b/08-def-type-hints/messages/hints_2/messages_test.py
index f7b2fe1..2678d9b 100644
--- a/08-def-type-hints/messages/hints_2/messages_test.py
+++ b/08-def-type-hints/messages/hints_2/messages_test.py
@@ -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[]
diff --git a/08-def-type-hints/messages/no_hints/messages.py b/08-def-type-hints/messages/no_hints/messages.py
index d7898c5..df037e7 100644
--- a/08-def-type-hints/messages/no_hints/messages.py
+++ b/08-def-type-hints/messages/no_hints/messages.py
@@ -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[]
diff --git a/08-def-type-hints/messages/no_hints/messages_test.py b/08-def-type-hints/messages/no_hints/messages_test.py
index a8ec100..09532d3 100644
--- a/08-def-type-hints/messages/no_hints/messages_test.py
+++ b/08-def-type-hints/messages/no_hints/messages_test.py
@@ -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'
diff --git a/08-def-type-hints/mode/mode_T.py b/08-def-type-hints/mode/mode_T.py
deleted file mode 100644
index cf88bbb..0000000
--- a/08-def-type-hints/mode/mode_T.py
+++ /dev/null
@@ -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()
diff --git a/08-def-type-hints/mode/mode_float.py b/08-def-type-hints/mode/mode_float.py
index 0684202..79308be 100644
--- a/08-def-type-hints/mode/mode_float.py
+++ b/08-def-type-hints/mode/mode_float.py
@@ -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()
\ No newline at end of file
+ demo()
diff --git a/08-def-type-hints/mode/mode_hashable.py b/08-def-type-hints/mode/mode_hashable.py
index aa7f313..57c3a30 100644
--- a/08-def-type-hints/mode/mode_hashable.py
+++ b/08-def-type-hints/mode/mode_hashable.py
@@ -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)
diff --git a/08-def-type-hints/mode/mode_hashable_wrong.py b/08-def-type-hints/mode/mode_hashable_wrong.py
deleted file mode 100644
index ff770a7..0000000
--- a/08-def-type-hints/mode/mode_hashable_wrong.py
+++ /dev/null
@@ -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()
\ No newline at end of file
diff --git a/08-def-type-hints/mode/mode_number.py b/08-def-type-hints/mode/mode_number.py
deleted file mode 100644
index 999387f..0000000
--- a/08-def-type-hints/mode/mode_number.py
+++ /dev/null
@@ -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()
\ No newline at end of file
diff --git a/08-def-type-hints/messages/hints_1/mypy.ini b/08-def-type-hints/mypy.ini
similarity index 50%
rename from 08-def-type-hints/messages/hints_1/mypy.ini
rename to 08-def-type-hints/mypy.ini
index f658867..ab42819 100644
--- a/08-def-type-hints/messages/hints_1/mypy.ini
+++ b/08-def-type-hints/mypy.ini
@@ -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
diff --git a/08-def-type-hints/passdrill.py b/08-def-type-hints/passdrill.py
deleted file mode 100755
index 83b1be1..0000000
--- a/08-def-type-hints/passdrill.py
+++ /dev/null
@@ -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)
diff --git a/08-def-type-hints/replacer.py b/08-def-type-hints/replacer.py
index 73ec77d..553a927 100644
--- a/08-def-type-hints/replacer.py
+++ b/08-def-type-hints/replacer.py
@@ -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:
diff --git a/08-def-type-hints/replacer2.py b/08-def-type-hints/replacer2.py
deleted file mode 100644
index 33cdaeb..0000000
--- a/08-def-type-hints/replacer2.py
+++ /dev/null
@@ -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()
diff --git a/08-def-type-hints/reveal_array.py b/08-def-type-hints/reveal_array.py
deleted file mode 100644
index 149c85a..0000000
--- a/08-def-type-hints/reveal_array.py
+++ /dev/null
@@ -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)
-
diff --git a/08-def-type-hints/sample.py b/08-def-type-hints/sample.py
index 0e7e2e9..04b0319 100644
--- a/08-def-type-hints/sample.py
+++ b/08-def-type-hints/sample.py
@@ -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)
diff --git a/08-def-type-hints/typevar_bounded.py b/08-def-type-hints/typevar_bounded.py
new file mode 100644
index 0000000..5c2adf0
--- /dev/null
+++ b/08-def-type-hints/typevar_bounded.py
@@ -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)
diff --git a/08-def-type-hints/typevars_constrained.py b/08-def-type-hints/typevars_constrained.py
index 2bfea13..8fb8d8e 100644
--- a/08-def-type-hints/typevars_constrained.py
+++ b/08-def-type-hints/typevars_constrained.py
@@ -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)
diff --git a/09-closure-deco/clockdeco.py b/09-closure-deco/clock/clockdeco.py
similarity index 100%
rename from 09-closure-deco/clockdeco.py
rename to 09-closure-deco/clock/clockdeco.py
diff --git a/09-closure-deco/clockdeco0.py b/09-closure-deco/clock/clockdeco0.py
similarity index 100%
rename from 09-closure-deco/clockdeco0.py
rename to 09-closure-deco/clock/clockdeco0.py
diff --git a/09-closure-deco/clockdeco_cls.py b/09-closure-deco/clock/clockdeco_cls.py
similarity index 100%
rename from 09-closure-deco/clockdeco_cls.py
rename to 09-closure-deco/clock/clockdeco_cls.py
diff --git a/09-closure-deco/clockdeco_demo.py b/09-closure-deco/clock/clockdeco_demo.py
similarity index 90%
rename from 09-closure-deco/clockdeco_demo.py
rename to 09-closure-deco/clock/clockdeco_demo.py
index 121b52f..bd41b5c 100644
--- a/09-closure-deco/clockdeco_demo.py
+++ b/09-closure-deco/clock/clockdeco_demo.py
@@ -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)
diff --git a/09-closure-deco/clockdeco_param.py b/09-closure-deco/clock/clockdeco_param.py
similarity index 100%
rename from 09-closure-deco/clockdeco_param.py
rename to 09-closure-deco/clock/clockdeco_param.py
diff --git a/09-closure-deco/clockdeco_param_demo1.py b/09-closure-deco/clock/clockdeco_param_demo1.py
similarity index 100%
rename from 09-closure-deco/clockdeco_param_demo1.py
rename to 09-closure-deco/clock/clockdeco_param_demo1.py
diff --git a/09-closure-deco/clockdeco_param_demo2.py b/09-closure-deco/clock/clockdeco_param_demo2.py
similarity index 100%
rename from 09-closure-deco/clockdeco_param_demo2.py
rename to 09-closure-deco/clock/clockdeco_param_demo2.py
diff --git a/10-dp-1class-func/classic_strategy.py b/10-dp-1class-func/classic_strategy.py
index dc72e19..d796a79 100644
--- a/10-dp-1class-func/classic_strategy.py
+++ b/10-dp-1class-func/classic_strategy.py
@@ -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(ann, cart, FidelityPromo()) # <4>
- >>> 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>
- >>> 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(joe, cart, LargeOrderPromo())
@@ -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[]
diff --git a/10-dp-1class-func/classic_strategy_test.py b/10-dp-1class-func/classic_strategy_test.py
index 8735811..143cd0c 100644
--- a/10-dp-1class-func/classic_strategy_test.py
+++ b/10-dp-1class-func/classic_strategy_test.py
@@ -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')
diff --git a/10-dp-1class-func/monkeytype/classic_strategy.py b/10-dp-1class-func/monkeytype/classic_strategy.py
index 0057d96..670c4cb 100644
--- a/10-dp-1class-func/monkeytype/classic_strategy.py
+++ b/10-dp-1class-func/monkeytype/classic_strategy.py
@@ -17,9 +17,9 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
- >>> 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(joe, cart, LargeOrderPromo())
diff --git a/10-dp-1class-func/promotions.py b/10-dp-1class-func/promotions.py
index 0f8a823..ee64b20 100644
--- a/10-dp-1class-func/promotions.py
+++ b/10-dp-1class-func/promotions.py
@@ -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)
diff --git a/10-dp-1class-func/pytypes/classic_strategy.py b/10-dp-1class-func/pytypes/classic_strategy.py
index d929ffa..710fd26 100644
--- a/10-dp-1class-func/pytypes/classic_strategy.py
+++ b/10-dp-1class-func/pytypes/classic_strategy.py
@@ -17,9 +17,9 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
- >>> 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(joe, cart, LargeOrderPromo())
diff --git a/10-dp-1class-func/requirements.txt b/10-dp-1class-func/requirements.txt
index 1826922..4c4f0e1 100644
--- a/10-dp-1class-func/requirements.txt
+++ b/10-dp-1class-func/requirements.txt
@@ -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
diff --git a/10-dp-1class-func/strategy.py b/10-dp-1class-func/strategy.py
index 2ab49d1..89a93ba 100644
--- a/10-dp-1class-func/strategy.py
+++ b/10-dp-1class-func/strategy.py
@@ -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(ann, cart, fidelity_promo)
- >>> 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>
- >>> 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(joe, cart, large_order_promo)
@@ -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''
-# <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[]
diff --git a/10-dp-1class-func/strategy_best.py b/10-dp-1class-func/strategy_best.py
index a7c4d74..68cab48 100644
--- a/10-dp-1class-func/strategy_best.py
+++ b/10-dp-1class-func/strategy_best.py
@@ -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(joe, banana_cart, best_promo) # <2>
@@ -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>
diff --git a/10-dp-1class-func/strategy_best2.py b/10-dp-1class-func/strategy_best2.py
index 62a993e..1c6ad9c 100644
--- a/10-dp-1class-func/strategy_best2.py
+++ b/10-dp-1class-func/strategy_best2.py
@@ -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(ann, cart, fidelity_promo)
- >>> 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)
- >>> 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(joe, cart, large_order_promo)
# tag::STRATEGY_BEST_TESTS[]
- >>> Order(joe, long_order, best_promo)
+ >>> Order(joe, long_cart, best_promo)
>>> Order(joe, banana_cart, best_promo)
@@ -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''
-
-
-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[]
diff --git a/10-dp-1class-func/strategy_best3.py b/10-dp-1class-func/strategy_best3.py
index 39ce3bf..8b16ceb 100644
--- a/10-dp-1class-func/strategy_best3.py
+++ b/10-dp-1class-func/strategy_best3.py
@@ -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(ann, cart, fidelity_promo)
- >>> 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)
- >>> 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(joe, cart, large_order_promo)
# tag::STRATEGY_BEST_TESTS[]
- >>> Order(joe, long_order, best_promo)
+ >>> Order(joe, long_cart, best_promo)
>>> Order(joe, banana_cart, best_promo)
@@ -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''
-
-
# 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[]
diff --git a/10-dp-1class-func/strategy_best4.py b/10-dp-1class-func/strategy_best4.py
index 955ac3a..8e124bb 100644
--- a/10-dp-1class-func/strategy_best4.py
+++ b/10-dp-1class-func/strategy_best4.py
@@ -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(ann, cart, fidelity)
+ >>> Order(ann, cart, fidelity_promo)
- >>> 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)
- >>> 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(joe, cart, large_order)
+ >>> Order(joe, cart, large_order_promo)
# tag::STRATEGY_BEST_TESTS[]
- >>> Order(joe, long_order, best_promo)
+ >>> Order(joe, long_cart, best_promo)
>>> Order(joe, banana_cart, best_promo)
@@ -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''
+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[]
diff --git a/10-dp-1class-func/strategy_param.py b/10-dp-1class-func/strategy_param.py
index 7318530..5490666 100644
--- a/10-dp-1class-func/strategy_param.py
+++ b/10-dp-1class-func/strategy_param.py
@@ -15,9 +15,9 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo(10))
- >>> 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(joe, cart, LargeOrderPromo(7))
diff --git a/10-dp-1class-func/strategy_param_test.py b/10-dp-1class-func/strategy_param_test.py
index d550e2b..cfb965e 100644
--- a/10-dp-1class-func/strategy_param_test.py
+++ b/10-dp-1class-func/strategy_param_test.py
@@ -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
diff --git a/10-dp-1class-func/strategy_test.py b/10-dp-1class-func/strategy_test.py
index bf225ae..c47440e 100644
--- a/10-dp-1class-func/strategy_test.py
+++ b/10-dp-1class-func/strategy_test.py
@@ -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')
diff --git a/10-dp-1class-func/untyped/classic_strategy.py b/10-dp-1class-func/untyped/classic_strategy.py
index b969780..61cccd5 100644
--- a/10-dp-1class-func/untyped/classic_strategy.py
+++ b/10-dp-1class-func/untyped/classic_strategy.py
@@ -17,9 +17,9 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
- >>> 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(joe, cart, LargeOrderPromo())
diff --git a/10-dp-1class-func/untyped/strategy.py b/10-dp-1class-func/untyped/strategy.py
index 1f8ad4c..518c69d 100644
--- a/10-dp-1class-func/untyped/strategy.py
+++ b/10-dp-1class-func/untyped/strategy.py
@@ -17,9 +17,9 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo) # <3>
- >>> 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(joe, cart, large_order_promo)
diff --git a/10-dp-1class-func/untyped/strategy_best.py b/10-dp-1class-func/untyped/strategy_best.py
index c0585f7..718b672 100644
--- a/10-dp-1class-func/untyped/strategy_best.py
+++ b/10-dp-1class-func/untyped/strategy_best.py
@@ -16,16 +16,16 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo)
- >>> 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(joe, cart, large_order_promo)
# tag::STRATEGY_BEST_TESTS[]
- >>> Order(joe, long_order, best_promo) # <1>
+ >>> Order(joe, long_cart, best_promo) # <1>
>>> Order(joe, banana_cart, best_promo) # <2>
diff --git a/10-dp-1class-func/untyped/strategy_best2.py b/10-dp-1class-func/untyped/strategy_best2.py
index 1f4700f..bfcd839 100644
--- a/10-dp-1class-func/untyped/strategy_best2.py
+++ b/10-dp-1class-func/untyped/strategy_best2.py
@@ -16,16 +16,16 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo)
- >>> 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(joe, cart, large_order_promo)
# tag::STRATEGY_BEST_TESTS[]
- >>> Order(joe, long_order, best_promo)
+ >>> Order(joe, long_cart, best_promo)
>>> Order(joe, banana_cart, best_promo)
diff --git a/10-dp-1class-func/untyped/strategy_best3.py b/10-dp-1class-func/untyped/strategy_best3.py
index 8d21ffc..f911ce3 100644
--- a/10-dp-1class-func/untyped/strategy_best3.py
+++ b/10-dp-1class-func/untyped/strategy_best3.py
@@ -17,16 +17,16 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo)
- >>> 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(joe, cart, large_order_promo)
# tag::STRATEGY_BEST_TESTS[]
- >>> Order(joe, long_order, best_promo)
+ >>> Order(joe, long_cart, best_promo)
>>> Order(joe, banana_cart, best_promo)
diff --git a/10-dp-1class-func/untyped/strategy_best4.py b/10-dp-1class-func/untyped/strategy_best4.py
index b523752..9573176 100644
--- a/10-dp-1class-func/untyped/strategy_best4.py
+++ b/10-dp-1class-func/untyped/strategy_best4.py
@@ -17,16 +17,16 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item)
- >>> 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(joe, cart, large_order)
# tag::STRATEGY_BEST_TESTS[]
- >>> Order(joe, long_order, best_promo)
+ >>> Order(joe, long_cart, best_promo)
>>> Order(joe, banana_cart, best_promo)
diff --git a/10-dp-1class-func/untyped/strategy_param.py b/10-dp-1class-func/untyped/strategy_param.py
index d5cc931..ab07132 100644
--- a/10-dp-1class-func/untyped/strategy_param.py
+++ b/10-dp-1class-func/untyped/strategy_param.py
@@ -15,9 +15,9 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo(10))
- >>> 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(joe, cart, large_order_promo(7))
diff --git a/10-dp-1class-func/untyped/strategy_param2.py b/10-dp-1class-func/untyped/strategy_param2.py
index 625bbca..332f49d 100644
--- a/10-dp-1class-func/untyped/strategy_param2.py
+++ b/10-dp-1class-func/untyped/strategy_param2.py
@@ -15,9 +15,9 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo(10))
- >>> 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(joe, cart, LargeOrderPromo(7))
diff --git a/11-pythonic-obj/mem_test.py b/11-pythonic-obj/mem_test.py
index 0d745f2..12fc54e 100644
--- a/11-pythonic-obj/mem_test.py
+++ b/11-pythonic-obj/mem_test.py
@@ -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]} ')
- 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,}')
diff --git a/11-pythonic-obj/patterns.py b/11-pythonic-obj/patterns.py
new file mode 100644
index 0000000..9317a4c
--- /dev/null
+++ b/11-pythonic-obj/patterns.py
@@ -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()
diff --git a/11-pythonic-obj/slots.rst b/11-pythonic-obj/slots.rst
new file mode 100644
index 0000000..351c9de
--- /dev/null
+++ b/11-pythonic-obj/slots.rst
@@ -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[]
diff --git a/11-pythonic-obj/vector2d_v3.py b/11-pythonic-obj/vector2d_v3.py
index 376a8a4..9ee716c 100644
--- a/11-pythonic-obj/vector2d_v3.py
+++ b/11-pythonic-obj/vector2d_v3.py
@@ -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):
diff --git a/11-pythonic-obj/vector2d_v3_prophash.py b/11-pythonic-obj/vector2d_v3_prophash.py
index 3552530..6d7dceb 100644
--- a/11-pythonic-obj/vector2d_v3_prophash.py
+++ b/11-pythonic-obj/vector2d_v3_prophash.py
@@ -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):
diff --git a/11-pythonic-obj/vector2d_v3_slots.py b/11-pythonic-obj/vector2d_v3_slots.py
index 20b6da4..c48fb5b 100644
--- a/11-pythonic-obj/vector2d_v3_slots.py
+++ b/11-pythonic-obj/vector2d_v3_slots.py
@@ -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):
diff --git a/12-seq-hacking/vector_v3.py b/12-seq-hacking/vector_v3.py
index 6ee18b5..c1f7859 100644
--- a/12-seq-hacking/vector_v3.py
+++ b/12-seq-hacking/vector_v3.py
@@ -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:
diff --git a/12-seq-hacking/vector_v4.py b/12-seq-hacking/vector_v4.py
index 95530eb..856f338 100644
--- a/12-seq-hacking/vector_v4.py
+++ b/12-seq-hacking/vector_v4.py
@@ -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)
diff --git a/12-seq-hacking/vector_v5.py b/12-seq-hacking/vector_v5.py
index ebbd523..09b3044 100644
--- a/12-seq-hacking/vector_v5.py
+++ b/12-seq-hacking/vector_v5.py
@@ -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)
diff --git a/13-protocol-abc/frenchdeck2.py b/13-protocol-abc/frenchdeck2.py
index e3ccc2c..612581d 100644
--- a/13-protocol-abc/frenchdeck2.py
+++ b/13-protocol-abc/frenchdeck2.py
@@ -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()
diff --git a/13-protocol-abc/typing/randompick_test.py b/13-protocol-abc/typing/randompick_test.py
index f090022..115c4d3 100644
--- a/13-protocol-abc/typing/randompick_test.py
+++ b/13-protocol-abc/typing/randompick_test.py
@@ -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)
diff --git a/13-protocol-abc/typing/vector2d_v4.py b/13-protocol-abc/typing/vector2d_v4.py
index 1e9d4ac..deaa824 100644
--- a/13-protocol-abc/typing/vector2d_v4.py
+++ b/13-protocol-abc/typing/vector2d_v4.py
@@ -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[]
diff --git a/13-protocol-abc/typing/vector2d_v5.py b/13-protocol-abc/typing/vector2d_v5.py
index ceda21c..378b826 100644
--- a/13-protocol-abc/typing/vector2d_v5.py
+++ b/13-protocol-abc/typing/vector2d_v5.py
@@ -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[]
diff --git a/14-inheritance/uppermixin.py b/14-inheritance/uppermixin.py
index d86ecea..12c4a17 100644
--- a/14-inheritance/uppermixin.py
+++ b/14-inheritance/uppermixin.py
@@ -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[]
diff --git a/15-more-types/protocol/abs_demo.py b/15-more-types/protocol/abs_demo.py
index 9cb7f80..6af78ea 100755
--- a/15-more-types/protocol/abs_demo.py
+++ b/15-more-types/protocol/abs_demo.py
@@ -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[]
diff --git a/16-op-overloading/vector2d_v3.py b/16-op-overloading/vector2d_v3.py
index 5812dcf..9ee716c 100644
--- a/16-op-overloading/vector2d_v3.py
+++ b/16-op-overloading/vector2d_v3.py
@@ -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):
diff --git a/16-op-overloading/vector_v6.py b/16-op-overloading/vector_v6.py
index 7154851..6ae4dcf 100644
--- a/16-op-overloading/vector_v6.py
+++ b/16-op-overloading/vector_v6.py
@@ -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)
diff --git a/16-op-overloading/vector_v7.py b/16-op-overloading/vector_v7.py
index e59c896..953622e 100644
--- a/16-op-overloading/vector_v7.py
+++ b/16-op-overloading/vector_v7.py
@@ -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)
diff --git a/16-op-overloading/vector_v8.py b/16-op-overloading/vector_v8.py
index ee2cb48..44c05fd 100644
--- a/16-op-overloading/vector_v8.py
+++ b/16-op-overloading/vector_v8.py
@@ -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)