updated from Atlas

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

View File

@ -23,4 +23,14 @@ The copyright holder is Peter Norvig and the code is licensed under the
[MIT license](https://github.com/norvig/pytudes/blob/60168bce8cdfacf57c92a5b2979f0b2e95367753/LICENSE).
## 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_

View File

@ -58,7 +58,7 @@ Test ``find`` with single result::
Test ``find`` with two results::
>>> find('chess', 'queen', last=0xFFFF) # doctest:+NORMALIZE_WHITESPACE
>>> find('chess', 'queen', end=0xFFFF) # doctest:+NORMALIZE_WHITESPACE
U+2655 ♕ WHITE CHESS QUEEN
U+265B ♛ BLACK CHESS QUEEN

View File

@ -2,11 +2,11 @@
import sys
import unicodedata
FIRST, LAST = ord(' '), sys.maxunicode # <1>
START, END = ord(' '), sys.maxunicode + 1 # <1>
def find(*query_words, first=FIRST, last=LAST): # <2>
def find(*query_words, start=START, end=END): # <2>
query = {w.upper() for w in query_words} # <3>
for code in range(first, last + 1):
for code in range(start, end):
char = chr(code) # <4>
name = unicodedata.name(char, None) # <5>
if name and query.issubset(name.split()): # <6>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,48 +0,0 @@
"""
>>> clip('banana split', 5)
'banana'
>>> clip('banana split', 6)
'banana'
>>> clip('banana split', 7)
'banana'
>>> clip('banana split', 8)
'banana'
>>> clip('banana split', 11)
'banana'
>>> clip('banana split', 12)
'banana split'
>>> clip('banana-split', 3)
'banana-split'
Jess' tests:
>>> text = 'The quick brown fox jumps over the lazy dog.'
>>> clip14 = clip(text, max_len=14)
>>> clip14
'The quick'
>>> len(clip14)
9
>>> clip15 = clip(text, max_len=15)
>>> clip15
'The quick brown'
>>> len(clip15)
15
"""
# tag::CLIP[]
def clip(text, max_len=80):
"""Return max_len characters clipped at space if possible"""
text = text.rstrip()
if len(text) <= max_len or ' ' not in text:
return text
end = len(text)
space_at = text.rfind(' ', 0, max_len + 1)
if space_at >= 0:
end = space_at
else:
space_at = text.find(' ', max_len)
if space_at >= 0:
end = space_at
return text[:end].rstrip()
# end::CLIP[]

View File

@ -1,9 +0,0 @@
>>> from clip import clip
>>> clip.__defaults__
(80,)
>>> clip.__code__ # doctest: +ELLIPSIS
<code object clip at 0x...>
>>> clip.__code__.co_varnames
('text', 'max_len', 'end', 'space_at')
>>> clip.__code__.co_argcount
2

View File

@ -1,12 +0,0 @@
>>> from clip import clip
>>> from inspect import signature
>>> sig = signature(clip)
>>> sig
<Signature (text, max_len=80)>
>>> str(sig)
'(text, max_len=80)'
>>> for name, param in sig.parameters.items():
... print(param.kind, ':', name, '=', param.default)
...
POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
POSITIONAL_OR_KEYWORD : max_len = 80

View File

@ -1,67 +0,0 @@
#!/usr/bin/env python3
import sys
from array import array
from typing import Mapping, MutableSequence, Callable, Iterable, Union, Any
OPERATORS: Mapping[str, Callable[[float, float], float]] = {
'+': lambda a, b: a + b,
'-': lambda a, b: a - b,
'*': lambda a, b: a * b,
'/': lambda a, b: a / b,
'^': lambda a, b: a ** b,
}
Stack = MutableSequence[float]
def parse_token(token: str) -> Union[str, float]:
try:
return float(token)
except ValueError:
return token
def evaluate(tokens: Iterable[str], stack: Stack) -> None:
for token in tokens:
atom = parse_token(token)
if isinstance(atom, float):
stack.append(atom)
else: # not float, must be operator
op = OPERATORS[atom]
x, y = stack.pop(), stack.pop()
result = op(y, x)
stack.append(result)
def display(s: Stack) -> str:
items = (repr(n) for n in s)
return ''.join(items) + ''
def repl(input_fn: Callable[[Any], str] = input) -> None:
"""Read-Eval-Print-Loop"""
print('Use CTRL+C to quit.', file=sys.stderr)
stack: Stack = array('d')
while True:
try:
line = input_fn('> ') # Read
except (EOFError, KeyboardInterrupt):
break
try:
evaluate(line.split(), stack) # Eval
except IndexError:
print('*** Not enough arguments.', file=sys.stderr)
except KeyError as exc:
print('*** Unknown operator:', exc.args[0], file=sys.stderr)
print(display(stack)) # Print
print()
if __name__ == '__main__':
repl()

View File

@ -1,51 +0,0 @@
from pytest import mark, approx # type: ignore
from dialogue import Dialogue # type: ignore
from calc import evaluate, repl, display, Stack
TOLERANCE = .0001
@mark.parametrize("source, want", [
('2', 2),
('2 3 +', 5),
('5 3 -', 2),
('3 5 * 2 +', 17),
('2 3 4 5 * * *', 120),
('1.1 1.1 1.1 + +', approx(3.3, TOLERANCE)),
('100 32 - 5 * 9 /', approx(37.78, TOLERANCE)),
])
def test_evaluate(source, want) -> None:
stack: Stack = []
evaluate(source.split(), stack)
assert want == stack[-1]
@mark.parametrize("value, want", [
([], ''),
([3.], '3.0 →'),
([3., 4., 5.], '3.0 │ 4.0 │ 5.0 →'),
])
def test_display(value, want) -> None:
assert want == display(value)
@mark.parametrize("session", [
"""
> 3
3.0
""",
"""
> 3 5 6
3.0 5.0 6.0
> *
3.0 30.0
> -
-27.0
""",
])
def test_repl(capsys, session) -> None:
dlg = Dialogue(session)
repl(dlg.fake_input)
captured = capsys.readouterr()
assert dlg.session.strip() == captured.out.strip()

View File

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

View File

@ -1,4 +1,4 @@
from typing import Tuple, Mapping
from collections.abc import Mapping
NAMES = {
'aqua': 65535,
@ -19,7 +19,7 @@ NAMES = {
'yellow': 16776960,
}
def rgb2hex(color=Tuple[int, int, int]) -> str:
def rgb2hex(color: tuple[int, int, int]) -> str:
if any(c not in range(256) for c in color):
raise ValueError('Color components must be in range(256)')
values = (f'{n % 256:02x}' for n in color)
@ -27,7 +27,7 @@ def rgb2hex(color=Tuple[int, int, int]) -> str:
HEX_ERROR = "Color must use format '#0099ff', got: {!r}"
def hex2rgb(color=str) -> Tuple[int, int, int]:
def hex2rgb(color: str) -> tuple[int, int, int]:
if len(color) != 7 or color[0] != '#':
raise ValueError(HEX_ERROR.format(color))
try:

View File

@ -1,7 +1,7 @@
# tag::COLUMNIZE[]
from typing import Sequence, List, Tuple
from collections.abc import Sequence
def columnize(sequence: Sequence[str], num_columns: int = 0) -> List[Tuple[str, ...]]:
def columnize(sequence: Sequence[str], num_columns: int = 0) -> list[tuple[str, ...]]:
if num_columns == 0:
num_columns = round(len(sequence) ** .5)
num_rows, reminder = divmod(len(sequence), num_columns)

View File

@ -1,35 +0,0 @@
from typing import Sequence, List, Tuple, TypeVar
T = TypeVar('T')
def columnize(sequence: Sequence[T], num_columns: int = 0) -> List[Tuple[T, ...]]:
if num_columns == 0:
num_columns = round(len(sequence) ** .5)
num_rows, reminder = divmod(len(sequence), num_columns)
num_rows += bool(reminder)
return [tuple(sequence[i::num_rows]) for i in range(num_rows)]
def demo() -> None:
nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India'
' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo'
' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu'
).split()
for line in columnize(nato):
for word in line:
print(f'{word:15}', end='')
print()
print()
for length in range(2, 21, 6):
values = list(range(1, length + 1))
for row in columnize(values):
for cell in row:
print(f'{cell:5}', end='')
print()
print()
if __name__ == '__main__':
demo()

View File

@ -1,26 +0,0 @@
from typing import Sequence, List, Tuple
Row = Tuple[str, ...]
def columnize(sequence: Sequence[str], num_columns: int) -> List[Row]:
if num_columns == 0:
num_columns = round(len(sequence) ** .5)
num_rows, reminder = divmod(len(sequence), num_columns)
num_rows += bool(reminder)
return [tuple(sequence[i::num_rows]) for i in range(num_rows)]
def demo() -> None:
nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India'
' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo'
' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu'
).split()
for row in columnize(nato, 4):
for word in row:
print(f'{word:15}', end='')
print()
if __name__ == '__main__':
demo()

View File

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

View File

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

View File

@ -8,10 +8,10 @@
"""
# tag::GEOHASH[]
from geolib import geohash as gh # type: ignore
from geolib import geohash as gh # type: ignore # <1>
PRECISION = 9
def geohash(lat_lon: tuple[float, float]) -> str:
def geohash(lat_lon: tuple[float, float]) -> str: # <2>
return gh.encode(*lat_lon, PRECISION)
# end::GEOHASH[]

View File

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

View File

@ -1,10 +0,0 @@
from typing import List, Tuple
def tokenize(text: str) -> List[str]:
return text.upper().split()
l: List[str] = []
l.append(1)
print(l)
t: Tuple[str, float] = ('São Paulo', 12_176_866)

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
from pytest import mark # type: ignore
from pytest import mark
from messages import show_count
@ -6,9 +6,9 @@ from messages import show_count
@mark.parametrize('qty, expected', [
(1, '1 part'),
(2, '2 parts'),
(0, 'no part'),
(0, 'no parts'),
])
def test_show_count(qty, expected):
def test_show_count(qty: int, expected: str) -> None:
got = show_count(qty, 'part')
assert got == expected
@ -17,9 +17,9 @@ def test_show_count(qty, expected):
@mark.parametrize('qty, expected', [
(1, '1 child'),
(2, '2 children'),
(0, 'no child'),
(0, 'no children'),
])
def test_irregular(qty, expected) -> None:
def test_irregular(qty: int, expected: str) -> None:
got = show_count(qty, 'child', 'children')
assert got == expected
# end::TEST_IRREGULAR[]

View File

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

View File

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

View File

@ -1,26 +0,0 @@
from collections import Counter
from typing import Iterable, TypeVar
T = TypeVar('T')
def mode(data: Iterable[T]) -> T:
data = iter(data)
pairs = Counter(data).most_common(1)
if len(pairs) == 0:
raise ValueError('no mode for empty data')
return pairs[0][0]
def demo() -> None:
from typing import TYPE_CHECKING
pop: list[set] = [set(), set()]
m = mode(pop)
if TYPE_CHECKING:
reveal_type(pop)
reveal_type(m)
print(pop)
print(repr(m), type(m))
if __name__ == '__main__':
demo()

View File

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

View File

@ -1,6 +1,7 @@
# tag::MODE_HASHABLE_T[]
from collections import Counter
from typing import Iterable, Hashable, TypeVar
from collections.abc import Iterable, Hashable
from typing import TypeVar
HashableT = TypeVar('HashableT', bound=Hashable)

View File

@ -1,24 +0,0 @@
# tag::MODE_FLOAT[]
from collections import Counter
from typing import Iterable, Hashable
def mode(data: Iterable[Hashable]) -> Hashable:
data = iter(data)
pairs = Counter(data).most_common(1)
if len(pairs) == 0:
raise ValueError('no mode for empty data')
return pairs[0][0]
# end::MODE_FLOAT[]
def demo() -> None:
import typing
pop = 'abracadabra'
m = mode(pop)
if typing.TYPE_CHECKING:
reveal_type(pop)
reveal_type(m)
print(pop)
print(m.upper(), type(m))
if __name__ == '__main__':
demo()

View File

@ -1,26 +0,0 @@
from collections import Counter
from typing import Iterable, TypeVar
from decimal import Decimal
from fractions import Fraction
NumberT = TypeVar('NumberT', float, Decimal, Fraction)
def mode(data: Iterable[NumberT]) -> NumberT:
pairs = Counter(data).most_common(1)
if len(pairs) == 0:
raise ValueError('no mode for empty data')
return pairs[0][0]
def demo() -> None:
from typing import TYPE_CHECKING
pop = [Fraction(1, 2), Fraction(1, 3), Fraction(1, 4), Fraction(1, 2)]
m = mode(pop)
if TYPE_CHECKING:
reveal_type(pop)
reveal_type(m)
print(pop)
print(repr(m), type(m))
if __name__ == '__main__':
demo()

View File

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

View File

@ -1,101 +0,0 @@
#!/usr/bin/env python3
"""passdrill: typing drills for practicing passphrases
"""
import os
import sys
from base64 import b64encode, b64decode
from getpass import getpass
from hashlib import scrypt
from typing import Sequence, Tuple
HASH_FILENAME = 'passdrill.hash'
HELP = 'Use -s to save passphrase hash for practice.'
def prompt() -> str:
print('WARNING: the passphrase WILL BE SHOWN so that you can check it!')
confirmed = ''
while confirmed != 'y':
passphrase = input('Type passphrase to hash (it will be echoed): ')
if passphrase in ('', 'q'):
print('ERROR: the passphrase cannot be empty or "q".')
continue
print(f'Passphrase to be hashed -> {passphrase}')
confirmed = input('Confirm (y/n): ').lower()
return passphrase
def crypto_hash(salt: bytes, passphrase: str) -> bytes:
octets = passphrase.encode('utf-8')
# Recommended parameters for interactive logins as of 2017:
# N=32768, r=8 and p=1 (https://godoc.org/golang.org/x/crypto/scrypt)
return scrypt(octets, salt=salt, n=32768, r=8, p=1, maxmem=2 ** 26)
def build_hash(passphrase: str) -> bytes:
salt = os.urandom(32)
payload = crypto_hash(salt, passphrase)
return b64encode(salt) + b':' + b64encode(payload)
def save_hash() -> None:
salted_hash = build_hash(prompt())
with open(HASH_FILENAME, 'wb') as fp:
fp.write(salted_hash)
print(f'Passphrase hash saved to {HASH_FILENAME}')
def load_hash() -> Tuple[bytes, bytes]:
try:
with open(HASH_FILENAME, 'rb') as fp:
salted_hash = fp.read()
except FileNotFoundError:
print('ERROR: passphrase hash file not found.', HELP)
# "standard" exit status codes:
# https://stackoverflow.com/questions/1101957/are-there-any-standard-exit-status-codes-in-linux/40484670#40484670
sys.exit(74) # input/output error
salt, stored_hash = salted_hash.split(b':')
return b64decode(salt), b64decode(stored_hash)
def practice() -> None:
salt, stored_hash = load_hash()
print('Type q to end practice.')
turn = 0
correct = 0
while True:
turn += 1
response = getpass(f'{turn}:')
if response == '':
print('Type q to quit.')
turn -= 1 # don't count this response
continue
elif response == 'q':
turn -= 1 # don't count this response
break
if crypto_hash(salt, response) == stored_hash:
correct += 1
answer = 'OK'
else:
answer = 'wrong'
print(f' {answer}\thits={correct}\tmisses={turn-correct}')
if turn:
print(f'\n{turn} turns. {correct / turn:.1%} correct.')
def main(argv: Sequence[str]) -> None:
if len(argv) < 2:
practice()
elif len(argv) == 2 and argv[1] == '-s':
save_hash()
else:
print('ERROR: invalid argument.', HELP)
sys.exit(2) # command line usage error
if __name__ == '__main__':
main(sys.argv)

View File

@ -13,9 +13,9 @@
"""
# tag::ZIP_REPLACE[]
from typing import Iterable, Tuple
from collections.abc import Iterable
FromTo = Tuple[str, str] # <1>
FromTo = tuple[str, str] # <1>
def zip_replace(text: str, changes: Iterable[FromTo]) -> str: # <2>
for from_, to in changes:

View File

@ -1,39 +0,0 @@
"""
``zip_replace`` replaces multiple calls to ``str.replace``::
>>> changes = [
... ('(', ' ( '),
... (')', ' ) '),
... (' ', ' '),
... ]
>>> expr = '(+ 2 (* 3 7))'
>>> zip_replace(expr, changes)
' ( + 2 ( * 3 7 ) ) '
"""
from typing import Iterable, NamedTuple
class FromTo(NamedTuple):
from_: str
to: str
def zip_replace(text: str, changes: Iterable[FromTo], count: int = -1) -> str:
for from_, to in changes:
text = text.replace(from_, to, count)
return text
def demo() -> None:
import doctest
failed, count = doctest.testmod()
print(f'{count-failed} of {count} doctests OK')
l33t = [FromTo(*p) for p in 'a4 e3 i1 o0'.split()]
text = 'mad skilled noob powned leet'
print(zip_replace(text, l33t))
if __name__ == '__main__':
demo()

View File

@ -1,8 +0,0 @@
from array import array
from typing import MutableSequence
a = array('d')
reveal_type(a)
b: MutableSequence[float] = array('b')
reveal_type(b)

View File

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

View File

@ -0,0 +1,11 @@
from typing import TypeVar, TYPE_CHECKING
BT = TypeVar('BT', bound=float)
def triple2(a: BT) -> BT:
return a * 3
res2 = triple2(2)
if TYPE_CHECKING:
reveal_type(res2)

View File

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

View File

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

View File

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

View File

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

View File

@ -17,9 +17,9 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0) # <7>
>>> long_cart = [LineItem(str(item_code), 1, 1.0) # <7>
... for item_code in range(10)]
>>> Order(joe, long_order, LargeOrderPromo()) # <8>
>>> Order(joe, long_cart, LargeOrderPromo()) # <8>
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo())
<Order total: 42.00 due: 42.00>

View File

@ -1,20 +1,25 @@
def fidelity_promo(order):
from decimal import Decimal
from strategy import Order
def fidelity_promo(order: Order) -> Decimal: # <3>
"""5% discount for customers with 1000 or more fidelity points"""
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
if order.customer.fidelity >= 1000:
return order.total() * Decimal('0.05')
return Decimal(0)
def bulk_item_promo(order):
def bulk_item_promo(order: Order) -> Decimal:
"""10% discount for each LineItem with 20 or more units"""
discount = 0
discount = Decimal(0)
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * 0.1
discount += item.total() * Decimal('0.1')
return discount
def large_order_promo(order):
def large_order_promo(order: Order) -> Decimal:
"""7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * 0.07
return 0
return order.total() * Decimal('0.07')
return Decimal(0)

View File

@ -17,9 +17,9 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0) # <7>
>>> long_cart = [LineItem(str(item_code), 1, 1.0) # <7>
... for item_code in range(10)]
>>> Order(joe, long_order, LargeOrderPromo()) # <8>
>>> Order(joe, long_cart, LargeOrderPromo()) # <8>
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo())
<Order total: 42.00 due: 42.00>

View File

@ -1,13 +1,2 @@
mypy==0.770
mypy-extensions==0.4.3
typed-ast==1.4.1
typing-extensions==3.7.4.1
attrs==19.3.0
more-itertools==8.2.0
packaging==20.3
pluggy==0.13.1
py==1.10.0
pyparsing==2.4.6
pytest==5.4.1
six==1.14.0
wcwidth==0.1.9
mypy==0.910
pytest==6.2.4

View File

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

View File

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

View File

@ -3,29 +3,31 @@
# selecting best promotion from current module globals
"""
>>> from decimal import Decimal
>>> from strategy import Customer, LineItem, Order
>>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5),
... LineItem('apple', 10, 1.5),
... LineItem('watermelon', 5, 5.0)]
>>> cart = [LineItem('banana', 4, Decimal('.5')),
... LineItem('apple', 10, Decimal('1.5')),
... LineItem('watermelon', 5, Decimal(5))]
>>> Order(joe, cart, fidelity_promo)
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity_promo)
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5),
... LineItem('apple', 10, 1.5)]
>>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
... LineItem('apple', 10, Decimal('1.5'))]
>>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
>>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
... for item_code in range(10)]
>>> Order(joe, long_order, large_order_promo)
>>> Order(joe, long_cart, large_order_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo)
>>> Order(joe, long_cart, best_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50>
@ -35,78 +37,21 @@
# end::STRATEGY_BEST_TESTS[]
"""
from collections import namedtuple
Customer = namedtuple('Customer', 'name fidelity')
class LineItem:
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
class Order: # the Context
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion(self)
return self.total() - discount
def __repr__(self):
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
def fidelity_promo(order):
"""5% discount for customers with 1000 or more fidelity points"""
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
def bulk_item_promo(order):
"""10% discount for each LineItem with 20 or more units"""
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * 0.1
return discount
def large_order_promo(order):
"""7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * 0.07
return 0
# tag::STRATEGY_BEST2[]
from decimal import Decimal
from strategy import Order
from strategy import (
fidelity_promo, bulk_item_promo, large_order_promo # <1>
)
promos = [
globals()[name]
for name in globals() # <1>
if name.endswith('_promo') and name != 'best_promo' # <2>
] # <3>
promos = [promo for name, promo in globals().items() # <2>
if name.endswith('_promo') and # <3>
name != 'best_promo' # <4>
]
def best_promo(order):
"""Select best discount available
"""
return max(promo(order) for promo in promos) # <4>
def best_promo(order: Order) -> Decimal: # <5>
"""Compute the best discount available"""
return max(promo(order) for promo in promos)
# end::STRATEGY_BEST2[]

View File

@ -3,30 +3,32 @@
# selecting best promotion from imported module
"""
>>> from decimal import Decimal
>>> from strategy import Customer, LineItem, Order
>>> from promotions import *
>>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5),
... LineItem('apple', 10, 1.5),
... LineItem('watermelon', 5, 5.0)]
>>> cart = [LineItem('banana', 4, Decimal('.5')),
... LineItem('apple', 10, Decimal('1.5')),
... LineItem('watermelon', 5, Decimal(5))]
>>> Order(joe, cart, fidelity_promo)
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity_promo)
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5),
... LineItem('apple', 10, 1.5)]
>>> banana_cart = [LineItem('banana', 30, Decimal('.5')),
... LineItem('apple', 10, Decimal('1.5'))]
>>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
>>> long_cart = [LineItem(str(item_code), 1, Decimal(1))
... for item_code in range(10)]
>>> Order(joe, long_order, large_order_promo)
>>> Order(joe, long_cart, large_order_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo)
>>> Order(joe, long_cart, best_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50>
@ -36,55 +38,20 @@
# end::STRATEGY_BEST_TESTS[]
"""
from collections import namedtuple
import inspect
import promotions
Customer = namedtuple('Customer', 'name fidelity')
class LineItem:
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
class Order: # the Context
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion(self)
return self.total() - discount
def __repr__(self):
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
# tag::STRATEGY_BEST3[]
promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)]
from decimal import Decimal
import inspect
from strategy import Order
import promotions
def best_promo(order):
"""Select best discount available
"""
promos = [func for _, func in inspect.getmembers(promotions, inspect.isfunction)]
def best_promo(order: Order) -> Decimal:
"""Compute the best discount available"""
return max(promo(order) for promo in promos)
# end::STRATEGY_BEST3[]

View File

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

View File

@ -15,9 +15,9 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo(10))
<Order total: 30.00 due: 28.50>
>>> big_cart = [LineItem(str(item_code), 1, 1.0)
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, big_cart, LargeOrderPromo(7))
>>> Order(joe, long_cart, LargeOrderPromo(7))
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo(7))
<Order total: 42.00 due: 42.00>

View File

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

View File

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

View File

@ -17,9 +17,9 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo()) # <6>
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0) # <7>
>>> long_cart = [LineItem(str(item_code), 1, 1.0) # <7>
... for item_code in range(10)]
>>> Order(joe, long_order, LargeOrderPromo()) # <8>
>>> Order(joe, long_cart, LargeOrderPromo()) # <8>
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo())
<Order total: 42.00 due: 42.00>

View File

@ -17,9 +17,9 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo) # <3>
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, long_order, large_order_promo)
>>> Order(joe, long_cart, large_order_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>

View File

@ -16,16 +16,16 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, long_order, large_order_promo)
>>> Order(joe, long_cart, large_order_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo) # <1>
>>> Order(joe, long_cart, best_promo) # <1>
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo) # <2>
<Order total: 30.00 due: 28.50>

View File

@ -16,16 +16,16 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, long_order, large_order_promo)
>>> Order(joe, long_cart, large_order_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo)
>>> Order(joe, long_cart, best_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50>

View File

@ -17,16 +17,16 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, long_order, large_order_promo)
>>> Order(joe, long_cart, large_order_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo)
>>> Order(joe, long_cart, best_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50>

View File

@ -17,16 +17,16 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item)
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, long_order, large_order)
>>> Order(joe, long_cart, large_order)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order)
<Order total: 42.00 due: 42.00>
# tag::STRATEGY_BEST_TESTS[]
>>> Order(joe, long_order, best_promo)
>>> Order(joe, long_cart, best_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50>

View File

@ -15,9 +15,9 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo(10))
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, long_order, large_order_promo(7))
>>> Order(joe, long_cart, large_order_promo(7))
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo(7))
<Order total: 42.00 due: 42.00>

View File

@ -15,9 +15,9 @@
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo(10))
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
>>> long_cart = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, long_order, LargeOrderPromo(7))
>>> Order(joe, long_cart, LargeOrderPromo(7))
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo(7))
<Order total: 42.00 due: 42.00>

View File

@ -4,20 +4,25 @@ import resource
NUM_VECTORS = 10**7
module = None
if len(sys.argv) == 2:
module_name = sys.argv[1].replace('.py', '')
module = importlib.import_module(module_name)
else:
print(f'Usage: {sys.argv[0]} <vector-module-to-test>')
sys.exit(2) # command line usage error
fmt = 'Selected Vector2d type: {.__name__}.{.__name__}'
print(fmt.format(module, module.Vector2d))
if module is None:
print('Running test with built-in `complex`')
cls = complex
else:
fmt = 'Selected Vector2d type: {.__name__}.{.__name__}'
print(fmt.format(module, module.Vector2d))
cls = module.Vector2d
mem_init = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
print(f'Creating {NUM_VECTORS:,} Vector2d instances')
print(f'Creating {NUM_VECTORS:,} {cls.__qualname__!r} instances')
vectors = [module.Vector2d(3.0, 4.0) for i in range(NUM_VECTORS)]
vectors = [cls(3.0, 4.0) for i in range(NUM_VECTORS)]
mem_final = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
print(f'Initial RAM usage: {mem_init:14,}')

View File

@ -0,0 +1,53 @@
from vector2d_v3 import Vector2d
# tag::KEYWORD_PATTERNS[]
def keyword_pattern_demo(v: Vector2d) -> None:
match v:
case Vector2d(x=0, y=0):
print(f'{v!r} is null')
case Vector2d(x=0):
print(f'{v!r} is vertical')
case Vector2d(y=0):
print(f'{v!r} is horizontal')
case Vector2d(x=x, y=y) if x==y:
print(f'{v!r} is diagonal')
case _:
print(f'{v!r} is awesome')
# end::KEYWORD_PATTERNS[]
# tag::POSITIONAL_PATTERNS[]
def positional_pattern_demo(v: Vector2d) -> None:
match v:
case Vector2d(0, 0):
print(f'{v!r} is null')
case Vector2d(0):
print(f'{v!r} is vertical')
case Vector2d(_, 0):
print(f'{v!r} is horizontal')
case Vector2d(x, y) if x==y:
print(f'{v!r} is diagonal')
case _:
print(f'{v!r} is awesome')
# end::POSITIONAL_PATTERNS[]
def main():
vectors = (
Vector2d(1, 1),
Vector2d(0, 1),
Vector2d(1, 0),
Vector2d(1, 2),
Vector2d(0, 0),
)
print('KEYWORD PATTERNS:')
for vector in vectors:
keyword_pattern_demo(vector)
print('POSITIONAL PATTERNS:')
for vector in vectors:
positional_pattern_demo(vector)
if __name__ == '__main__':
main()

52
11-pythonic-obj/slots.rst Normal file
View File

@ -0,0 +1,52 @@
# tag::PIXEL[]
>>> class Pixel:
... __slots__ = ('x', 'y') # <1>
...
>>> p = Pixel() # <2>
>>> p.__dict__ # <3>
Traceback (most recent call last):
...
AttributeError: 'Pixel' object has no attribute '__dict__'
>>> p.x = 10 # <4>
>>> p.y = 20
>>> p.color = 'red' # <5>
Traceback (most recent call last):
...
AttributeError: 'Pixel' object has no attribute 'color'
# end::PIXEL[]
# tag::OPEN_PIXEL[]
>>> class OpenPixel(Pixel): # <1>
... pass
...
>>> op = OpenPixel()
>>> op.__dict__ # <2>
{}
>>> op.x = 8 # <3>
>>> op.__dict__ # <4>
{}
>>> op.x # <5>
8
>>> op.color = 'green' # <6>
>>> op.__dict__ # <7>
{'color': 'green'}
# end::OPEN_PIXEL[]
# tag::COLOR_PIXEL[]
>>> class ColorPixel(Pixel):
... __slots__ = ('color',) # <1>
>>> cp = ColorPixel()
>>> cp.__dict__ # <2>
Traceback (most recent call last):
...
AttributeError: 'ColorPixel' object has no attribute '__dict__'
>>> cp.x = 2
>>> cp.color = 'blue' # <3>
>>> cp.flavor = 'banana'
Traceback (most recent call last):
...
AttributeError: 'ColorPixel' object has no attribute 'flavor'
# end::COLOR_PIXEL[]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import collections
from collections import namedtuple, abc
Card = collections.namedtuple('Card', ['rank', 'suit'])
Card = namedtuple('Card', ['rank', 'suit'])
class FrenchDeck2(collections.MutableSequence):
class FrenchDeck2(abc.MutableSequence):
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()

View File

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

View File

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

View File

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

View File

@ -1,10 +1,39 @@
"""UpperDict uppercases all string keys.
"""
Short demos
===========
Test for initializer. `str` keys are uppercased::
``UpperDict`` behaves like a case-insensitive mapping`::
# tag::UPPERDICT_DEMO[]
>>> d = UpperDict([('a', 'letter A'), (2, 'digit two')])
>>> list(d.keys())
['A', 2]
>>> d['b'] = 'letter B'
>>> 'b' in d
True
>>> d['a'], d.get('B')
('letter A', 'letter B')
>>> list(d.keys())
['A', 2, 'B']
# end::UPPERDICT_DEMO[]
And ``UpperCounter`` is also case-insensitive::
# tag::UPPERCOUNTER_DEMO[]
>>> c = UpperCounter('BaNanA')
>>> c.most_common()
[('A', 3), ('N', 2), ('B', 1)]
# end::UPPERCOUNTER_DEMO[]
Detailed tests
==============
UpperDict uppercases all string keys.
>>> d = UpperDict([('a', 'letter A'), ('B', 'letter B'), (2, 'digit two')])
>>> list(d.keys())
['A', 'B', 2]
Tests for item retrieval using `d[key]` notation::
@ -82,14 +111,12 @@ Tests for count retrieval using `d[key]` notation::
# tag::UPPERCASE_MIXIN[]
import collections
def _upper(key): # <1>
try:
return key.upper()
except AttributeError:
return key
class UpperCaseMixin: # <2>
def __setitem__(self, key, item):
super().__setitem__(_upper(key), item)
@ -108,8 +135,6 @@ class UpperCaseMixin: # <2>
class UpperDict(UpperCaseMixin, collections.UserDict): # <1>
pass
class UpperCounter(UpperCaseMixin, collections.Counter): # <2>
"""Specialized 'Counter' that uppercases string keys""" # <3>
# end::UPPERDICT[]

View File

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

View File

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

View File

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

View File

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

View File

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