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)