updated from Atlas
This commit is contained in:
parent
cbd13885fc
commit
01e717b60a
@ -23,4 +23,14 @@ The copyright holder is Peter Norvig and the code is licensed under the
|
|||||||
[MIT license](https://github.com/norvig/pytudes/blob/60168bce8cdfacf57c92a5b2979f0b2e95367753/LICENSE).
|
[MIT license](https://github.com/norvig/pytudes/blob/60168bce8cdfacf57c92a5b2979f0b2e95367753/LICENSE).
|
||||||
|
|
||||||
|
|
||||||
|
## Changes to Norvig's code
|
||||||
|
|
||||||
|
I made small changes to the programs in `original/`:
|
||||||
|
|
||||||
|
* In `lis.py`:
|
||||||
|
* The `Procedure` class accepts a list of expressions as the `body`, and `__call__` evaluates those expressions in order, and returns the value of the last. This is consistent with Scheme's `lambda` syntax and provided a useful example for pattern matching.
|
||||||
|
* In the `elif` block for `'lambda'`, I added the `*` in front of the `*body` variable in the tuple unpacking to capture the expressions as a list, before calling the `Procedure` constructor.
|
||||||
|
|
||||||
|
* In `lispy.py` I made [changes and a pull request](https://github.com/norvig/pytudes/pull/106) to make it run on Python 3.
|
||||||
|
|
||||||
_Luciano Ramalho<br/>June 29, 2021_
|
_Luciano Ramalho<br/>June 29, 2021_
|
||||||
|
@ -58,7 +58,7 @@ Test ``find`` with single result::
|
|||||||
|
|
||||||
Test ``find`` with two results::
|
Test ``find`` with two results::
|
||||||
|
|
||||||
>>> find('chess', 'queen', last=0xFFFF) # doctest:+NORMALIZE_WHITESPACE
|
>>> find('chess', 'queen', end=0xFFFF) # doctest:+NORMALIZE_WHITESPACE
|
||||||
U+2655 ♕ WHITE CHESS QUEEN
|
U+2655 ♕ WHITE CHESS QUEEN
|
||||||
U+265B ♛ BLACK CHESS QUEEN
|
U+265B ♛ BLACK CHESS QUEEN
|
||||||
|
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
import sys
|
import sys
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
FIRST, LAST = ord(' '), sys.maxunicode # <1>
|
START, END = ord(' '), sys.maxunicode + 1 # <1>
|
||||||
|
|
||||||
def find(*query_words, first=FIRST, last=LAST): # <2>
|
def find(*query_words, start=START, end=END): # <2>
|
||||||
query = {w.upper() for w in query_words} # <3>
|
query = {w.upper() for w in query_words} # <3>
|
||||||
for code in range(first, last + 1):
|
for code in range(start, end):
|
||||||
char = chr(code) # <4>
|
char = chr(code) # <4>
|
||||||
name = unicodedata.name(char, None) # <5>
|
name = unicodedata.name(char, None) # <5>
|
||||||
if name and query.issubset(name.split()): # <6>
|
if name and query.issubset(name.split()): # <6>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ClubMember:
|
class ClubMember:
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
guests: list = field(default_factory=list)
|
guests: list = field(default_factory=list)
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ from dataclasses import dataclass
|
|||||||
# tag::CLUBMEMBER[]
|
# tag::CLUBMEMBER[]
|
||||||
@dataclass
|
@dataclass
|
||||||
class ClubMember:
|
class ClubMember:
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
guests: list = []
|
guests: list = []
|
||||||
# end::CLUBMEMBER[]
|
# end::CLUBMEMBER[]
|
||||||
|
@ -34,9 +34,7 @@ from club import ClubMember
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class HackerClubMember(ClubMember): # <1>
|
class HackerClubMember(ClubMember): # <1>
|
||||||
|
|
||||||
all_handles = set() # <2>
|
all_handles = set() # <2>
|
||||||
|
|
||||||
handle: str = '' # <3>
|
handle: str = '' # <3>
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
|
@ -35,9 +35,7 @@ from club import ClubMember
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class HackerClubMember(ClubMember):
|
class HackerClubMember(ClubMember):
|
||||||
|
|
||||||
all_handles: ClassVar[set[str]] = set()
|
all_handles: ClassVar[set[str]] = set()
|
||||||
|
|
||||||
handle: str = ''
|
handle: str = ''
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
|
@ -32,7 +32,7 @@ from enum import Enum, auto
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
|
|
||||||
class ResourceType(Enum): # <1>
|
class ResourceType(Enum): # <1>
|
||||||
BOOK = auto()
|
BOOK = auto()
|
||||||
EBOOK = auto()
|
EBOOK = auto()
|
||||||
VIDEO = auto()
|
VIDEO = auto()
|
||||||
|
@ -2,7 +2,6 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DemoDataClass:
|
class DemoDataClass:
|
||||||
|
|
||||||
a: int # <1>
|
a: int # <1>
|
||||||
b: float = 1.1 # <2>
|
b: float = 1.1 # <2>
|
||||||
c = 'spam' # <3>
|
c = 'spam' # <3>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import typing
|
import typing
|
||||||
|
|
||||||
class DemoNTClass(typing.NamedTuple):
|
class DemoNTClass(typing.NamedTuple):
|
||||||
|
|
||||||
a: int # <1>
|
a: int # <1>
|
||||||
b: float = 1.1 # <2>
|
b: float = 1.1 # <2>
|
||||||
c = 'spam' # <3>
|
c = 'spam' # <3>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
class DemoPlainClass:
|
class DemoPlainClass:
|
||||||
|
|
||||||
a: int # <1>
|
a: int # <1>
|
||||||
b: float = 1.1 # <2>
|
b: float = 1.1 # <2>
|
||||||
c = 'spam' # <3>
|
c = 'spam' # <3>
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
class Coordinate(NamedTuple):
|
class Coordinate(NamedTuple):
|
||||||
|
|
||||||
lat: float
|
lat: float
|
||||||
lon: float
|
lon: float
|
||||||
|
|
||||||
|
@ -13,8 +13,7 @@ This version has a field with a default value::
|
|||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
class Coordinate(NamedTuple):
|
class Coordinate(NamedTuple):
|
||||||
|
|
||||||
lat: float # <1>
|
lat: float # <1>
|
||||||
lon: float
|
lon: float
|
||||||
reference: str = 'WGS84' # <2>
|
reference: str = 'WGS84' # <2>
|
||||||
# end::COORDINATE[]
|
# end::COORDINATE[]
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import typing
|
import typing
|
||||||
|
|
||||||
class Coordinate(typing.NamedTuple):
|
class Coordinate(typing.NamedTuple):
|
||||||
|
|
||||||
lat: float
|
lat: float
|
||||||
lon: float
|
lon: float
|
||||||
|
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
"""
|
|
||||||
>>> clip('banana split', 5)
|
|
||||||
'banana'
|
|
||||||
>>> clip('banana split', 6)
|
|
||||||
'banana'
|
|
||||||
>>> clip('banana split', 7)
|
|
||||||
'banana'
|
|
||||||
>>> clip('banana split', 8)
|
|
||||||
'banana'
|
|
||||||
>>> clip('banana split', 11)
|
|
||||||
'banana'
|
|
||||||
>>> clip('banana split', 12)
|
|
||||||
'banana split'
|
|
||||||
>>> clip('banana-split', 3)
|
|
||||||
'banana-split'
|
|
||||||
|
|
||||||
Jess' tests:
|
|
||||||
|
|
||||||
>>> text = 'The quick brown fox jumps over the lazy dog.'
|
|
||||||
>>> clip14 = clip(text, max_len=14)
|
|
||||||
>>> clip14
|
|
||||||
'The quick'
|
|
||||||
>>> len(clip14)
|
|
||||||
9
|
|
||||||
>>> clip15 = clip(text, max_len=15)
|
|
||||||
>>> clip15
|
|
||||||
'The quick brown'
|
|
||||||
>>> len(clip15)
|
|
||||||
15
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# tag::CLIP[]
|
|
||||||
def clip(text, max_len=80):
|
|
||||||
"""Return max_len characters clipped at space if possible"""
|
|
||||||
text = text.rstrip()
|
|
||||||
if len(text) <= max_len or ' ' not in text:
|
|
||||||
return text
|
|
||||||
end = len(text)
|
|
||||||
space_at = text.rfind(' ', 0, max_len + 1)
|
|
||||||
if space_at >= 0:
|
|
||||||
end = space_at
|
|
||||||
else:
|
|
||||||
space_at = text.find(' ', max_len)
|
|
||||||
if space_at >= 0:
|
|
||||||
end = space_at
|
|
||||||
return text[:end].rstrip()
|
|
||||||
# end::CLIP[]
|
|
@ -1,9 +0,0 @@
|
|||||||
>>> from clip import clip
|
|
||||||
>>> clip.__defaults__
|
|
||||||
(80,)
|
|
||||||
>>> clip.__code__ # doctest: +ELLIPSIS
|
|
||||||
<code object clip at 0x...>
|
|
||||||
>>> clip.__code__.co_varnames
|
|
||||||
('text', 'max_len', 'end', 'space_at')
|
|
||||||
>>> clip.__code__.co_argcount
|
|
||||||
2
|
|
@ -1,12 +0,0 @@
|
|||||||
>>> from clip import clip
|
|
||||||
>>> from inspect import signature
|
|
||||||
>>> sig = signature(clip)
|
|
||||||
>>> sig
|
|
||||||
<Signature (text, max_len=80)>
|
|
||||||
>>> str(sig)
|
|
||||||
'(text, max_len=80)'
|
|
||||||
>>> for name, param in sig.parameters.items():
|
|
||||||
... print(param.kind, ':', name, '=', param.default)
|
|
||||||
...
|
|
||||||
POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
|
|
||||||
POSITIONAL_OR_KEYWORD : max_len = 80
|
|
@ -1,67 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from array import array
|
|
||||||
from typing import Mapping, MutableSequence, Callable, Iterable, Union, Any
|
|
||||||
|
|
||||||
|
|
||||||
OPERATORS: Mapping[str, Callable[[float, float], float]] = {
|
|
||||||
'+': lambda a, b: a + b,
|
|
||||||
'-': lambda a, b: a - b,
|
|
||||||
'*': lambda a, b: a * b,
|
|
||||||
'/': lambda a, b: a / b,
|
|
||||||
'^': lambda a, b: a ** b,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Stack = MutableSequence[float]
|
|
||||||
|
|
||||||
|
|
||||||
def parse_token(token: str) -> Union[str, float]:
|
|
||||||
try:
|
|
||||||
return float(token)
|
|
||||||
except ValueError:
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate(tokens: Iterable[str], stack: Stack) -> None:
|
|
||||||
for token in tokens:
|
|
||||||
atom = parse_token(token)
|
|
||||||
if isinstance(atom, float):
|
|
||||||
stack.append(atom)
|
|
||||||
else: # not float, must be operator
|
|
||||||
op = OPERATORS[atom]
|
|
||||||
x, y = stack.pop(), stack.pop()
|
|
||||||
result = op(y, x)
|
|
||||||
stack.append(result)
|
|
||||||
|
|
||||||
|
|
||||||
def display(s: Stack) -> str:
|
|
||||||
items = (repr(n) for n in s)
|
|
||||||
return ' │ '.join(items) + ' →'
|
|
||||||
|
|
||||||
|
|
||||||
def repl(input_fn: Callable[[Any], str] = input) -> None:
|
|
||||||
"""Read-Eval-Print-Loop"""
|
|
||||||
|
|
||||||
print('Use CTRL+C to quit.', file=sys.stderr)
|
|
||||||
stack: Stack = array('d')
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
line = input_fn('> ') # Read
|
|
||||||
except (EOFError, KeyboardInterrupt):
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
evaluate(line.split(), stack) # Eval
|
|
||||||
except IndexError:
|
|
||||||
print('*** Not enough arguments.', file=sys.stderr)
|
|
||||||
except KeyError as exc:
|
|
||||||
print('*** Unknown operator:', exc.args[0], file=sys.stderr)
|
|
||||||
print(display(stack)) # Print
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
repl()
|
|
@ -1,51 +0,0 @@
|
|||||||
from pytest import mark, approx # type: ignore
|
|
||||||
|
|
||||||
from dialogue import Dialogue # type: ignore
|
|
||||||
|
|
||||||
from calc import evaluate, repl, display, Stack
|
|
||||||
|
|
||||||
TOLERANCE = .0001
|
|
||||||
|
|
||||||
@mark.parametrize("source, want", [
|
|
||||||
('2', 2),
|
|
||||||
('2 3 +', 5),
|
|
||||||
('5 3 -', 2),
|
|
||||||
('3 5 * 2 +', 17),
|
|
||||||
('2 3 4 5 * * *', 120),
|
|
||||||
('1.1 1.1 1.1 + +', approx(3.3, TOLERANCE)),
|
|
||||||
('100 32 - 5 * 9 /', approx(37.78, TOLERANCE)),
|
|
||||||
])
|
|
||||||
def test_evaluate(source, want) -> None:
|
|
||||||
stack: Stack = []
|
|
||||||
evaluate(source.split(), stack)
|
|
||||||
assert want == stack[-1]
|
|
||||||
|
|
||||||
|
|
||||||
@mark.parametrize("value, want", [
|
|
||||||
([], ' →'),
|
|
||||||
([3.], '3.0 →'),
|
|
||||||
([3., 4., 5.], '3.0 │ 4.0 │ 5.0 →'),
|
|
||||||
])
|
|
||||||
def test_display(value, want) -> None:
|
|
||||||
assert want == display(value)
|
|
||||||
|
|
||||||
|
|
||||||
@mark.parametrize("session", [
|
|
||||||
"""
|
|
||||||
> 3
|
|
||||||
3.0 →
|
|
||||||
""",
|
|
||||||
"""
|
|
||||||
> 3 5 6
|
|
||||||
3.0 │ 5.0 │ 6.0 →
|
|
||||||
> *
|
|
||||||
3.0 │ 30.0 →
|
|
||||||
> -
|
|
||||||
-27.0 →
|
|
||||||
""",
|
|
||||||
])
|
|
||||||
def test_repl(capsys, session) -> None:
|
|
||||||
dlg = Dialogue(session)
|
|
||||||
repl(dlg.fake_input)
|
|
||||||
captured = capsys.readouterr()
|
|
||||||
assert dlg.session.strip() == captured.out.strip()
|
|
@ -15,7 +15,7 @@ characters which contain that word in their names. For example::
|
|||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from typing import Dict, Set, Iterator
|
from collections.abc import Iterator
|
||||||
|
|
||||||
RE_WORD = re.compile(r'\w+')
|
RE_WORD = re.compile(r'\w+')
|
||||||
STOP_CODE = sys.maxunicode + 1
|
STOP_CODE = sys.maxunicode + 1
|
||||||
@ -25,8 +25,8 @@ def tokenize(text: str) -> Iterator[str]: # <1>
|
|||||||
for match in RE_WORD.finditer(text):
|
for match in RE_WORD.finditer(text):
|
||||||
yield match.group().upper()
|
yield match.group().upper()
|
||||||
|
|
||||||
def name_index(start: int = 32, end: int = STOP_CODE) -> Dict[str, Set[str]]:
|
def name_index(start: int = 32, end: int = STOP_CODE) -> dict[str, set[str]]:
|
||||||
index: Dict[str, Set[str]] = {} # <2>
|
index: dict[str, set[str]] = {} # <2>
|
||||||
for char in (chr(i) for i in range(start, end)):
|
for char in (chr(i) for i in range(start, end)):
|
||||||
if name := unicodedata.name(char, ''): # <3>
|
if name := unicodedata.name(char, ''): # <3>
|
||||||
for word in tokenize(name):
|
for word in tokenize(name):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Tuple, Mapping
|
from collections.abc import Mapping
|
||||||
|
|
||||||
NAMES = {
|
NAMES = {
|
||||||
'aqua': 65535,
|
'aqua': 65535,
|
||||||
@ -19,7 +19,7 @@ NAMES = {
|
|||||||
'yellow': 16776960,
|
'yellow': 16776960,
|
||||||
}
|
}
|
||||||
|
|
||||||
def rgb2hex(color=Tuple[int, int, int]) -> str:
|
def rgb2hex(color: tuple[int, int, int]) -> str:
|
||||||
if any(c not in range(256) for c in color):
|
if any(c not in range(256) for c in color):
|
||||||
raise ValueError('Color components must be in range(256)')
|
raise ValueError('Color components must be in range(256)')
|
||||||
values = (f'{n % 256:02x}' for n in color)
|
values = (f'{n % 256:02x}' for n in color)
|
||||||
@ -27,7 +27,7 @@ def rgb2hex(color=Tuple[int, int, int]) -> str:
|
|||||||
|
|
||||||
HEX_ERROR = "Color must use format '#0099ff', got: {!r}"
|
HEX_ERROR = "Color must use format '#0099ff', got: {!r}"
|
||||||
|
|
||||||
def hex2rgb(color=str) -> Tuple[int, int, int]:
|
def hex2rgb(color: str) -> tuple[int, int, int]:
|
||||||
if len(color) != 7 or color[0] != '#':
|
if len(color) != 7 or color[0] != '#':
|
||||||
raise ValueError(HEX_ERROR.format(color))
|
raise ValueError(HEX_ERROR.format(color))
|
||||||
try:
|
try:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# tag::COLUMNIZE[]
|
# tag::COLUMNIZE[]
|
||||||
from typing import Sequence, List, Tuple
|
from collections.abc import Sequence
|
||||||
|
|
||||||
def columnize(sequence: Sequence[str], num_columns: int = 0) -> List[Tuple[str, ...]]:
|
def columnize(sequence: Sequence[str], num_columns: int = 0) -> list[tuple[str, ...]]:
|
||||||
if num_columns == 0:
|
if num_columns == 0:
|
||||||
num_columns = round(len(sequence) ** .5)
|
num_columns = round(len(sequence) ** .5)
|
||||||
num_rows, reminder = divmod(len(sequence), num_columns)
|
num_rows, reminder = divmod(len(sequence), num_columns)
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
from typing import Sequence, List, Tuple, TypeVar
|
|
||||||
|
|
||||||
T = TypeVar('T')
|
|
||||||
|
|
||||||
def columnize(sequence: Sequence[T], num_columns: int = 0) -> List[Tuple[T, ...]]:
|
|
||||||
if num_columns == 0:
|
|
||||||
num_columns = round(len(sequence) ** .5)
|
|
||||||
num_rows, reminder = divmod(len(sequence), num_columns)
|
|
||||||
num_rows += bool(reminder)
|
|
||||||
return [tuple(sequence[i::num_rows]) for i in range(num_rows)]
|
|
||||||
|
|
||||||
|
|
||||||
def demo() -> None:
|
|
||||||
nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India'
|
|
||||||
' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo'
|
|
||||||
' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu'
|
|
||||||
).split()
|
|
||||||
|
|
||||||
for line in columnize(nato):
|
|
||||||
for word in line:
|
|
||||||
print(f'{word:15}', end='')
|
|
||||||
print()
|
|
||||||
|
|
||||||
print()
|
|
||||||
for length in range(2, 21, 6):
|
|
||||||
values = list(range(1, length + 1))
|
|
||||||
for row in columnize(values):
|
|
||||||
for cell in row:
|
|
||||||
print(f'{cell:5}', end='')
|
|
||||||
print()
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
demo()
|
|
@ -1,26 +0,0 @@
|
|||||||
from typing import Sequence, List, Tuple
|
|
||||||
|
|
||||||
Row = Tuple[str, ...]
|
|
||||||
|
|
||||||
def columnize(sequence: Sequence[str], num_columns: int) -> List[Row]:
|
|
||||||
if num_columns == 0:
|
|
||||||
num_columns = round(len(sequence) ** .5)
|
|
||||||
num_rows, reminder = divmod(len(sequence), num_columns)
|
|
||||||
num_rows += bool(reminder)
|
|
||||||
return [tuple(sequence[i::num_rows]) for i in range(num_rows)]
|
|
||||||
|
|
||||||
|
|
||||||
def demo() -> None:
|
|
||||||
nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India'
|
|
||||||
' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo'
|
|
||||||
' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu'
|
|
||||||
).split()
|
|
||||||
|
|
||||||
for row in columnize(nato, 4):
|
|
||||||
for word in row:
|
|
||||||
print(f'{word:15}', end='')
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
demo()
|
|
@ -20,11 +20,14 @@ Example:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# tag::TOP[]
|
# tag::TOP[]
|
||||||
from typing import TypeVar, Iterable, List
|
from collections.abc import Iterable
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
from comparable import SupportsLessThan
|
from comparable import SupportsLessThan
|
||||||
|
|
||||||
LT = TypeVar('LT', bound=SupportsLessThan)
|
LT = TypeVar('LT', bound=SupportsLessThan)
|
||||||
|
|
||||||
def top(series: Iterable[LT], length: int) -> List[LT]:
|
def top(series: Iterable[LT], length: int) -> list[LT]:
|
||||||
return sorted(series, reverse=True)[:length]
|
ordered = sorted(series, reverse=True)
|
||||||
|
return ordered[:length]
|
||||||
# end::TOP[]
|
# end::TOP[]
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
from typing import Tuple, List, Iterator, TYPE_CHECKING
|
# tag::TOP_IMPORT[]
|
||||||
import pytest # type: ignore
|
from collections.abc import Iterator
|
||||||
|
from typing import TYPE_CHECKING # <1>
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from top import top
|
from top import top
|
||||||
|
# end::TOP_IMPORT[]
|
||||||
|
|
||||||
@pytest.mark.parametrize('series, length, expected', [
|
@pytest.mark.parametrize('series, length, expected', [
|
||||||
((1, 2, 3), 2, [3, 2]),
|
((1, 2, 3), 2, [3, 2]),
|
||||||
@ -8,9 +13,9 @@ from top import top
|
|||||||
((3, 3, 3), 1, [3]),
|
((3, 3, 3), 1, [3]),
|
||||||
])
|
])
|
||||||
def test_top(
|
def test_top(
|
||||||
series: Tuple[float, ...],
|
series: tuple[float, ...],
|
||||||
length: int,
|
length: int,
|
||||||
expected: List[float],
|
expected: list[float],
|
||||||
) -> None:
|
) -> None:
|
||||||
result = top(series, length)
|
result = top(series, length)
|
||||||
assert expected == result
|
assert expected == result
|
||||||
@ -18,13 +23,13 @@ def test_top(
|
|||||||
# tag::TOP_TEST[]
|
# tag::TOP_TEST[]
|
||||||
def test_top_tuples() -> None:
|
def test_top_tuples() -> None:
|
||||||
fruit = 'mango pear apple kiwi banana'.split()
|
fruit = 'mango pear apple kiwi banana'.split()
|
||||||
series: Iterator[Tuple[int, str]] = (
|
series: Iterator[tuple[int, str]] = ( # <2>
|
||||||
(len(s), s) for s in fruit)
|
(len(s), s) for s in fruit)
|
||||||
length = 3
|
length = 3
|
||||||
expected = [(6, 'banana'), (5, 'mango'), (5, 'apple')]
|
expected = [(6, 'banana'), (5, 'mango'), (5, 'apple')]
|
||||||
result = top(series, length)
|
result = top(series, length)
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING: # <3>
|
||||||
reveal_type(series)
|
reveal_type(series) # <4>
|
||||||
reveal_type(expected)
|
reveal_type(expected)
|
||||||
reveal_type(result)
|
reveal_type(result)
|
||||||
assert result == expected
|
assert result == expected
|
||||||
@ -34,7 +39,7 @@ def test_top_objects_error() -> None:
|
|||||||
series = [object() for _ in range(4)]
|
series = [object() for _ in range(4)]
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
reveal_type(series)
|
reveal_type(series)
|
||||||
with pytest.raises(TypeError) as exc:
|
with pytest.raises(TypeError) as excinfo:
|
||||||
top(series, 3)
|
top(series, 3) # <5>
|
||||||
assert "'<' not supported" in str(exc)
|
assert "'<' not supported" in str(excinfo.value)
|
||||||
# end::TOP_TEST[]
|
# end::TOP_TEST[]
|
||||||
|
@ -8,10 +8,10 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# tag::GEOHASH[]
|
# tag::GEOHASH[]
|
||||||
from geolib import geohash as gh # type: ignore
|
from geolib import geohash as gh # type: ignore # <1>
|
||||||
|
|
||||||
PRECISION = 9
|
PRECISION = 9
|
||||||
|
|
||||||
def geohash(lat_lon: tuple[float, float]) -> str:
|
def geohash(lat_lon: tuple[float, float]) -> str: # <2>
|
||||||
return gh.encode(*lat_lon, PRECISION)
|
return gh.encode(*lat_lon, PRECISION)
|
||||||
# end::GEOHASH[]
|
# end::GEOHASH[]
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# tag::GEOHASH[]
|
# tag::GEOHASH[]
|
||||||
from typing import Tuple, NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
from geolib import geohash as gh # type: ignore
|
from geolib import geohash as gh # type: ignore
|
||||||
|
|
||||||
@ -21,16 +21,21 @@ class Coordinate(NamedTuple):
|
|||||||
|
|
||||||
def geohash(lat_lon: Coordinate) -> str:
|
def geohash(lat_lon: Coordinate) -> str:
|
||||||
return gh.encode(*lat_lon, PRECISION)
|
return gh.encode(*lat_lon, PRECISION)
|
||||||
|
# end::GEOHASH[]
|
||||||
|
|
||||||
def display(lat_lon: Tuple[float, float]) -> str:
|
# tag::DISPLAY[]
|
||||||
|
def display(lat_lon: tuple[float, float]) -> str:
|
||||||
lat, lon = lat_lon
|
lat, lon = lat_lon
|
||||||
ns = 'N' if lat >= 0 else 'S'
|
ns = 'N' if lat >= 0 else 'S'
|
||||||
ew = 'E' if lon >= 0 else 'W'
|
ew = 'E' if lon >= 0 else 'W'
|
||||||
return f'{abs(lat):0.1f}°{ns}, {abs(lon):0.1f}°{ew}'
|
return f'{abs(lat):0.1f}°{ns}, {abs(lon):0.1f}°{ew}'
|
||||||
|
# end::DISPLAY[]
|
||||||
# end::GEOHASH[]
|
|
||||||
|
|
||||||
def demo():
|
def demo():
|
||||||
shanghai = 31.2304, 121.4737
|
shanghai = 31.2304, 121.4737
|
||||||
|
print(display(shanghai))
|
||||||
s = geohash(shanghai)
|
s = geohash(shanghai)
|
||||||
print(s)
|
print(s)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
demo()
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
from typing import List, Tuple
|
|
||||||
|
|
||||||
def tokenize(text: str) -> List[str]:
|
|
||||||
return text.upper().split()
|
|
||||||
|
|
||||||
l: List[str] = []
|
|
||||||
l.append(1)
|
|
||||||
print(l)
|
|
||||||
|
|
||||||
t: Tuple[str, float] = ('São Paulo', 12_176_866)
|
|
@ -5,16 +5,15 @@
|
|||||||
>>> show_count(1, 'bird')
|
>>> show_count(1, 'bird')
|
||||||
'1 bird'
|
'1 bird'
|
||||||
>>> show_count(0, 'bird')
|
>>> show_count(0, 'bird')
|
||||||
'no bird'
|
'no birds'
|
||||||
|
|
||||||
# end::SHOW_COUNT_DOCTEST[]
|
# end::SHOW_COUNT_DOCTEST[]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# tag::SHOW_COUNT[]
|
# tag::SHOW_COUNT[]
|
||||||
def show_count(count: int, word: str) -> str:
|
def show_count(count: int, word: str) -> str:
|
||||||
if count == 0:
|
if count == 1:
|
||||||
return f'no {word}'
|
return f'1 {word}'
|
||||||
elif count == 1:
|
count_str = str(count) if count else 'no'
|
||||||
return f'{count} {word}'
|
return f'{count_str} {word}s'
|
||||||
return f'{count} {word}s'
|
|
||||||
# end::SHOW_COUNT[]
|
# end::SHOW_COUNT[]
|
||||||
|
@ -7,11 +7,11 @@ from messages import show_count
|
|||||||
(1, '1 part'),
|
(1, '1 part'),
|
||||||
(2, '2 parts'),
|
(2, '2 parts'),
|
||||||
])
|
])
|
||||||
def test_show_count(qty, expected):
|
def test_show_count(qty: int, expected: str) -> None:
|
||||||
got = show_count(qty, 'part')
|
got = show_count(qty, 'part')
|
||||||
assert got == expected
|
assert got == expected
|
||||||
|
|
||||||
|
|
||||||
def test_show_count_zero():
|
def test_show_count_zero():
|
||||||
got = show_count(0, 'part')
|
got = show_count(0, 'part')
|
||||||
assert got == 'no part'
|
assert got == 'no parts'
|
||||||
|
@ -4,21 +4,22 @@
|
|||||||
>>> show_count(1, 'bird')
|
>>> show_count(1, 'bird')
|
||||||
'1 bird'
|
'1 bird'
|
||||||
>>> show_count(0, 'bird')
|
>>> show_count(0, 'bird')
|
||||||
'no bird'
|
'no birds'
|
||||||
>>> show_count(3, 'virus', 'viruses')
|
>>> show_count(3, 'virus', 'viruses')
|
||||||
'3 viruses'
|
'3 viruses'
|
||||||
|
>>> show_count(1, 'virus', 'viruses')
|
||||||
|
'1 virus'
|
||||||
|
>>> show_count(0, 'virus', 'viruses')
|
||||||
|
'no viruses'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# tag::SHOW_COUNT[]
|
# tag::SHOW_COUNT[]
|
||||||
def show_count(count: int, singular: str, plural: str = '') -> str:
|
def show_count(count: int, singular: str, plural: str = '') -> str:
|
||||||
if count == 0:
|
if count == 1:
|
||||||
return f'no {singular}'
|
|
||||||
elif count == 1:
|
|
||||||
return f'1 {singular}'
|
return f'1 {singular}'
|
||||||
else:
|
count_str = str(count) if count else 'no'
|
||||||
if plural:
|
if not plural:
|
||||||
return f'{count} {plural}'
|
plural = singular + 's'
|
||||||
else:
|
return f'{count_str} {plural}'
|
||||||
return f'{count} {singular}s'
|
|
||||||
|
|
||||||
# end::SHOW_COUNT[]
|
# end::SHOW_COUNT[]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from pytest import mark # type: ignore
|
from pytest import mark
|
||||||
|
|
||||||
from messages import show_count
|
from messages import show_count
|
||||||
|
|
||||||
@ -6,9 +6,9 @@ from messages import show_count
|
|||||||
@mark.parametrize('qty, expected', [
|
@mark.parametrize('qty, expected', [
|
||||||
(1, '1 part'),
|
(1, '1 part'),
|
||||||
(2, '2 parts'),
|
(2, '2 parts'),
|
||||||
(0, 'no part'),
|
(0, 'no parts'),
|
||||||
])
|
])
|
||||||
def test_show_count(qty, expected):
|
def test_show_count(qty: int, expected: str) -> None:
|
||||||
got = show_count(qty, 'part')
|
got = show_count(qty, 'part')
|
||||||
assert got == expected
|
assert got == expected
|
||||||
|
|
||||||
@ -17,9 +17,9 @@ def test_show_count(qty, expected):
|
|||||||
@mark.parametrize('qty, expected', [
|
@mark.parametrize('qty, expected', [
|
||||||
(1, '1 child'),
|
(1, '1 child'),
|
||||||
(2, '2 children'),
|
(2, '2 children'),
|
||||||
(0, 'no child'),
|
(0, 'no children'),
|
||||||
])
|
])
|
||||||
def test_irregular(qty, expected) -> None:
|
def test_irregular(qty: int, expected: str) -> None:
|
||||||
got = show_count(qty, 'child', 'children')
|
got = show_count(qty, 'child', 'children')
|
||||||
assert got == expected
|
assert got == expected
|
||||||
# end::TEST_IRREGULAR[]
|
# end::TEST_IRREGULAR[]
|
||||||
|
@ -5,16 +5,15 @@
|
|||||||
>>> show_count(1, 'bird')
|
>>> show_count(1, 'bird')
|
||||||
'1 bird'
|
'1 bird'
|
||||||
>>> show_count(0, 'bird')
|
>>> show_count(0, 'bird')
|
||||||
'no bird'
|
'no birds'
|
||||||
|
|
||||||
# end::SHOW_COUNT_DOCTEST[]
|
# end::SHOW_COUNT_DOCTEST[]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# tag::SHOW_COUNT[]
|
# tag::SHOW_COUNT[]
|
||||||
def show_count(count, word):
|
def show_count(count, word):
|
||||||
if count == 0:
|
if count == 1:
|
||||||
return f'no {word}'
|
return f'1 {word}'
|
||||||
elif count == 1:
|
count_str = str(count) if count else 'no'
|
||||||
return f'{count} {word}'
|
return f'{count_str} {word}s'
|
||||||
return f'{count} {word}s'
|
|
||||||
# end::SHOW_COUNT[]
|
# end::SHOW_COUNT[]
|
||||||
|
@ -12,4 +12,4 @@ def test_show_count(qty, expected):
|
|||||||
|
|
||||||
def test_show_count_zero():
|
def test_show_count_zero():
|
||||||
got = show_count(0, 'part')
|
got = show_count(0, 'part')
|
||||||
assert got == 'no part'
|
assert got == 'no parts'
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
from collections import Counter
|
|
||||||
from typing import Iterable, TypeVar
|
|
||||||
|
|
||||||
T = TypeVar('T')
|
|
||||||
|
|
||||||
def mode(data: Iterable[T]) -> T:
|
|
||||||
data = iter(data)
|
|
||||||
pairs = Counter(data).most_common(1)
|
|
||||||
if len(pairs) == 0:
|
|
||||||
raise ValueError('no mode for empty data')
|
|
||||||
return pairs[0][0]
|
|
||||||
|
|
||||||
|
|
||||||
def demo() -> None:
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
pop: list[set] = [set(), set()]
|
|
||||||
m = mode(pop)
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
reveal_type(pop)
|
|
||||||
reveal_type(m)
|
|
||||||
print(pop)
|
|
||||||
print(repr(m), type(m))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
demo()
|
|
@ -1,6 +1,6 @@
|
|||||||
# tag::MODE_FLOAT[]
|
# tag::MODE_FLOAT[]
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import Iterable
|
from collections.abc import Iterable
|
||||||
|
|
||||||
def mode(data: Iterable[float]) -> float:
|
def mode(data: Iterable[float]) -> float:
|
||||||
pairs = Counter(data).most_common(1)
|
pairs = Counter(data).most_common(1)
|
||||||
@ -20,4 +20,4 @@ def demo() -> None:
|
|||||||
print(repr(m), type(m))
|
print(repr(m), type(m))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
demo()
|
demo()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# tag::MODE_HASHABLE_T[]
|
# tag::MODE_HASHABLE_T[]
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import Iterable, Hashable, TypeVar
|
from collections.abc import Iterable, Hashable
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
HashableT = TypeVar('HashableT', bound=Hashable)
|
HashableT = TypeVar('HashableT', bound=Hashable)
|
||||||
|
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
# tag::MODE_FLOAT[]
|
|
||||||
from collections import Counter
|
|
||||||
from typing import Iterable, Hashable
|
|
||||||
|
|
||||||
def mode(data: Iterable[Hashable]) -> Hashable:
|
|
||||||
data = iter(data)
|
|
||||||
pairs = Counter(data).most_common(1)
|
|
||||||
if len(pairs) == 0:
|
|
||||||
raise ValueError('no mode for empty data')
|
|
||||||
return pairs[0][0]
|
|
||||||
# end::MODE_FLOAT[]
|
|
||||||
|
|
||||||
def demo() -> None:
|
|
||||||
import typing
|
|
||||||
pop = 'abracadabra'
|
|
||||||
m = mode(pop)
|
|
||||||
if typing.TYPE_CHECKING:
|
|
||||||
reveal_type(pop)
|
|
||||||
reveal_type(m)
|
|
||||||
print(pop)
|
|
||||||
print(m.upper(), type(m))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
demo()
|
|
@ -1,26 +0,0 @@
|
|||||||
from collections import Counter
|
|
||||||
from typing import Iterable, TypeVar
|
|
||||||
from decimal import Decimal
|
|
||||||
from fractions import Fraction
|
|
||||||
|
|
||||||
NumberT = TypeVar('NumberT', float, Decimal, Fraction)
|
|
||||||
|
|
||||||
def mode(data: Iterable[NumberT]) -> NumberT:
|
|
||||||
pairs = Counter(data).most_common(1)
|
|
||||||
if len(pairs) == 0:
|
|
||||||
raise ValueError('no mode for empty data')
|
|
||||||
return pairs[0][0]
|
|
||||||
|
|
||||||
|
|
||||||
def demo() -> None:
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
pop = [Fraction(1, 2), Fraction(1, 3), Fraction(1, 4), Fraction(1, 2)]
|
|
||||||
m = mode(pop)
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
reveal_type(pop)
|
|
||||||
reveal_type(m)
|
|
||||||
print(pop)
|
|
||||||
print(repr(m), type(m))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
demo()
|
|
@ -1,6 +1,4 @@
|
|||||||
[mypy]
|
[mypy]
|
||||||
python_version = 3.8
|
python_version = 3.9
|
||||||
warn_unused_configs = True
|
warn_unused_configs = True
|
||||||
disallow_incomplete_defs = True
|
disallow_incomplete_defs = True
|
||||||
[mypy-pytest]
|
|
||||||
ignore_missing_imports = True
|
|
@ -1,101 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
"""passdrill: typing drills for practicing passphrases
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from base64 import b64encode, b64decode
|
|
||||||
from getpass import getpass
|
|
||||||
from hashlib import scrypt
|
|
||||||
from typing import Sequence, Tuple
|
|
||||||
|
|
||||||
HASH_FILENAME = 'passdrill.hash'
|
|
||||||
HELP = 'Use -s to save passphrase hash for practice.'
|
|
||||||
|
|
||||||
|
|
||||||
def prompt() -> str:
|
|
||||||
print('WARNING: the passphrase WILL BE SHOWN so that you can check it!')
|
|
||||||
confirmed = ''
|
|
||||||
while confirmed != 'y':
|
|
||||||
passphrase = input('Type passphrase to hash (it will be echoed): ')
|
|
||||||
if passphrase in ('', 'q'):
|
|
||||||
print('ERROR: the passphrase cannot be empty or "q".')
|
|
||||||
continue
|
|
||||||
print(f'Passphrase to be hashed -> {passphrase}')
|
|
||||||
confirmed = input('Confirm (y/n): ').lower()
|
|
||||||
return passphrase
|
|
||||||
|
|
||||||
|
|
||||||
def crypto_hash(salt: bytes, passphrase: str) -> bytes:
|
|
||||||
octets = passphrase.encode('utf-8')
|
|
||||||
# Recommended parameters for interactive logins as of 2017:
|
|
||||||
# N=32768, r=8 and p=1 (https://godoc.org/golang.org/x/crypto/scrypt)
|
|
||||||
return scrypt(octets, salt=salt, n=32768, r=8, p=1, maxmem=2 ** 26)
|
|
||||||
|
|
||||||
|
|
||||||
def build_hash(passphrase: str) -> bytes:
|
|
||||||
salt = os.urandom(32)
|
|
||||||
payload = crypto_hash(salt, passphrase)
|
|
||||||
return b64encode(salt) + b':' + b64encode(payload)
|
|
||||||
|
|
||||||
|
|
||||||
def save_hash() -> None:
|
|
||||||
salted_hash = build_hash(prompt())
|
|
||||||
with open(HASH_FILENAME, 'wb') as fp:
|
|
||||||
fp.write(salted_hash)
|
|
||||||
print(f'Passphrase hash saved to {HASH_FILENAME}')
|
|
||||||
|
|
||||||
|
|
||||||
def load_hash() -> Tuple[bytes, bytes]:
|
|
||||||
try:
|
|
||||||
with open(HASH_FILENAME, 'rb') as fp:
|
|
||||||
salted_hash = fp.read()
|
|
||||||
except FileNotFoundError:
|
|
||||||
print('ERROR: passphrase hash file not found.', HELP)
|
|
||||||
# "standard" exit status codes:
|
|
||||||
# https://stackoverflow.com/questions/1101957/are-there-any-standard-exit-status-codes-in-linux/40484670#40484670
|
|
||||||
sys.exit(74) # input/output error
|
|
||||||
|
|
||||||
salt, stored_hash = salted_hash.split(b':')
|
|
||||||
return b64decode(salt), b64decode(stored_hash)
|
|
||||||
|
|
||||||
|
|
||||||
def practice() -> None:
|
|
||||||
salt, stored_hash = load_hash()
|
|
||||||
print('Type q to end practice.')
|
|
||||||
turn = 0
|
|
||||||
correct = 0
|
|
||||||
while True:
|
|
||||||
turn += 1
|
|
||||||
response = getpass(f'{turn}:')
|
|
||||||
if response == '':
|
|
||||||
print('Type q to quit.')
|
|
||||||
turn -= 1 # don't count this response
|
|
||||||
continue
|
|
||||||
elif response == 'q':
|
|
||||||
turn -= 1 # don't count this response
|
|
||||||
break
|
|
||||||
if crypto_hash(salt, response) == stored_hash:
|
|
||||||
correct += 1
|
|
||||||
answer = 'OK'
|
|
||||||
else:
|
|
||||||
answer = 'wrong'
|
|
||||||
print(f' {answer}\thits={correct}\tmisses={turn-correct}')
|
|
||||||
|
|
||||||
if turn:
|
|
||||||
print(f'\n{turn} turns. {correct / turn:.1%} correct.')
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv: Sequence[str]) -> None:
|
|
||||||
if len(argv) < 2:
|
|
||||||
practice()
|
|
||||||
elif len(argv) == 2 and argv[1] == '-s':
|
|
||||||
save_hash()
|
|
||||||
else:
|
|
||||||
print('ERROR: invalid argument.', HELP)
|
|
||||||
sys.exit(2) # command line usage error
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main(sys.argv)
|
|
@ -13,9 +13,9 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# tag::ZIP_REPLACE[]
|
# tag::ZIP_REPLACE[]
|
||||||
from typing import Iterable, Tuple
|
from collections.abc import Iterable
|
||||||
|
|
||||||
FromTo = Tuple[str, str] # <1>
|
FromTo = tuple[str, str] # <1>
|
||||||
|
|
||||||
def zip_replace(text: str, changes: Iterable[FromTo]) -> str: # <2>
|
def zip_replace(text: str, changes: Iterable[FromTo]) -> str: # <2>
|
||||||
for from_, to in changes:
|
for from_, to in changes:
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
"""
|
|
||||||
``zip_replace`` replaces multiple calls to ``str.replace``::
|
|
||||||
|
|
||||||
>>> changes = [
|
|
||||||
... ('(', ' ( '),
|
|
||||||
... (')', ' ) '),
|
|
||||||
... (' ', ' '),
|
|
||||||
... ]
|
|
||||||
>>> expr = '(+ 2 (* 3 7))'
|
|
||||||
>>> zip_replace(expr, changes)
|
|
||||||
' ( + 2 ( * 3 7 ) ) '
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Iterable, NamedTuple
|
|
||||||
|
|
||||||
|
|
||||||
class FromTo(NamedTuple):
|
|
||||||
from_: str
|
|
||||||
to: str
|
|
||||||
|
|
||||||
|
|
||||||
def zip_replace(text: str, changes: Iterable[FromTo], count: int = -1) -> str:
|
|
||||||
for from_, to in changes:
|
|
||||||
text = text.replace(from_, to, count)
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
def demo() -> None:
|
|
||||||
import doctest
|
|
||||||
failed, count = doctest.testmod()
|
|
||||||
print(f'{count-failed} of {count} doctests OK')
|
|
||||||
l33t = [FromTo(*p) for p in 'a4 e3 i1 o0'.split()]
|
|
||||||
text = 'mad skilled noob powned leet'
|
|
||||||
print(zip_replace(text, l33t))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
demo()
|
|
@ -1,8 +0,0 @@
|
|||||||
from array import array
|
|
||||||
from typing import MutableSequence
|
|
||||||
|
|
||||||
a = array('d')
|
|
||||||
reveal_type(a)
|
|
||||||
b: MutableSequence[float] = array('b')
|
|
||||||
reveal_type(b)
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
|||||||
# tag::SAMPLE[]
|
# tag::SAMPLE[]
|
||||||
|
from collections.abc import Sequence
|
||||||
from random import shuffle
|
from random import shuffle
|
||||||
from typing import Sequence, List, TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
|
||||||
def sample(population: Sequence[T], size: int) -> List[T]:
|
def sample(population: Sequence[T], size: int) -> list[T]:
|
||||||
if size < 1:
|
if size < 1:
|
||||||
raise ValueError('size must be >= 1')
|
raise ValueError('size must be >= 1')
|
||||||
result = list(population)
|
result = list(population)
|
||||||
|
11
08-def-type-hints/typevar_bounded.py
Normal file
11
08-def-type-hints/typevar_bounded.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from typing import TypeVar, TYPE_CHECKING
|
||||||
|
|
||||||
|
BT = TypeVar('BT', bound=float)
|
||||||
|
|
||||||
|
def triple2(a: BT) -> BT:
|
||||||
|
return a * 3
|
||||||
|
|
||||||
|
res2 = triple2(2)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
reveal_type(res2)
|
@ -7,7 +7,7 @@ RT = TypeVar('RT', float, Decimal)
|
|||||||
def triple1(a: RT) -> RT:
|
def triple1(a: RT) -> RT:
|
||||||
return a * 3
|
return a * 3
|
||||||
|
|
||||||
res1 = triple1(1, 2)
|
res1 = triple1(2)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
reveal_type(res1)
|
reveal_type(res1)
|
||||||
@ -19,7 +19,7 @@ BT = TypeVar('BT', bound=float)
|
|||||||
def triple2(a: BT) -> BT:
|
def triple2(a: BT) -> BT:
|
||||||
return a * 3
|
return a * 3
|
||||||
|
|
||||||
res2 = triple2(1, 2)
|
res2 = triple2(2)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
reveal_type(res2)
|
reveal_type(res2)
|
||||||
|
@ -1,17 +1,14 @@
|
|||||||
import time
|
import time
|
||||||
from clockdeco import clock
|
from clockdeco0 import clock
|
||||||
|
|
||||||
|
|
||||||
@clock
|
@clock
|
||||||
def snooze(seconds):
|
def snooze(seconds):
|
||||||
time.sleep(seconds)
|
time.sleep(seconds)
|
||||||
|
|
||||||
|
|
||||||
@clock
|
@clock
|
||||||
def factorial(n):
|
def factorial(n):
|
||||||
return 1 if n < 2 else n*factorial(n-1)
|
return 1 if n < 2 else n*factorial(n-1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print('*' * 40, 'Calling snooze(.123)')
|
print('*' * 40, 'Calling snooze(.123)')
|
||||||
snooze(.123)
|
snooze(.123)
|
@ -6,20 +6,20 @@
|
|||||||
|
|
||||||
>>> joe = Customer('John Doe', 0) # <1>
|
>>> joe = Customer('John Doe', 0) # <1>
|
||||||
>>> ann = Customer('Ann Smith', 1100)
|
>>> ann = Customer('Ann Smith', 1100)
|
||||||
>>> cart = [LineItem('banana', 4, .5), # <2>
|
>>> cart = (LineItem('banana', 4, Decimal('.5')), # <2>
|
||||||
... LineItem('apple', 10, 1.5),
|
... LineItem('apple', 10, Decimal('1.5')),
|
||||||
... LineItem('watermelon', 5, 5.0)]
|
... LineItem('watermelon', 5, Decimal(5)))
|
||||||
>>> Order(joe, cart, FidelityPromo()) # <3>
|
>>> Order(joe, cart, FidelityPromo()) # <3>
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
>>> Order(ann, cart, FidelityPromo()) # <4>
|
>>> Order(ann, cart, FidelityPromo()) # <4>
|
||||||
<Order total: 42.00 due: 39.90>
|
<Order total: 42.00 due: 39.90>
|
||||||
>>> banana_cart = [LineItem('banana', 30, .5), # <5>
|
>>> banana_cart = (LineItem('banana', 30, Decimal('.5')), # <5>
|
||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, Decimal('1.5')))
|
||||||
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
|
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> big_cart = [LineItem(str(item_code), 1, 1.0) # <7>
|
>>> long_cart = tuple(LineItem(str(sku), 1, Decimal(1)) # <7>
|
||||||
... for item_code in range(10)]
|
... for sku in range(10))
|
||||||
>>> Order(joe, big_cart, LargeOrderPromo()) # <8>
|
>>> Order(joe, long_cart, LargeOrderPromo()) # <8>
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, LargeOrderPromo())
|
>>> Order(joe, cart, LargeOrderPromo())
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
@ -29,44 +29,37 @@
|
|||||||
# tag::CLASSIC_STRATEGY[]
|
# tag::CLASSIC_STRATEGY[]
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
import typing
|
from collections.abc import Sequence
|
||||||
from typing import Sequence, Optional
|
from decimal import Decimal
|
||||||
|
from typing import NamedTuple, Optional
|
||||||
|
|
||||||
|
|
||||||
class Customer(typing.NamedTuple):
|
class Customer(NamedTuple):
|
||||||
name: str
|
name: str
|
||||||
fidelity: int
|
fidelity: int
|
||||||
|
|
||||||
|
|
||||||
class LineItem:
|
class LineItem(NamedTuple):
|
||||||
def __init__(self, product: str, quantity: int, price: float):
|
product: str
|
||||||
self.product = product
|
quantity: int
|
||||||
self.quantity = quantity
|
price: Decimal
|
||||||
self.price = price
|
|
||||||
|
|
||||||
def total(self):
|
def total(self) -> Decimal:
|
||||||
return self.price * self.quantity
|
return self.price * self.quantity
|
||||||
|
|
||||||
|
|
||||||
class Order: # the Context
|
class Order(NamedTuple): # the Context
|
||||||
def __init__(
|
customer: Customer
|
||||||
self,
|
cart: Sequence[LineItem]
|
||||||
customer: Customer,
|
promotion: Optional['Promotion'] = None
|
||||||
cart: Sequence[LineItem],
|
|
||||||
promotion: Optional['Promotion'] = None,
|
|
||||||
):
|
|
||||||
self.customer = customer
|
|
||||||
self.cart = list(cart)
|
|
||||||
self.promotion = promotion
|
|
||||||
|
|
||||||
def total(self) -> float:
|
def total(self) -> Decimal:
|
||||||
if not hasattr(self, '__total'):
|
totals = (item.total() for item in self.cart)
|
||||||
self.__total = sum(item.total() for item in self.cart)
|
return sum(totals, start=Decimal(0))
|
||||||
return self.__total
|
|
||||||
|
|
||||||
def due(self) -> float:
|
def due(self) -> Decimal:
|
||||||
if self.promotion is None:
|
if self.promotion is None:
|
||||||
discount = 0.0
|
discount = Decimal(0)
|
||||||
else:
|
else:
|
||||||
discount = self.promotion.discount(self)
|
discount = self.promotion.discount(self)
|
||||||
return self.total() - discount
|
return self.total() - discount
|
||||||
@ -77,36 +70,37 @@ class Order: # the Context
|
|||||||
|
|
||||||
class Promotion(ABC): # the Strategy: an abstract base class
|
class Promotion(ABC): # the Strategy: an abstract base class
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def discount(self, order: Order) -> float:
|
def discount(self, order: Order) -> Decimal:
|
||||||
"""Return discount as a positive dollar amount"""
|
"""Return discount as a positive dollar amount"""
|
||||||
|
|
||||||
|
|
||||||
class FidelityPromo(Promotion): # first Concrete Strategy
|
class FidelityPromo(Promotion): # first Concrete Strategy
|
||||||
"""5% discount for customers with 1000 or more fidelity points"""
|
"""5% discount for customers with 1000 or more fidelity points"""
|
||||||
|
|
||||||
def discount(self, order: Order) -> float:
|
def discount(self, order: Order) -> Decimal:
|
||||||
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
|
rate = Decimal('0.05')
|
||||||
|
if order.customer.fidelity >= 1000:
|
||||||
|
return order.total() * rate
|
||||||
|
return Decimal(0)
|
||||||
|
|
||||||
|
|
||||||
class BulkItemPromo(Promotion): # second Concrete Strategy
|
class BulkItemPromo(Promotion): # second Concrete Strategy
|
||||||
"""10% discount for each LineItem with 20 or more units"""
|
"""10% discount for each LineItem with 20 or more units"""
|
||||||
|
|
||||||
def discount(self, order: Order) -> float:
|
def discount(self, order: Order) -> Decimal:
|
||||||
discount = 0
|
discount = Decimal(0)
|
||||||
for item in order.cart:
|
for item in order.cart:
|
||||||
if item.quantity >= 20:
|
if item.quantity >= 20:
|
||||||
discount += item.total() * 0.1
|
discount += item.total() * Decimal('0.1')
|
||||||
return discount
|
return discount
|
||||||
|
|
||||||
|
|
||||||
class LargeOrderPromo(Promotion): # third Concrete Strategy
|
class LargeOrderPromo(Promotion): # third Concrete Strategy
|
||||||
"""7% discount for orders with 10 or more distinct items"""
|
"""7% discount for orders with 10 or more distinct items"""
|
||||||
|
|
||||||
def discount(self, order: Order) -> float:
|
def discount(self, order: Order) -> Decimal:
|
||||||
distinct_items = {item.product for item in order.cart}
|
distinct_items = {item.product for item in order.cart}
|
||||||
if len(distinct_items) >= 10:
|
if len(distinct_items) >= 10:
|
||||||
return order.total() * 0.07
|
return order.total() * Decimal('0.07')
|
||||||
return 0
|
return Decimal(0)
|
||||||
|
|
||||||
|
|
||||||
# end::CLASSIC_STRATEGY[]
|
# end::CLASSIC_STRATEGY[]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import List
|
from decimal import Decimal
|
||||||
|
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
|
|
||||||
@ -17,47 +17,49 @@ def customer_fidelity_1100() -> Customer:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def cart_plain() -> List[LineItem]:
|
def cart_plain() -> tuple[LineItem, ...]:
|
||||||
return [
|
return (
|
||||||
LineItem('banana', 4, 0.5),
|
LineItem('banana', 4, Decimal('0.5')),
|
||||||
LineItem('apple', 10, 1.5),
|
LineItem('apple', 10, Decimal('1.5')),
|
||||||
LineItem('watermelon', 5, 5.0),
|
LineItem('watermelon', 5, Decimal('5.0')),
|
||||||
]
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_fidelity_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
def test_fidelity_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
||||||
order = Order(customer_fidelity_0, cart_plain, FidelityPromo())
|
order = Order(customer_fidelity_0, cart_plain, FidelityPromo())
|
||||||
assert order.total() == 42.0
|
assert order.total() == 42
|
||||||
assert order.due() == 42.0
|
assert order.due() == 42
|
||||||
|
|
||||||
|
|
||||||
def test_fidelity_promo_with_discount(customer_fidelity_1100, cart_plain) -> None:
|
def test_fidelity_promo_with_discount(customer_fidelity_1100, cart_plain) -> None:
|
||||||
order = Order(customer_fidelity_1100, cart_plain, FidelityPromo())
|
order = Order(customer_fidelity_1100, cart_plain, FidelityPromo())
|
||||||
assert order.total() == 42.0
|
assert order.total() == 42
|
||||||
assert order.due() == 39.9
|
assert order.due() == Decimal('39.9')
|
||||||
|
|
||||||
|
|
||||||
def test_bulk_item_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
def test_bulk_item_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
||||||
order = Order(customer_fidelity_0, cart_plain, BulkItemPromo())
|
order = Order(customer_fidelity_0, cart_plain, BulkItemPromo())
|
||||||
assert order.total() == 42.0
|
assert order.total() == 42
|
||||||
assert order.due() == 42.0
|
assert order.due() == 42
|
||||||
|
|
||||||
|
|
||||||
def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None:
|
def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None:
|
||||||
cart = [LineItem('banana', 30, 0.5), LineItem('apple', 10, 1.5)]
|
cart = [LineItem('banana', 30, Decimal('0.5')),
|
||||||
|
LineItem('apple', 10, Decimal('1.5'))]
|
||||||
order = Order(customer_fidelity_0, cart, BulkItemPromo())
|
order = Order(customer_fidelity_0, cart, BulkItemPromo())
|
||||||
assert order.total() == 30.0
|
assert order.total() == 30
|
||||||
assert order.due() == 28.5
|
assert order.due() == Decimal('28.5')
|
||||||
|
|
||||||
|
|
||||||
def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
||||||
order = Order(customer_fidelity_0, cart_plain, LargeOrderPromo())
|
order = Order(customer_fidelity_0, cart_plain, LargeOrderPromo())
|
||||||
assert order.total() == 42.0
|
assert order.total() == 42
|
||||||
assert order.due() == 42.0
|
assert order.due() == 42
|
||||||
|
|
||||||
|
|
||||||
def test_large_order_promo_with_discount(customer_fidelity_0) -> None:
|
def test_large_order_promo_with_discount(customer_fidelity_0) -> None:
|
||||||
cart = [LineItem(str(item_code), 1, 1.0) for item_code in range(10)]
|
cart = [LineItem(str(item_code), 1, Decimal(1))
|
||||||
|
for item_code in range(10)]
|
||||||
order = Order(customer_fidelity_0, cart, LargeOrderPromo())
|
order = Order(customer_fidelity_0, cart, LargeOrderPromo())
|
||||||
assert order.total() == 10.0
|
assert order.total() == 10
|
||||||
assert order.due() == 9.3
|
assert order.due() == Decimal('9.3')
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, 1.5)]
|
||||||
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
|
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> long_order = [LineItem(str(item_code), 1, 1.0) # <7>
|
>>> long_cart = [LineItem(str(item_code), 1, 1.0) # <7>
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, long_order, LargeOrderPromo()) # <8>
|
>>> Order(joe, long_cart, LargeOrderPromo()) # <8>
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, LargeOrderPromo())
|
>>> Order(joe, cart, LargeOrderPromo())
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
def fidelity_promo(order):
|
from decimal import Decimal
|
||||||
|
from strategy import Order
|
||||||
|
|
||||||
|
def fidelity_promo(order: Order) -> Decimal: # <3>
|
||||||
"""5% discount for customers with 1000 or more fidelity points"""
|
"""5% discount for customers with 1000 or more fidelity points"""
|
||||||
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
|
if order.customer.fidelity >= 1000:
|
||||||
|
return order.total() * Decimal('0.05')
|
||||||
|
return Decimal(0)
|
||||||
|
|
||||||
|
|
||||||
def bulk_item_promo(order):
|
def bulk_item_promo(order: Order) -> Decimal:
|
||||||
"""10% discount for each LineItem with 20 or more units"""
|
"""10% discount for each LineItem with 20 or more units"""
|
||||||
discount = 0
|
discount = Decimal(0)
|
||||||
for item in order.cart:
|
for item in order.cart:
|
||||||
if item.quantity >= 20:
|
if item.quantity >= 20:
|
||||||
discount += item.total() * 0.1
|
discount += item.total() * Decimal('0.1')
|
||||||
return discount
|
return discount
|
||||||
|
|
||||||
|
|
||||||
def large_order_promo(order):
|
def large_order_promo(order: Order) -> Decimal:
|
||||||
"""7% discount for orders with 10 or more distinct items"""
|
"""7% discount for orders with 10 or more distinct items"""
|
||||||
distinct_items = {item.product for item in order.cart}
|
distinct_items = {item.product for item in order.cart}
|
||||||
if len(distinct_items) >= 10:
|
if len(distinct_items) >= 10:
|
||||||
return order.total() * 0.07
|
return order.total() * Decimal('0.07')
|
||||||
return 0
|
return Decimal(0)
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, 1.5)]
|
||||||
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
|
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> long_order = [LineItem(str(item_code), 1, 1.0) # <7>
|
>>> long_cart = [LineItem(str(item_code), 1, 1.0) # <7>
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, long_order, LargeOrderPromo()) # <8>
|
>>> Order(joe, long_cart, LargeOrderPromo()) # <8>
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, LargeOrderPromo())
|
>>> Order(joe, cart, LargeOrderPromo())
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
|
@ -1,13 +1,2 @@
|
|||||||
mypy==0.770
|
mypy==0.910
|
||||||
mypy-extensions==0.4.3
|
pytest==6.2.4
|
||||||
typed-ast==1.4.1
|
|
||||||
typing-extensions==3.7.4.1
|
|
||||||
attrs==19.3.0
|
|
||||||
more-itertools==8.2.0
|
|
||||||
packaging==20.3
|
|
||||||
pluggy==0.13.1
|
|
||||||
py==1.10.0
|
|
||||||
pyparsing==2.4.6
|
|
||||||
pytest==5.4.1
|
|
||||||
six==1.14.0
|
|
||||||
wcwidth==0.1.9
|
|
||||||
|
@ -6,20 +6,20 @@
|
|||||||
|
|
||||||
>>> joe = Customer('John Doe', 0) # <1>
|
>>> joe = Customer('John Doe', 0) # <1>
|
||||||
>>> ann = Customer('Ann Smith', 1100)
|
>>> ann = Customer('Ann Smith', 1100)
|
||||||
>>> cart = [LineItem('banana', 4, .5),
|
>>> cart = [LineItem('banana', 4, Decimal('.5')),
|
||||||
... LineItem('apple', 10, 1.5),
|
... LineItem('apple', 10, Decimal('1.5')),
|
||||||
... LineItem('watermelon', 5, 5.0)]
|
... LineItem('watermelon', 5, Decimal(5))]
|
||||||
>>> Order(joe, cart, fidelity_promo) # <2>
|
>>> Order(joe, cart, fidelity_promo) # <2>
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
>>> Order(ann, cart, fidelity_promo)
|
>>> Order(ann, cart, fidelity_promo)
|
||||||
<Order total: 42.00 due: 39.90>
|
<Order total: 42.00 due: 39.90>
|
||||||
>>> banana_cart = [LineItem('banana', 30, .5),
|
>>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
|
||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, Decimal('1.5'))]
|
||||||
>>> Order(joe, banana_cart, bulk_item_promo) # <3>
|
>>> Order(joe, banana_cart, bulk_item_promo) # <3>
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> big_cart = [LineItem(str(item_code), 1, 1.0)
|
>>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, big_cart, large_order_promo)
|
>>> Order(joe, long_cart, large_order_promo)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, large_order_promo)
|
>>> Order(joe, cart, large_order_promo)
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
@ -28,75 +28,71 @@
|
|||||||
"""
|
"""
|
||||||
# tag::STRATEGY[]
|
# tag::STRATEGY[]
|
||||||
|
|
||||||
import typing
|
from collections.abc import Sequence
|
||||||
from typing import Sequence, Optional, Callable
|
from dataclasses import dataclass
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Optional, Callable, NamedTuple
|
||||||
|
|
||||||
|
|
||||||
class Customer(typing.NamedTuple):
|
class Customer(NamedTuple):
|
||||||
name: str
|
name: str
|
||||||
fidelity: int
|
fidelity: int
|
||||||
|
|
||||||
|
|
||||||
class LineItem:
|
class LineItem(NamedTuple):
|
||||||
def __init__(self, product: str, quantity: int, price: float):
|
product: str
|
||||||
self.product = product
|
quantity: int
|
||||||
self.quantity = quantity
|
price: Decimal
|
||||||
self.price = price
|
|
||||||
|
|
||||||
def total(self):
|
def total(self):
|
||||||
return self.price * self.quantity
|
return self.price * self.quantity
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
class Order: # the Context
|
class Order: # the Context
|
||||||
def __init__(
|
customer: Customer
|
||||||
self,
|
cart: Sequence[LineItem]
|
||||||
customer: Customer,
|
promotion: Optional[Callable[['Order'], Decimal]] = None # <1>
|
||||||
cart: Sequence[LineItem],
|
|
||||||
promotion: Optional[Callable[['Order'], float]] = None,
|
|
||||||
) -> None:
|
|
||||||
self.customer = customer
|
|
||||||
self.cart = list(cart)
|
|
||||||
self.promotion = promotion
|
|
||||||
|
|
||||||
def total(self) -> float:
|
def total(self) -> Decimal:
|
||||||
if not hasattr(self, '__total'):
|
totals = (item.total() for item in self.cart)
|
||||||
self.__total = sum(item.total() for item in self.cart)
|
return sum(totals, start=Decimal(0))
|
||||||
return self.__total
|
|
||||||
|
|
||||||
def due(self) -> float:
|
def due(self) -> Decimal:
|
||||||
if self.promotion is None:
|
if self.promotion is None:
|
||||||
discount = 0.0
|
discount = Decimal(0)
|
||||||
else:
|
else:
|
||||||
discount = self.promotion(self) # <1>
|
discount = self.promotion(self) # <2>
|
||||||
return self.total() - discount
|
return self.total() - discount
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
|
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
|
||||||
|
|
||||||
|
|
||||||
# <2>
|
# <3>
|
||||||
|
|
||||||
|
|
||||||
def fidelity_promo(order: Order) -> float: # <3>
|
def fidelity_promo(order: Order) -> Decimal: # <4>
|
||||||
"""5% discount for customers with 1000 or more fidelity points"""
|
"""5% discount for customers with 1000 or more fidelity points"""
|
||||||
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
|
if order.customer.fidelity >= 1000:
|
||||||
|
return order.total() * Decimal('0.05')
|
||||||
|
return Decimal(0)
|
||||||
|
|
||||||
|
|
||||||
def bulk_item_promo(order: Order):
|
def bulk_item_promo(order: Order) -> Decimal:
|
||||||
"""10% discount for each LineItem with 20 or more units"""
|
"""10% discount for each LineItem with 20 or more units"""
|
||||||
discount = 0
|
discount = Decimal(0)
|
||||||
for item in order.cart:
|
for item in order.cart:
|
||||||
if item.quantity >= 20:
|
if item.quantity >= 20:
|
||||||
discount += item.total() * 0.1
|
discount += item.total() * Decimal('0.1')
|
||||||
return discount
|
return discount
|
||||||
|
|
||||||
|
|
||||||
def large_order_promo(order: Order):
|
def large_order_promo(order: Order) -> Decimal:
|
||||||
"""7% discount for orders with 10 or more distinct items"""
|
"""7% discount for orders with 10 or more distinct items"""
|
||||||
distinct_items = {item.product for item in order.cart}
|
distinct_items = {item.product for item in order.cart}
|
||||||
if len(distinct_items) >= 10:
|
if len(distinct_items) >= 10:
|
||||||
return order.total() * 0.07
|
return order.total() * Decimal('0.07')
|
||||||
return 0
|
return Decimal(0)
|
||||||
|
|
||||||
|
|
||||||
# end::STRATEGY[]
|
# end::STRATEGY[]
|
||||||
|
@ -3,19 +3,20 @@
|
|||||||
# selecting best promotion from static list of functions
|
# selecting best promotion from static list of functions
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
>>> from strategy import Customer, LineItem
|
||||||
>>> joe = Customer('John Doe', 0)
|
>>> joe = Customer('John Doe', 0)
|
||||||
>>> ann = Customer('Ann Smith', 1100)
|
>>> ann = Customer('Ann Smith', 1100)
|
||||||
>>> cart = [LineItem('banana', 4, .5),
|
>>> cart = [LineItem('banana', 4, Decimal('.5')),
|
||||||
... LineItem('apple', 10, 1.5),
|
... LineItem('apple', 10, Decimal('1.5')),
|
||||||
... LineItem('watermelon', 5, 5.0)]
|
... LineItem('watermelon', 5, Decimal(5))]
|
||||||
>>> banana_cart = [LineItem('banana', 30, .5),
|
>>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
|
||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, Decimal('1.5'))]
|
||||||
>>> big_cart = [LineItem(str(item_code), 1, 1.0)
|
>>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
|
|
||||||
# tag::STRATEGY_BEST_TESTS[]
|
# tag::STRATEGY_BEST_TESTS[]
|
||||||
|
|
||||||
>>> Order(joe, big_cart, best_promo) # <1>
|
>>> Order(joe, long_cart, best_promo) # <1>
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, banana_cart, best_promo) # <2>
|
>>> Order(joe, banana_cart, best_promo) # <2>
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
@ -25,7 +26,9 @@
|
|||||||
# end::STRATEGY_BEST_TESTS[]
|
# end::STRATEGY_BEST_TESTS[]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from strategy import Customer, LineItem, Order
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from strategy import Order
|
||||||
from strategy import fidelity_promo, bulk_item_promo, large_order_promo
|
from strategy import fidelity_promo, bulk_item_promo, large_order_promo
|
||||||
|
|
||||||
# tag::STRATEGY_BEST[]
|
# tag::STRATEGY_BEST[]
|
||||||
@ -33,9 +36,8 @@ from strategy import fidelity_promo, bulk_item_promo, large_order_promo
|
|||||||
promos = [fidelity_promo, bulk_item_promo, large_order_promo] # <1>
|
promos = [fidelity_promo, bulk_item_promo, large_order_promo] # <1>
|
||||||
|
|
||||||
|
|
||||||
def best_promo(order) -> float: # <2>
|
def best_promo(order: Order) -> Decimal: # <2>
|
||||||
"""Select best discount available
|
"""Compute the best discount available"""
|
||||||
"""
|
|
||||||
return max(promo(order) for promo in promos) # <3>
|
return max(promo(order) for promo in promos) # <3>
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,29 +3,31 @@
|
|||||||
# selecting best promotion from current module globals
|
# selecting best promotion from current module globals
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
>>> from decimal import Decimal
|
||||||
|
>>> from strategy import Customer, LineItem, Order
|
||||||
>>> joe = Customer('John Doe', 0)
|
>>> joe = Customer('John Doe', 0)
|
||||||
>>> ann = Customer('Ann Smith', 1100)
|
>>> ann = Customer('Ann Smith', 1100)
|
||||||
>>> cart = [LineItem('banana', 4, .5),
|
>>> cart = [LineItem('banana', 4, Decimal('.5')),
|
||||||
... LineItem('apple', 10, 1.5),
|
... LineItem('apple', 10, Decimal('1.5')),
|
||||||
... LineItem('watermelon', 5, 5.0)]
|
... LineItem('watermelon', 5, Decimal(5))]
|
||||||
>>> Order(joe, cart, fidelity_promo)
|
>>> Order(joe, cart, fidelity_promo)
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
>>> Order(ann, cart, fidelity_promo)
|
>>> Order(ann, cart, fidelity_promo)
|
||||||
<Order total: 42.00 due: 39.90>
|
<Order total: 42.00 due: 39.90>
|
||||||
>>> banana_cart = [LineItem('banana', 30, .5),
|
>>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
|
||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, Decimal('1.5'))]
|
||||||
>>> Order(joe, banana_cart, bulk_item_promo)
|
>>> Order(joe, banana_cart, bulk_item_promo)
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> long_order = [LineItem(str(item_code), 1, 1.0)
|
>>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, long_order, large_order_promo)
|
>>> Order(joe, long_cart, large_order_promo)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, large_order_promo)
|
>>> Order(joe, cart, large_order_promo)
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
|
|
||||||
# tag::STRATEGY_BEST_TESTS[]
|
# tag::STRATEGY_BEST_TESTS[]
|
||||||
|
|
||||||
>>> Order(joe, long_order, best_promo)
|
>>> Order(joe, long_cart, best_promo)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, banana_cart, best_promo)
|
>>> Order(joe, banana_cart, best_promo)
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
@ -35,78 +37,21 @@
|
|||||||
# end::STRATEGY_BEST_TESTS[]
|
# end::STRATEGY_BEST_TESTS[]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
Customer = namedtuple('Customer', 'name fidelity')
|
|
||||||
|
|
||||||
|
|
||||||
class LineItem:
|
|
||||||
def __init__(self, product, quantity, price):
|
|
||||||
self.product = product
|
|
||||||
self.quantity = quantity
|
|
||||||
self.price = price
|
|
||||||
|
|
||||||
def total(self):
|
|
||||||
return self.price * self.quantity
|
|
||||||
|
|
||||||
|
|
||||||
class Order: # the Context
|
|
||||||
def __init__(self, customer, cart, promotion=None):
|
|
||||||
self.customer = customer
|
|
||||||
self.cart = list(cart)
|
|
||||||
self.promotion = promotion
|
|
||||||
|
|
||||||
def total(self):
|
|
||||||
if not hasattr(self, '__total'):
|
|
||||||
self.__total = sum(item.total() for item in self.cart)
|
|
||||||
return self.__total
|
|
||||||
|
|
||||||
def due(self):
|
|
||||||
if self.promotion is None:
|
|
||||||
discount = 0
|
|
||||||
else:
|
|
||||||
discount = self.promotion(self)
|
|
||||||
return self.total() - discount
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
|
|
||||||
|
|
||||||
|
|
||||||
def fidelity_promo(order):
|
|
||||||
"""5% discount for customers with 1000 or more fidelity points"""
|
|
||||||
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
|
|
||||||
|
|
||||||
|
|
||||||
def bulk_item_promo(order):
|
|
||||||
"""10% discount for each LineItem with 20 or more units"""
|
|
||||||
discount = 0
|
|
||||||
for item in order.cart:
|
|
||||||
if item.quantity >= 20:
|
|
||||||
discount += item.total() * 0.1
|
|
||||||
return discount
|
|
||||||
|
|
||||||
|
|
||||||
def large_order_promo(order):
|
|
||||||
"""7% discount for orders with 10 or more distinct items"""
|
|
||||||
distinct_items = {item.product for item in order.cart}
|
|
||||||
if len(distinct_items) >= 10:
|
|
||||||
return order.total() * 0.07
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
# tag::STRATEGY_BEST2[]
|
# tag::STRATEGY_BEST2[]
|
||||||
|
from decimal import Decimal
|
||||||
|
from strategy import Order
|
||||||
|
from strategy import (
|
||||||
|
fidelity_promo, bulk_item_promo, large_order_promo # <1>
|
||||||
|
)
|
||||||
|
|
||||||
promos = [
|
promos = [promo for name, promo in globals().items() # <2>
|
||||||
globals()[name]
|
if name.endswith('_promo') and # <3>
|
||||||
for name in globals() # <1>
|
name != 'best_promo' # <4>
|
||||||
if name.endswith('_promo') and name != 'best_promo' # <2>
|
]
|
||||||
] # <3>
|
|
||||||
|
|
||||||
|
|
||||||
def best_promo(order):
|
def best_promo(order: Order) -> Decimal: # <5>
|
||||||
"""Select best discount available
|
"""Compute the best discount available"""
|
||||||
"""
|
return max(promo(order) for promo in promos)
|
||||||
return max(promo(order) for promo in promos) # <4>
|
|
||||||
|
|
||||||
|
|
||||||
# end::STRATEGY_BEST2[]
|
# end::STRATEGY_BEST2[]
|
||||||
|
@ -3,30 +3,32 @@
|
|||||||
# selecting best promotion from imported module
|
# selecting best promotion from imported module
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
>>> from decimal import Decimal
|
||||||
|
>>> from strategy import Customer, LineItem, Order
|
||||||
>>> from promotions import *
|
>>> from promotions import *
|
||||||
>>> joe = Customer('John Doe', 0)
|
>>> joe = Customer('John Doe', 0)
|
||||||
>>> ann = Customer('Ann Smith', 1100)
|
>>> ann = Customer('Ann Smith', 1100)
|
||||||
>>> cart = [LineItem('banana', 4, .5),
|
>>> cart = [LineItem('banana', 4, Decimal('.5')),
|
||||||
... LineItem('apple', 10, 1.5),
|
... LineItem('apple', 10, Decimal('1.5')),
|
||||||
... LineItem('watermelon', 5, 5.0)]
|
... LineItem('watermelon', 5, Decimal(5))]
|
||||||
>>> Order(joe, cart, fidelity_promo)
|
>>> Order(joe, cart, fidelity_promo)
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
>>> Order(ann, cart, fidelity_promo)
|
>>> Order(ann, cart, fidelity_promo)
|
||||||
<Order total: 42.00 due: 39.90>
|
<Order total: 42.00 due: 39.90>
|
||||||
>>> banana_cart = [LineItem('banana', 30, .5),
|
>>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
|
||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, Decimal('1.5'))]
|
||||||
>>> Order(joe, banana_cart, bulk_item_promo)
|
>>> Order(joe, banana_cart, bulk_item_promo)
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> long_order = [LineItem(str(item_code), 1, 1.0)
|
>>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, long_order, large_order_promo)
|
>>> Order(joe, long_cart, large_order_promo)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, large_order_promo)
|
>>> Order(joe, cart, large_order_promo)
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
|
|
||||||
# tag::STRATEGY_BEST_TESTS[]
|
# tag::STRATEGY_BEST_TESTS[]
|
||||||
|
|
||||||
>>> Order(joe, long_order, best_promo)
|
>>> Order(joe, long_cart, best_promo)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, banana_cart, best_promo)
|
>>> Order(joe, banana_cart, best_promo)
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
@ -36,55 +38,20 @@
|
|||||||
# end::STRATEGY_BEST_TESTS[]
|
# end::STRATEGY_BEST_TESTS[]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections import namedtuple
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
import promotions
|
|
||||||
|
|
||||||
Customer = namedtuple('Customer', 'name fidelity')
|
|
||||||
|
|
||||||
|
|
||||||
class LineItem:
|
|
||||||
def __init__(self, product, quantity, price):
|
|
||||||
self.product = product
|
|
||||||
self.quantity = quantity
|
|
||||||
self.price = price
|
|
||||||
|
|
||||||
def total(self):
|
|
||||||
return self.price * self.quantity
|
|
||||||
|
|
||||||
|
|
||||||
class Order: # the Context
|
|
||||||
def __init__(self, customer, cart, promotion=None):
|
|
||||||
self.customer = customer
|
|
||||||
self.cart = list(cart)
|
|
||||||
self.promotion = promotion
|
|
||||||
|
|
||||||
def total(self):
|
|
||||||
if not hasattr(self, '__total'):
|
|
||||||
self.__total = sum(item.total() for item in self.cart)
|
|
||||||
return self.__total
|
|
||||||
|
|
||||||
def due(self):
|
|
||||||
if self.promotion is None:
|
|
||||||
discount = 0
|
|
||||||
else:
|
|
||||||
discount = self.promotion(self)
|
|
||||||
return self.total() - discount
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
|
|
||||||
|
|
||||||
|
|
||||||
# tag::STRATEGY_BEST3[]
|
# tag::STRATEGY_BEST3[]
|
||||||
|
|
||||||
promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)]
|
from decimal import Decimal
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from strategy import Order
|
||||||
|
import promotions
|
||||||
|
|
||||||
|
|
||||||
def best_promo(order):
|
promos = [func for _, func in inspect.getmembers(promotions, inspect.isfunction)]
|
||||||
"""Select best discount available
|
|
||||||
"""
|
|
||||||
|
def best_promo(order: Order) -> Decimal:
|
||||||
|
"""Compute the best discount available"""
|
||||||
return max(promo(order) for promo in promos)
|
return max(promo(order) for promo in promos)
|
||||||
|
|
||||||
|
|
||||||
# end::STRATEGY_BEST3[]
|
# end::STRATEGY_BEST3[]
|
||||||
|
@ -4,29 +4,32 @@
|
|||||||
# registered by a decorator
|
# registered by a decorator
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
>>> from decimal import Decimal
|
||||||
|
>>> from strategy import Customer, LineItem, Order
|
||||||
|
>>> from promotions import *
|
||||||
>>> joe = Customer('John Doe', 0)
|
>>> joe = Customer('John Doe', 0)
|
||||||
>>> ann = Customer('Ann Smith', 1100)
|
>>> ann = Customer('Ann Smith', 1100)
|
||||||
>>> cart = [LineItem('banana', 4, .5),
|
>>> cart = [LineItem('banana', 4, Decimal('.5')),
|
||||||
... LineItem('apple', 10, 1.5),
|
... LineItem('apple', 10, Decimal('1.5')),
|
||||||
... LineItem('watermelon', 5, 5.0)]
|
... LineItem('watermelon', 5, Decimal(5))]
|
||||||
>>> Order(joe, cart, fidelity)
|
>>> Order(joe, cart, fidelity_promo)
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
>>> Order(ann, cart, fidelity)
|
>>> Order(ann, cart, fidelity_promo)
|
||||||
<Order total: 42.00 due: 39.90>
|
<Order total: 42.00 due: 39.90>
|
||||||
>>> banana_cart = [LineItem('banana', 30, .5),
|
>>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
|
||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, Decimal('1.5'))]
|
||||||
>>> Order(joe, banana_cart, bulk_item)
|
>>> Order(joe, banana_cart, bulk_item_promo)
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> long_order = [LineItem(str(item_code), 1, 1.0)
|
>>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, long_order, large_order)
|
>>> Order(joe, long_cart, large_order_promo)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, large_order)
|
>>> Order(joe, cart, large_order_promo)
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
|
|
||||||
# tag::STRATEGY_BEST_TESTS[]
|
# tag::STRATEGY_BEST_TESTS[]
|
||||||
|
|
||||||
>>> Order(joe, long_order, best_promo)
|
>>> Order(joe, long_cart, best_promo)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, banana_cart, best_promo)
|
>>> Order(joe, banana_cart, best_promo)
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
@ -36,47 +39,16 @@
|
|||||||
# end::STRATEGY_BEST_TESTS[]
|
# end::STRATEGY_BEST_TESTS[]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections import namedtuple
|
from decimal import Decimal
|
||||||
from typing import Callable, List
|
from typing import Callable
|
||||||
|
|
||||||
Customer = namedtuple('Customer', 'name fidelity')
|
|
||||||
|
|
||||||
|
|
||||||
class LineItem:
|
|
||||||
def __init__(self, product, quantity, price):
|
|
||||||
self.product = product
|
|
||||||
self.quantity = quantity
|
|
||||||
self.price = price
|
|
||||||
|
|
||||||
def total(self):
|
|
||||||
return self.price * self.quantity
|
|
||||||
|
|
||||||
|
|
||||||
class Order: # the Context
|
|
||||||
def __init__(self, customer, cart, promotion=None):
|
|
||||||
self.customer = customer
|
|
||||||
self.cart = list(cart)
|
|
||||||
self.promotion = promotion
|
|
||||||
|
|
||||||
def total(self):
|
|
||||||
if not hasattr(self, '__total'):
|
|
||||||
self.__total = sum(item.total() for item in self.cart)
|
|
||||||
return self.__total
|
|
||||||
|
|
||||||
def due(self):
|
|
||||||
if self.promotion is None:
|
|
||||||
discount = 0
|
|
||||||
else:
|
|
||||||
discount = self.promotion(self)
|
|
||||||
return self.total() - discount
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
|
|
||||||
|
|
||||||
|
from strategy import Order
|
||||||
|
|
||||||
# tag::STRATEGY_BEST4[]
|
# tag::STRATEGY_BEST4[]
|
||||||
|
|
||||||
Promotion = Callable[[Order], float] # <2>
|
Promotion = Callable[[Order], Decimal]
|
||||||
|
|
||||||
|
promos: list[Promotion] = [] # <1>
|
||||||
|
|
||||||
|
|
||||||
def promotion(promo: Promotion) -> Promotion: # <2>
|
def promotion(promo: Promotion) -> Promotion: # <2>
|
||||||
@ -84,38 +56,35 @@ def promotion(promo: Promotion) -> Promotion: # <2>
|
|||||||
return promo
|
return promo
|
||||||
|
|
||||||
|
|
||||||
promos: List[Promotion] = [] # <1>
|
def best_promo(order: Order) -> Decimal:
|
||||||
|
"""Compute the best discount available"""
|
||||||
|
return max(promo(order) for promo in promos) # <3>
|
||||||
|
|
||||||
|
|
||||||
@promotion # <3>
|
@promotion # <4>
|
||||||
def fidelity(order: Order) -> float:
|
def fidelity(order: Order) -> Decimal:
|
||||||
"""5% discount for customers with 1000 or more fidelity points"""
|
"""5% discount for customers with 1000 or more fidelity points"""
|
||||||
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
|
if order.customer.fidelity >= 1000:
|
||||||
|
return order.total() * Decimal('0.05')
|
||||||
|
return Decimal(0)
|
||||||
|
|
||||||
|
|
||||||
@promotion
|
@promotion
|
||||||
def bulk_item(order: Order) -> float:
|
def bulk_item(order: Order) -> Decimal:
|
||||||
"""10% discount for each LineItem with 20 or more units"""
|
"""10% discount for each LineItem with 20 or more units"""
|
||||||
discount = 0
|
discount = Decimal(0)
|
||||||
for item in order.cart:
|
for item in order.cart:
|
||||||
if item.quantity >= 20:
|
if item.quantity >= 20:
|
||||||
discount += item.total() * 0.1
|
discount += item.total() * Decimal('0.1')
|
||||||
return discount
|
return discount
|
||||||
|
|
||||||
|
|
||||||
@promotion
|
@promotion
|
||||||
def large_order(order: Order) -> float:
|
def large_order(order: Order) -> Decimal:
|
||||||
"""7% discount for orders with 10 or more distinct items"""
|
"""7% discount for orders with 10 or more distinct items"""
|
||||||
distinct_items = {item.product for item in order.cart}
|
distinct_items = {item.product for item in order.cart}
|
||||||
if len(distinct_items) >= 10:
|
if len(distinct_items) >= 10:
|
||||||
return order.total() * 0.07
|
return order.total() * Decimal('0.07')
|
||||||
return 0
|
return Decimal(0)
|
||||||
|
|
||||||
|
|
||||||
def best_promo(order: Order) -> float: # <4>
|
|
||||||
"""Select best discount available
|
|
||||||
"""
|
|
||||||
return max(promo(order) for promo in promos)
|
|
||||||
|
|
||||||
|
|
||||||
# end::STRATEGY_BEST4[]
|
# end::STRATEGY_BEST4[]
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, 1.5)]
|
||||||
>>> Order(joe, banana_cart, bulk_item_promo(10))
|
>>> Order(joe, banana_cart, bulk_item_promo(10))
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> big_cart = [LineItem(str(item_code), 1, 1.0)
|
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, big_cart, LargeOrderPromo(7))
|
>>> Order(joe, long_cart, LargeOrderPromo(7))
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, LargeOrderPromo(7))
|
>>> Order(joe, cart, LargeOrderPromo(7))
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
|
@ -47,7 +47,7 @@ def test_large_order_promo_with_discount(customer_fidelity_0) -> None:
|
|||||||
assert order.due() == 9.3
|
assert order.due() == 9.3
|
||||||
|
|
||||||
|
|
||||||
def test_general_discount(customer_fidelity_0, cart_plain) -> None:
|
def test_general_discount(customer_fidelity_1100, cart_plain) -> None:
|
||||||
general_promo: Promotion = functools.partial(general_discount, 5)
|
general_promo: Promotion = functools.partial(general_discount, 5)
|
||||||
order = Order(customer_fidelity_1100, cart_plain, general_promo)
|
order = Order(customer_fidelity_1100, cart_plain, general_promo)
|
||||||
assert order.total() == 42.0
|
assert order.total() == 42.0
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import List
|
from decimal import Decimal
|
||||||
|
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
|
|
||||||
@ -17,47 +17,50 @@ def customer_fidelity_1100() -> Customer:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def cart_plain() -> List[LineItem]:
|
def cart_plain() -> tuple[LineItem, ...]:
|
||||||
return [
|
return (
|
||||||
LineItem('banana', 4, 0.5),
|
LineItem('banana', 4, Decimal('0.5')),
|
||||||
LineItem('apple', 10, 1.5),
|
LineItem('apple', 10, Decimal('1.5')),
|
||||||
LineItem('watermelon', 5, 5.0),
|
LineItem('watermelon', 5, Decimal('5.0')),
|
||||||
]
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_fidelity_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
def test_fidelity_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
||||||
order = Order(customer_fidelity_0, cart_plain, fidelity_promo)
|
order = Order(customer_fidelity_0, cart_plain, fidelity_promo)
|
||||||
assert order.total() == 42.0
|
assert order.total() == 42
|
||||||
assert order.due() == 42.0
|
assert order.due() == 42
|
||||||
|
|
||||||
|
|
||||||
def test_fidelity_promo_with_discount(customer_fidelity_1100, cart_plain) -> None:
|
def test_fidelity_promo_with_discount(customer_fidelity_1100, cart_plain) -> None:
|
||||||
order = Order(customer_fidelity_1100, cart_plain, fidelity_promo)
|
order = Order(customer_fidelity_1100, cart_plain, fidelity_promo)
|
||||||
assert order.total() == 42.0
|
assert order.total() == 42
|
||||||
assert order.due() == 39.9
|
assert order.due() == Decimal('39.9')
|
||||||
|
|
||||||
|
|
||||||
def test_bulk_item_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
def test_bulk_item_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
||||||
order = Order(customer_fidelity_0, cart_plain, bulk_item_promo)
|
order = Order(customer_fidelity_0, cart_plain, bulk_item_promo)
|
||||||
assert order.total() == 42.0
|
assert order.total() == 42
|
||||||
assert order.due() == 42.0
|
assert order.due() == 42
|
||||||
|
|
||||||
|
|
||||||
def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None:
|
def test_bulk_item_promo_with_discount(customer_fidelity_0) -> None:
|
||||||
cart = [LineItem('banana', 30, 0.5), LineItem('apple', 10, 1.5)]
|
cart = [LineItem('banana', 30, Decimal('0.5')),
|
||||||
|
LineItem('apple', 10, Decimal('1.5'))]
|
||||||
order = Order(customer_fidelity_0, cart, bulk_item_promo)
|
order = Order(customer_fidelity_0, cart, bulk_item_promo)
|
||||||
assert order.total() == 30.0
|
assert order.total() == 30
|
||||||
assert order.due() == 28.5
|
assert order.due() == Decimal('28.5')
|
||||||
|
|
||||||
|
|
||||||
def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
||||||
order = Order(customer_fidelity_0, cart_plain, large_order_promo)
|
order = Order(customer_fidelity_0, cart_plain, large_order_promo)
|
||||||
assert order.total() == 42.0
|
assert order.total() == 42
|
||||||
assert order.due() == 42.0
|
assert order.due() == 42
|
||||||
|
|
||||||
|
|
||||||
def test_large_order_promo_with_discount(customer_fidelity_0) -> None:
|
def test_large_order_promo_with_discount(customer_fidelity_0) -> None:
|
||||||
cart = [LineItem(str(item_code), 1, 1.0) for item_code in range(10)]
|
|
||||||
|
cart = [LineItem(str(item_code), 1, Decimal(1))
|
||||||
|
for item_code in range(10)]
|
||||||
order = Order(customer_fidelity_0, cart, large_order_promo)
|
order = Order(customer_fidelity_0, cart, large_order_promo)
|
||||||
assert order.total() == 10.0
|
assert order.total() == 10
|
||||||
assert order.due() == 9.3
|
assert order.due() == Decimal('9.3')
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, 1.5)]
|
||||||
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
|
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> long_order = [LineItem(str(item_code), 1, 1.0) # <7>
|
>>> long_cart = [LineItem(str(item_code), 1, 1.0) # <7>
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, long_order, LargeOrderPromo()) # <8>
|
>>> Order(joe, long_cart, LargeOrderPromo()) # <8>
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, LargeOrderPromo())
|
>>> Order(joe, cart, LargeOrderPromo())
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, 1.5)]
|
||||||
>>> Order(joe, banana_cart, bulk_item_promo) # <3>
|
>>> Order(joe, banana_cart, bulk_item_promo) # <3>
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> long_order = [LineItem(str(item_code), 1, 1.0)
|
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, long_order, large_order_promo)
|
>>> Order(joe, long_cart, large_order_promo)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, large_order_promo)
|
>>> Order(joe, cart, large_order_promo)
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
|
@ -16,16 +16,16 @@
|
|||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, 1.5)]
|
||||||
>>> Order(joe, banana_cart, bulk_item_promo)
|
>>> Order(joe, banana_cart, bulk_item_promo)
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> long_order = [LineItem(str(item_code), 1, 1.0)
|
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, long_order, large_order_promo)
|
>>> Order(joe, long_cart, large_order_promo)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, large_order_promo)
|
>>> Order(joe, cart, large_order_promo)
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
|
|
||||||
# tag::STRATEGY_BEST_TESTS[]
|
# tag::STRATEGY_BEST_TESTS[]
|
||||||
|
|
||||||
>>> Order(joe, long_order, best_promo) # <1>
|
>>> Order(joe, long_cart, best_promo) # <1>
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, banana_cart, best_promo) # <2>
|
>>> Order(joe, banana_cart, best_promo) # <2>
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
|
@ -16,16 +16,16 @@
|
|||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, 1.5)]
|
||||||
>>> Order(joe, banana_cart, bulk_item_promo)
|
>>> Order(joe, banana_cart, bulk_item_promo)
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> long_order = [LineItem(str(item_code), 1, 1.0)
|
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, long_order, large_order_promo)
|
>>> Order(joe, long_cart, large_order_promo)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, large_order_promo)
|
>>> Order(joe, cart, large_order_promo)
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
|
|
||||||
# tag::STRATEGY_BEST_TESTS[]
|
# tag::STRATEGY_BEST_TESTS[]
|
||||||
|
|
||||||
>>> Order(joe, long_order, best_promo)
|
>>> Order(joe, long_cart, best_promo)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, banana_cart, best_promo)
|
>>> Order(joe, banana_cart, best_promo)
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
|
@ -17,16 +17,16 @@
|
|||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, 1.5)]
|
||||||
>>> Order(joe, banana_cart, bulk_item_promo)
|
>>> Order(joe, banana_cart, bulk_item_promo)
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> long_order = [LineItem(str(item_code), 1, 1.0)
|
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, long_order, large_order_promo)
|
>>> Order(joe, long_cart, large_order_promo)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, large_order_promo)
|
>>> Order(joe, cart, large_order_promo)
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
|
|
||||||
# tag::STRATEGY_BEST_TESTS[]
|
# tag::STRATEGY_BEST_TESTS[]
|
||||||
|
|
||||||
>>> Order(joe, long_order, best_promo)
|
>>> Order(joe, long_cart, best_promo)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, banana_cart, best_promo)
|
>>> Order(joe, banana_cart, best_promo)
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
|
@ -17,16 +17,16 @@
|
|||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, 1.5)]
|
||||||
>>> Order(joe, banana_cart, bulk_item)
|
>>> Order(joe, banana_cart, bulk_item)
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> long_order = [LineItem(str(item_code), 1, 1.0)
|
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, long_order, large_order)
|
>>> Order(joe, long_cart, large_order)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, large_order)
|
>>> Order(joe, cart, large_order)
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
|
|
||||||
# tag::STRATEGY_BEST_TESTS[]
|
# tag::STRATEGY_BEST_TESTS[]
|
||||||
|
|
||||||
>>> Order(joe, long_order, best_promo)
|
>>> Order(joe, long_cart, best_promo)
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, banana_cart, best_promo)
|
>>> Order(joe, banana_cart, best_promo)
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, 1.5)]
|
||||||
>>> Order(joe, banana_cart, bulk_item_promo(10))
|
>>> Order(joe, banana_cart, bulk_item_promo(10))
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> long_order = [LineItem(str(item_code), 1, 1.0)
|
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, long_order, large_order_promo(7))
|
>>> Order(joe, long_cart, large_order_promo(7))
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, large_order_promo(7))
|
>>> Order(joe, cart, large_order_promo(7))
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
... LineItem('apple', 10, 1.5)]
|
... LineItem('apple', 10, 1.5)]
|
||||||
>>> Order(joe, banana_cart, BulkItemPromo(10))
|
>>> Order(joe, banana_cart, BulkItemPromo(10))
|
||||||
<Order total: 30.00 due: 28.50>
|
<Order total: 30.00 due: 28.50>
|
||||||
>>> long_order = [LineItem(str(item_code), 1, 1.0)
|
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
|
||||||
... for item_code in range(10)]
|
... for item_code in range(10)]
|
||||||
>>> Order(joe, long_order, LargeOrderPromo(7))
|
>>> Order(joe, long_cart, LargeOrderPromo(7))
|
||||||
<Order total: 10.00 due: 9.30>
|
<Order total: 10.00 due: 9.30>
|
||||||
>>> Order(joe, cart, LargeOrderPromo(7))
|
>>> Order(joe, cart, LargeOrderPromo(7))
|
||||||
<Order total: 42.00 due: 42.00>
|
<Order total: 42.00 due: 42.00>
|
||||||
|
@ -4,20 +4,25 @@ import resource
|
|||||||
|
|
||||||
NUM_VECTORS = 10**7
|
NUM_VECTORS = 10**7
|
||||||
|
|
||||||
|
module = None
|
||||||
if len(sys.argv) == 2:
|
if len(sys.argv) == 2:
|
||||||
module_name = sys.argv[1].replace('.py', '')
|
module_name = sys.argv[1].replace('.py', '')
|
||||||
module = importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
else:
|
else:
|
||||||
print(f'Usage: {sys.argv[0]} <vector-module-to-test>')
|
print(f'Usage: {sys.argv[0]} <vector-module-to-test>')
|
||||||
sys.exit(2) # command line usage error
|
|
||||||
|
|
||||||
fmt = 'Selected Vector2d type: {.__name__}.{.__name__}'
|
if module is None:
|
||||||
print(fmt.format(module, module.Vector2d))
|
print('Running test with built-in `complex`')
|
||||||
|
cls = complex
|
||||||
|
else:
|
||||||
|
fmt = 'Selected Vector2d type: {.__name__}.{.__name__}'
|
||||||
|
print(fmt.format(module, module.Vector2d))
|
||||||
|
cls = module.Vector2d
|
||||||
|
|
||||||
mem_init = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
mem_init = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
||||||
print(f'Creating {NUM_VECTORS:,} Vector2d instances')
|
print(f'Creating {NUM_VECTORS:,} {cls.__qualname__!r} instances')
|
||||||
|
|
||||||
vectors = [module.Vector2d(3.0, 4.0) for i in range(NUM_VECTORS)]
|
vectors = [cls(3.0, 4.0) for i in range(NUM_VECTORS)]
|
||||||
|
|
||||||
mem_final = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
mem_final = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
||||||
print(f'Initial RAM usage: {mem_init:14,}')
|
print(f'Initial RAM usage: {mem_init:14,}')
|
||||||
|
53
11-pythonic-obj/patterns.py
Normal file
53
11-pythonic-obj/patterns.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from vector2d_v3 import Vector2d
|
||||||
|
|
||||||
|
# tag::KEYWORD_PATTERNS[]
|
||||||
|
def keyword_pattern_demo(v: Vector2d) -> None:
|
||||||
|
match v:
|
||||||
|
case Vector2d(x=0, y=0):
|
||||||
|
print(f'{v!r} is null')
|
||||||
|
case Vector2d(x=0):
|
||||||
|
print(f'{v!r} is vertical')
|
||||||
|
case Vector2d(y=0):
|
||||||
|
print(f'{v!r} is horizontal')
|
||||||
|
case Vector2d(x=x, y=y) if x==y:
|
||||||
|
print(f'{v!r} is diagonal')
|
||||||
|
case _:
|
||||||
|
print(f'{v!r} is awesome')
|
||||||
|
# end::KEYWORD_PATTERNS[]
|
||||||
|
|
||||||
|
# tag::POSITIONAL_PATTERNS[]
|
||||||
|
def positional_pattern_demo(v: Vector2d) -> None:
|
||||||
|
match v:
|
||||||
|
case Vector2d(0, 0):
|
||||||
|
print(f'{v!r} is null')
|
||||||
|
case Vector2d(0):
|
||||||
|
print(f'{v!r} is vertical')
|
||||||
|
case Vector2d(_, 0):
|
||||||
|
print(f'{v!r} is horizontal')
|
||||||
|
case Vector2d(x, y) if x==y:
|
||||||
|
print(f'{v!r} is diagonal')
|
||||||
|
case _:
|
||||||
|
print(f'{v!r} is awesome')
|
||||||
|
# end::POSITIONAL_PATTERNS[]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
vectors = (
|
||||||
|
Vector2d(1, 1),
|
||||||
|
Vector2d(0, 1),
|
||||||
|
Vector2d(1, 0),
|
||||||
|
Vector2d(1, 2),
|
||||||
|
Vector2d(0, 0),
|
||||||
|
)
|
||||||
|
print('KEYWORD PATTERNS:')
|
||||||
|
for vector in vectors:
|
||||||
|
keyword_pattern_demo(vector)
|
||||||
|
|
||||||
|
print('POSITIONAL PATTERNS:')
|
||||||
|
for vector in vectors:
|
||||||
|
positional_pattern_demo(vector)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
52
11-pythonic-obj/slots.rst
Normal file
52
11-pythonic-obj/slots.rst
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# tag::PIXEL[]
|
||||||
|
>>> class Pixel:
|
||||||
|
... __slots__ = ('x', 'y') # <1>
|
||||||
|
...
|
||||||
|
>>> p = Pixel() # <2>
|
||||||
|
>>> p.__dict__ # <3>
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Pixel' object has no attribute '__dict__'
|
||||||
|
>>> p.x = 10 # <4>
|
||||||
|
>>> p.y = 20
|
||||||
|
>>> p.color = 'red' # <5>
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Pixel' object has no attribute 'color'
|
||||||
|
|
||||||
|
# end::PIXEL[]
|
||||||
|
|
||||||
|
# tag::OPEN_PIXEL[]
|
||||||
|
>>> class OpenPixel(Pixel): # <1>
|
||||||
|
... pass
|
||||||
|
...
|
||||||
|
>>> op = OpenPixel()
|
||||||
|
>>> op.__dict__ # <2>
|
||||||
|
{}
|
||||||
|
>>> op.x = 8 # <3>
|
||||||
|
>>> op.__dict__ # <4>
|
||||||
|
{}
|
||||||
|
>>> op.x # <5>
|
||||||
|
8
|
||||||
|
>>> op.color = 'green' # <6>
|
||||||
|
>>> op.__dict__ # <7>
|
||||||
|
{'color': 'green'}
|
||||||
|
|
||||||
|
# end::OPEN_PIXEL[]
|
||||||
|
|
||||||
|
# tag::COLOR_PIXEL[]
|
||||||
|
>>> class ColorPixel(Pixel):
|
||||||
|
... __slots__ = ('color',) # <1>
|
||||||
|
>>> cp = ColorPixel()
|
||||||
|
>>> cp.__dict__ # <2>
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'ColorPixel' object has no attribute '__dict__'
|
||||||
|
>>> cp.x = 2
|
||||||
|
>>> cp.color = 'blue' # <3>
|
||||||
|
>>> cp.flavor = 'banana'
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'ColorPixel' object has no attribute 'flavor'
|
||||||
|
|
||||||
|
# end::COLOR_PIXEL[]
|
@ -72,7 +72,7 @@ Tests of `x` and `y` read-only properties:
|
|||||||
>>> v1.x = 123
|
>>> v1.x = 123
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
AttributeError: can't set attribute
|
AttributeError: can't set attribute 'x'
|
||||||
|
|
||||||
|
|
||||||
Tests of hashing:
|
Tests of hashing:
|
||||||
@ -90,6 +90,8 @@ from array import array
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
class Vector2d:
|
class Vector2d:
|
||||||
|
__match_args__ = ('x', 'y')
|
||||||
|
|
||||||
typecode = 'd'
|
typecode = 'd'
|
||||||
|
|
||||||
def __init__(self, x, y):
|
def __init__(self, x, y):
|
||||||
|
@ -72,7 +72,7 @@ Tests of `x` and `y` read-only properties:
|
|||||||
>>> v1.x = 123
|
>>> v1.x = 123
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
AttributeError: can't set attribute
|
AttributeError: can't set attribute 'x'
|
||||||
|
|
||||||
# end::VECTOR2D_V3_HASH_DEMO[]
|
# end::VECTOR2D_V3_HASH_DEMO[]
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ class Vector2d:
|
|||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return (i for i in (self.x, self.y)) # <6>
|
return (i for i in (self.x, self.y)) # <6>
|
||||||
|
|
||||||
# remaining methods follow (omitted in book listing)
|
# remaining methods: same as previous Vector2d
|
||||||
# end::VECTOR2D_V3_PROP[]
|
# end::VECTOR2D_V3_PROP[]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
@ -72,7 +72,7 @@ Tests of `x` and `y` read-only properties:
|
|||||||
>>> v1.x = 123
|
>>> v1.x = 123
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
AttributeError: can't set attribute
|
AttributeError: can't set attribute 'x'
|
||||||
|
|
||||||
Tests of hashing:
|
Tests of hashing:
|
||||||
|
|
||||||
@ -90,11 +90,10 @@ import math
|
|||||||
|
|
||||||
# tag::VECTOR2D_V3_SLOTS[]
|
# tag::VECTOR2D_V3_SLOTS[]
|
||||||
class Vector2d:
|
class Vector2d:
|
||||||
__slots__ = ('__x', '__y')
|
__match_args__ = ('x', 'y') # <1>
|
||||||
|
__slots__ = ('__x', '__y') # <2>
|
||||||
|
|
||||||
typecode = 'd'
|
typecode = 'd'
|
||||||
|
|
||||||
# methods follow (omitted in book listing)
|
|
||||||
# end::VECTOR2D_V3_SLOTS[]
|
# end::VECTOR2D_V3_SLOTS[]
|
||||||
|
|
||||||
def __init__(self, x, y):
|
def __init__(self, x, y):
|
||||||
|
@ -199,15 +199,17 @@ class Vector:
|
|||||||
return self._components[index]
|
return self._components[index]
|
||||||
|
|
||||||
# tag::VECTOR_V3_GETATTR[]
|
# tag::VECTOR_V3_GETATTR[]
|
||||||
shortcut_names = 'xyzt'
|
__match_args__ = ('x', 'y', 'z', 't') # <1>
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
cls = type(self) # <1>
|
cls = type(self) # <2>
|
||||||
if len(name) == 1: # <2>
|
try:
|
||||||
pos = cls.shortcut_names.find(name) # <3>
|
pos = cls.__match_args__.index(name) # <3>
|
||||||
if 0 <= pos < len(self._components): # <4>
|
except ValueError: # <4>
|
||||||
return self._components[pos]
|
pos = -1
|
||||||
msg = f'{cls.__name__!r} object has no attribute {name!r}' # <5>
|
if 0 <= pos < len(self._components): # <5>
|
||||||
|
return self._components[pos]
|
||||||
|
msg = f'{cls.__name__!r} object has no attribute {name!r}' # <6>
|
||||||
raise AttributeError(msg)
|
raise AttributeError(msg)
|
||||||
# end::VECTOR_V3_GETATTR[]
|
# end::VECTOR_V3_GETATTR[]
|
||||||
|
|
||||||
@ -215,8 +217,8 @@ class Vector:
|
|||||||
def __setattr__(self, name, value):
|
def __setattr__(self, name, value):
|
||||||
cls = type(self)
|
cls = type(self)
|
||||||
if len(name) == 1: # <1>
|
if len(name) == 1: # <1>
|
||||||
if name in cls.shortcut_names: # <2>
|
if name in cls.__match_args__: # <2>
|
||||||
error = 'read-only attribute {attr_name!r}'
|
error = 'readonly attribute {attr_name!r}'
|
||||||
elif name.islower(): # <3>
|
elif name.islower(): # <3>
|
||||||
error = "can't set attributes 'a' to 'z' in {cls_name!r}"
|
error = "can't set attributes 'a' to 'z' in {cls_name!r}"
|
||||||
else:
|
else:
|
||||||
|
@ -199,14 +199,16 @@ class Vector:
|
|||||||
index = operator.index(key)
|
index = operator.index(key)
|
||||||
return self._components[index]
|
return self._components[index]
|
||||||
|
|
||||||
shortcut_names = 'xyzt'
|
__match_args__ = ('x', 'y', 'z', 't')
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
cls = type(self)
|
cls = type(self)
|
||||||
if len(name) == 1:
|
try:
|
||||||
pos = cls.shortcut_names.find(name)
|
pos = cls.__match_args__.index(name)
|
||||||
if 0 <= pos < len(self._components):
|
except ValueError:
|
||||||
return self._components[pos]
|
pos = -1
|
||||||
|
if 0 <= pos < len(self._components):
|
||||||
|
return self._components[pos]
|
||||||
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||||
raise AttributeError(msg)
|
raise AttributeError(msg)
|
||||||
|
|
||||||
|
@ -242,14 +242,16 @@ class Vector:
|
|||||||
index = operator.index(key)
|
index = operator.index(key)
|
||||||
return self._components[index]
|
return self._components[index]
|
||||||
|
|
||||||
shortcut_names = 'xyzt'
|
__match_args__ = ('x', 'y', 'z', 't')
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
cls = type(self)
|
cls = type(self)
|
||||||
if len(name) == 1:
|
try:
|
||||||
pos = cls.shortcut_names.find(name)
|
pos = cls.__match_args__.index(name)
|
||||||
if 0 <= pos < len(self._components):
|
except ValueError:
|
||||||
return self._components[pos]
|
pos = -1
|
||||||
|
if 0 <= pos < len(self._components):
|
||||||
|
return self._components[pos]
|
||||||
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||||
raise AttributeError(msg)
|
raise AttributeError(msg)
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import collections
|
from collections import namedtuple, abc
|
||||||
|
|
||||||
Card = collections.namedtuple('Card', ['rank', 'suit'])
|
Card = namedtuple('Card', ['rank', 'suit'])
|
||||||
|
|
||||||
class FrenchDeck2(collections.MutableSequence):
|
class FrenchDeck2(abc.MutableSequence):
|
||||||
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
|
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
|
||||||
suits = 'spades diamonds clubs hearts'.split()
|
suits = 'spades diamonds clubs hearts'.split()
|
||||||
|
|
||||||
|
@ -12,14 +12,14 @@ class SimplePicker: # <2>
|
|||||||
return self._items.pop()
|
return self._items.pop()
|
||||||
|
|
||||||
def test_isinstance() -> None: # <4>
|
def test_isinstance() -> None: # <4>
|
||||||
popper = SimplePicker([1])
|
popper: RandomPicker = SimplePicker([1]) # <5>
|
||||||
assert isinstance(popper, RandomPicker)
|
assert isinstance(popper, RandomPicker) # <6>
|
||||||
|
|
||||||
def test_item_type() -> None: # <5>
|
def test_item_type() -> None: # <7>
|
||||||
items = [1, 2]
|
items = [1, 2]
|
||||||
popper = SimplePicker(items)
|
popper = SimplePicker(items)
|
||||||
item = popper.pick()
|
item = popper.pick()
|
||||||
assert item in items
|
assert item in items
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
reveal_type(item) # <6>
|
reveal_type(item) # <8>
|
||||||
assert isinstance(item, int)
|
assert isinstance(item, int)
|
||||||
|
@ -168,5 +168,5 @@ class Vector2d:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromcomplex(cls, datum):
|
def fromcomplex(cls, datum):
|
||||||
return Vector2d(datum.real, datum.imag) # <1>
|
return cls(datum.real, datum.imag) # <1>
|
||||||
# end::VECTOR2D_V4_COMPLEX[]
|
# end::VECTOR2D_V4_COMPLEX[]
|
||||||
|
@ -170,5 +170,5 @@ class Vector2d:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def fromcomplex(cls, datum: SupportsComplex) -> Vector2d: # <3>
|
def fromcomplex(cls, datum: SupportsComplex) -> Vector2d: # <3>
|
||||||
c = complex(datum) # <4>
|
c = complex(datum) # <4>
|
||||||
return Vector2d(c.real, c.imag)
|
return cls(c.real, c.imag)
|
||||||
# end::VECTOR2D_V5_COMPLEX[]
|
# end::VECTOR2D_V5_COMPLEX[]
|
||||||
|
@ -1,10 +1,39 @@
|
|||||||
"""UpperDict uppercases all string keys.
|
"""
|
||||||
|
Short demos
|
||||||
|
===========
|
||||||
|
|
||||||
Test for initializer. `str` keys are uppercased::
|
``UpperDict`` behaves like a case-insensitive mapping`::
|
||||||
|
|
||||||
|
# tag::UPPERDICT_DEMO[]
|
||||||
|
>>> d = UpperDict([('a', 'letter A'), (2, 'digit two')])
|
||||||
|
>>> list(d.keys())
|
||||||
|
['A', 2]
|
||||||
|
>>> d['b'] = 'letter B'
|
||||||
|
>>> 'b' in d
|
||||||
|
True
|
||||||
|
>>> d['a'], d.get('B')
|
||||||
|
('letter A', 'letter B')
|
||||||
|
>>> list(d.keys())
|
||||||
|
['A', 2, 'B']
|
||||||
|
|
||||||
|
# end::UPPERDICT_DEMO[]
|
||||||
|
|
||||||
|
And ``UpperCounter`` is also case-insensitive::
|
||||||
|
|
||||||
|
# tag::UPPERCOUNTER_DEMO[]
|
||||||
|
>>> c = UpperCounter('BaNanA')
|
||||||
|
>>> c.most_common()
|
||||||
|
[('A', 3), ('N', 2), ('B', 1)]
|
||||||
|
|
||||||
|
# end::UPPERCOUNTER_DEMO[]
|
||||||
|
|
||||||
|
Detailed tests
|
||||||
|
==============
|
||||||
|
|
||||||
|
UpperDict uppercases all string keys.
|
||||||
|
|
||||||
>>> d = UpperDict([('a', 'letter A'), ('B', 'letter B'), (2, 'digit two')])
|
>>> d = UpperDict([('a', 'letter A'), ('B', 'letter B'), (2, 'digit two')])
|
||||||
>>> list(d.keys())
|
|
||||||
['A', 'B', 2]
|
|
||||||
|
|
||||||
Tests for item retrieval using `d[key]` notation::
|
Tests for item retrieval using `d[key]` notation::
|
||||||
|
|
||||||
@ -82,14 +111,12 @@ Tests for count retrieval using `d[key]` notation::
|
|||||||
# tag::UPPERCASE_MIXIN[]
|
# tag::UPPERCASE_MIXIN[]
|
||||||
import collections
|
import collections
|
||||||
|
|
||||||
|
|
||||||
def _upper(key): # <1>
|
def _upper(key): # <1>
|
||||||
try:
|
try:
|
||||||
return key.upper()
|
return key.upper()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
||||||
class UpperCaseMixin: # <2>
|
class UpperCaseMixin: # <2>
|
||||||
def __setitem__(self, key, item):
|
def __setitem__(self, key, item):
|
||||||
super().__setitem__(_upper(key), item)
|
super().__setitem__(_upper(key), item)
|
||||||
@ -108,8 +135,6 @@ class UpperCaseMixin: # <2>
|
|||||||
class UpperDict(UpperCaseMixin, collections.UserDict): # <1>
|
class UpperDict(UpperCaseMixin, collections.UserDict): # <1>
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UpperCounter(UpperCaseMixin, collections.Counter): # <2>
|
class UpperCounter(UpperCaseMixin, collections.Counter): # <2>
|
||||||
"""Specialized 'Counter' that uppercases string keys""" # <3>
|
"""Specialized 'Counter' that uppercases string keys""" # <3>
|
||||||
|
|
||||||
# end::UPPERDICT[]
|
# end::UPPERDICT[]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# tag::ABS_DEMO[]
|
||||||
import math
|
import math
|
||||||
from typing import NamedTuple, SupportsAbs
|
from typing import NamedTuple, SupportsAbs
|
||||||
|
|
||||||
@ -30,3 +31,4 @@ assert is_unit(v3)
|
|||||||
assert is_unit(v4)
|
assert is_unit(v4)
|
||||||
|
|
||||||
print('OK')
|
print('OK')
|
||||||
|
# end::ABS_DEMO[]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
A 2-dimensional vector class
|
A two-dimensional vector class
|
||||||
|
|
||||||
>>> v1 = Vector2d(3, 4)
|
>>> v1 = Vector2d(3, 4)
|
||||||
>>> print(v1.x, v1.y)
|
>>> print(v1.x, v1.y)
|
||||||
@ -72,7 +72,7 @@ Tests of `x` and `y` read-only properties:
|
|||||||
>>> v1.x = 123
|
>>> v1.x = 123
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
AttributeError: can't set attribute
|
AttributeError: can't set attribute 'x'
|
||||||
|
|
||||||
|
|
||||||
Tests of hashing:
|
Tests of hashing:
|
||||||
@ -81,7 +81,7 @@ Tests of hashing:
|
|||||||
>>> v2 = Vector2d(3.1, 4.2)
|
>>> v2 = Vector2d(3.1, 4.2)
|
||||||
>>> hash(v1), hash(v2)
|
>>> hash(v1), hash(v2)
|
||||||
(7, 384307168202284039)
|
(7, 384307168202284039)
|
||||||
>>> len(set([v1, v2]))
|
>>> len({v1, v2})
|
||||||
2
|
2
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -90,6 +90,8 @@ from array import array
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
class Vector2d:
|
class Vector2d:
|
||||||
|
__match_args__ = ('x', 'y')
|
||||||
|
|
||||||
typecode = 'd'
|
typecode = 'd'
|
||||||
|
|
||||||
def __init__(self, x, y):
|
def __init__(self, x, y):
|
||||||
|
@ -305,14 +305,16 @@ class Vector:
|
|||||||
index = operator.index(key)
|
index = operator.index(key)
|
||||||
return self._components[index]
|
return self._components[index]
|
||||||
|
|
||||||
shortcut_names = 'xyzt'
|
__match_args__ = ('x', 'y', 'z', 't')
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
cls = type(self)
|
cls = type(self)
|
||||||
if len(name) == 1:
|
try:
|
||||||
pos = cls.shortcut_names.find(name)
|
pos = cls.__match_args__.index(name)
|
||||||
if 0 <= pos < len(self._components):
|
except ValueError:
|
||||||
return self._components[pos]
|
pos = -1
|
||||||
|
if 0 <= pos < len(self._components):
|
||||||
|
return self._components[pos]
|
||||||
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||||
raise AttributeError(msg)
|
raise AttributeError(msg)
|
||||||
|
|
||||||
|
@ -355,14 +355,16 @@ class Vector:
|
|||||||
index = operator.index(key)
|
index = operator.index(key)
|
||||||
return self._components[index]
|
return self._components[index]
|
||||||
|
|
||||||
shortcut_names = 'xyzt'
|
__match_args__ = ('x', 'y', 'z', 't')
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
cls = type(self)
|
cls = type(self)
|
||||||
if len(name) == 1:
|
try:
|
||||||
pos = cls.shortcut_names.find(name)
|
pos = cls.__match_args__.index(name)
|
||||||
if 0 <= pos < len(self._components):
|
except ValueError:
|
||||||
return self._components[pos]
|
pos = -1
|
||||||
|
if 0 <= pos < len(self._components):
|
||||||
|
return self._components[pos]
|
||||||
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||||
raise AttributeError(msg)
|
raise AttributeError(msg)
|
||||||
|
|
||||||
|
@ -361,14 +361,16 @@ class Vector:
|
|||||||
index = operator.index(key)
|
index = operator.index(key)
|
||||||
return self._components[index]
|
return self._components[index]
|
||||||
|
|
||||||
shortcut_names = 'xyzt'
|
__match_args__ = ('x', 'y', 'z', 't')
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
cls = type(self)
|
cls = type(self)
|
||||||
if len(name) == 1:
|
try:
|
||||||
pos = cls.shortcut_names.find(name)
|
pos = cls.__match_args__.index(name)
|
||||||
if 0 <= pos < len(self._components):
|
except ValueError:
|
||||||
return self._components[pos]
|
pos = -1
|
||||||
|
if 0 <= pos < len(self._components):
|
||||||
|
return self._components[pos]
|
||||||
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||||
raise AttributeError(msg)
|
raise AttributeError(msg)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user