updade from Atlas repo
This commit is contained in:
@@ -17,7 +17,7 @@ d1 = dict(DIAL_CODES) # <1>
|
|||||||
print('d1:', d1.keys())
|
print('d1:', d1.keys())
|
||||||
d2 = dict(sorted(DIAL_CODES)) # <2>
|
d2 = dict(sorted(DIAL_CODES)) # <2>
|
||||||
print('d2:', d2.keys())
|
print('d2:', d2.keys())
|
||||||
d3 = dict(sorted(DIAL_CODES, key=lambda x:x[1])) # <3>
|
d3 = dict(sorted(DIAL_CODES, key=lambda x: x[1])) # <3>
|
||||||
print('d3:', d3.keys())
|
print('d3:', d3.keys())
|
||||||
assert d1 == d2 and d2 == d3 # <4>
|
assert d1 == d2 and d2 == d3 # <4>
|
||||||
# end::DIALCODES[]
|
# end::DIALCODES[]
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
# tag::INDEX[]
|
# tag::INDEX[]
|
||||||
"""Build an index mapping word -> list of occurrences"""
|
"""Build an index mapping word -> list of occurrences"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
WORD_RE = re.compile(r'\w+')
|
WORD_RE = re.compile(r'\w+')
|
||||||
|
|
||||||
@@ -15,11 +15,11 @@ with open(sys.argv[1], encoding='utf-8') as fp:
|
|||||||
for line_no, line in enumerate(fp, 1):
|
for line_no, line in enumerate(fp, 1):
|
||||||
for match in WORD_RE.finditer(line):
|
for match in WORD_RE.finditer(line):
|
||||||
word = match.group()
|
word = match.group()
|
||||||
column_no = match.start()+1
|
column_no = match.start() + 1
|
||||||
location = (line_no, column_no)
|
location = (line_no, column_no)
|
||||||
index.setdefault(word, []).append(location) # <1>
|
index.setdefault(word, []).append(location) # <1>
|
||||||
|
|
||||||
# print in alphabetical order
|
# display in alphabetical order
|
||||||
for word in sorted(index, key=str.upper):
|
for word in sorted(index, key=str.upper):
|
||||||
print(word, index[word])
|
print(word, index[word])
|
||||||
# end::INDEX[]
|
# end::INDEX[]
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
# tag::INDEX0[]
|
# tag::INDEX0[]
|
||||||
"""Build an index mapping word -> list of occurrences"""
|
"""Build an index mapping word -> list of occurrences"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
WORD_RE = re.compile(r'\w+')
|
WORD_RE = re.compile(r'\w+')
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ with open(sys.argv[1], encoding='utf-8') as fp:
|
|||||||
occurrences.append(location) # <2>
|
occurrences.append(location) # <2>
|
||||||
index[word] = occurrences # <3>
|
index[word] = occurrences # <3>
|
||||||
|
|
||||||
# print in alphabetical order
|
# display in alphabetical order
|
||||||
for word in sorted(index, key=str.upper): # <4>
|
for word in sorted(index, key=str.upper): # <4>
|
||||||
print(word, index[word])
|
print(word, index[word])
|
||||||
# end::INDEX0[]
|
# end::INDEX0[]
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
# tag::INDEX_DEFAULT[]
|
# tag::INDEX_DEFAULT[]
|
||||||
"""Build an index mapping word -> list of occurrences"""
|
"""Build an index mapping word -> list of occurrences"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
import collections
|
import collections
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
WORD_RE = re.compile(r'\w+')
|
WORD_RE = re.compile(r'\w+')
|
||||||
|
|
||||||
@@ -16,11 +16,11 @@ with open(sys.argv[1], encoding='utf-8') as fp:
|
|||||||
for line_no, line in enumerate(fp, 1):
|
for line_no, line in enumerate(fp, 1):
|
||||||
for match in WORD_RE.finditer(line):
|
for match in WORD_RE.finditer(line):
|
||||||
word = match.group()
|
word = match.group()
|
||||||
column_no = match.start()+1
|
column_no = match.start() + 1
|
||||||
location = (line_no, column_no)
|
location = (line_no, column_no)
|
||||||
index[word].append(location) # <2>
|
index[word].append(location) # <2>
|
||||||
|
|
||||||
# print in alphabetical order
|
# display in alphabetical order
|
||||||
for word in sorted(index, key=str.upper):
|
for word in sorted(index, key=str.upper):
|
||||||
print(word, index[word])
|
print(word, index[word])
|
||||||
# end::INDEX_DEFAULT[]
|
# end::INDEX_DEFAULT[]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
import collections
|
import collections
|
||||||
from unicodedata import name, category
|
from unicodedata import category
|
||||||
|
|
||||||
|
|
||||||
def category_stats():
|
def category_stats():
|
||||||
@@ -19,7 +19,7 @@ def category_scan(desired):
|
|||||||
for code in range(sys.maxunicode + 1):
|
for code in range(sys.maxunicode + 1):
|
||||||
char = chr(code)
|
char = chr(code)
|
||||||
if category(char) == desired:
|
if category(char) == desired:
|
||||||
yield char
|
yield char
|
||||||
|
|
||||||
|
|
||||||
def main(args):
|
def main(args):
|
||||||
@@ -30,7 +30,7 @@ def main(args):
|
|||||||
count += 1
|
count += 1
|
||||||
if count > 200:
|
if count > 200:
|
||||||
break
|
break
|
||||||
print()
|
print()
|
||||||
print(count, 'characters shown')
|
print(count, 'characters shown')
|
||||||
else:
|
else:
|
||||||
counts, firsts = category_stats()
|
counts, firsts = category_stats()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import sys, locale
|
import locale
|
||||||
|
import sys
|
||||||
|
|
||||||
expressions = """
|
expressions = """
|
||||||
locale.getpreferredencoding()
|
locale.getpreferredencoding()
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ zwg_sample = """
|
|||||||
1F469 200D 2764 FE0F 200D 1F48B 200D 1F469 |kiss: woman, woman |E2.0
|
1F469 200D 2764 FE0F 200D 1F48B 200D 1F469 |kiss: woman, woman |E2.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
markers = {'\u200D': 'ZWG', # ZERO WIDTH JOINER
|
markers = {'\u200D': 'ZWG', # ZERO WIDTH JOINER
|
||||||
'\uFE0F': 'V16', # VARIATION SELECTOR-16
|
'\uFE0F': 'V16', # VARIATION SELECTOR-16
|
||||||
}
|
}
|
||||||
|
|
||||||
for line in zwg_sample.strip().split('\n'):
|
for line in zwg_sample.strip().split('\n'):
|
||||||
code, descr, version = (s.strip() for s in line.split('|'))
|
code, descr, version = (s.strip() for s in line.split('|'))
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Coordinate:
|
class Coordinate:
|
||||||
|
|
||||||
lat: float
|
lat: float
|
||||||
long: float
|
long: float
|
||||||
|
|
||||||
@@ -21,4 +20,4 @@ class Coordinate:
|
|||||||
ns = 'N' if self.lat >= 0 else 'S'
|
ns = 'N' if self.lat >= 0 else 'S'
|
||||||
we = 'E' if self.long >= 0 else 'W'
|
we = 'E' if self.long >= 0 else 'W'
|
||||||
return f'{abs(self.lat):.1f}°{ns}, {abs(self.long):.1f}°{we}'
|
return f'{abs(self.lat):.1f}°{ns}, {abs(self.long):.1f}°{we}'
|
||||||
# end::COORDINATE[]
|
# end::COORDINATE[]
|
||||||
|
|||||||
@@ -76,6 +76,5 @@ def demo():
|
|||||||
print(n, name2hex(n, o))
|
print(n, name2hex(n, o))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
demo()
|
demo()
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ def columnize(sequence: Sequence[T], num_columns: int = 0) -> List[Tuple[T, ...]
|
|||||||
return [tuple(sequence[i::num_rows]) for i in range(num_rows)]
|
return [tuple(sequence[i::num_rows]) for i in range(num_rows)]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def demo() -> None:
|
def demo() -> None:
|
||||||
nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India'
|
nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India'
|
||||||
' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo'
|
' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo'
|
||||||
@@ -22,7 +21,6 @@ def demo() -> None:
|
|||||||
print(f'{word:15}', end='')
|
print(f'{word:15}', end='')
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
print()
|
print()
|
||||||
for length in range(2, 21, 6):
|
for length in range(2, 21, 6):
|
||||||
values = list(range(1, length + 1))
|
values = list(range(1, length + 1))
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ def columnize(sequence: Sequence[str], num_columns: int) -> List[Row]:
|
|||||||
return [tuple(sequence[i::num_rows]) for i in range(num_rows)]
|
return [tuple(sequence[i::num_rows]) for i in range(num_rows)]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def demo() -> None:
|
def demo() -> None:
|
||||||
nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India'
|
nato = ('Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India'
|
||||||
' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo'
|
' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import List, Callable, TypeVar
|
from typing import List, Callable
|
||||||
|
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
|
|
||||||
|
|||||||
@@ -53,4 +53,4 @@ def test_double_nparray() -> None:
|
|||||||
def test_double_none() -> None:
|
def test_double_none() -> None:
|
||||||
given = None
|
given = None
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
result = double(given)
|
double(given)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ def mode(data: Iterable[T]) -> T:
|
|||||||
|
|
||||||
|
|
||||||
def demo() -> None:
|
def demo() -> None:
|
||||||
from typing import List, Set, TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
pop: list[set] = [set(), set()]
|
pop: list[set] = [set(), set()]
|
||||||
m = mode(pop)
|
m = mode(pop)
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -21,5 +21,6 @@ def demo() -> None:
|
|||||||
print(pop)
|
print(pop)
|
||||||
print(repr(m), type(m))
|
print(repr(m), type(m))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
demo()
|
demo()
|
||||||
|
|||||||
@@ -3,12 +3,11 @@
|
|||||||
"""passdrill: typing drills for practicing passphrases
|
"""passdrill: typing drills for practicing passphrases
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
from base64 import b64encode, b64decode
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
from hashlib import scrypt
|
from hashlib import scrypt
|
||||||
from base64 import b64encode, b64decode
|
|
||||||
|
|
||||||
from typing import Sequence, Tuple
|
from typing import Sequence, Tuple
|
||||||
|
|
||||||
HASH_FILENAME = 'passdrill.hash'
|
HASH_FILENAME = 'passdrill.hash'
|
||||||
@@ -20,7 +19,7 @@ def prompt() -> str:
|
|||||||
confirmed = ''
|
confirmed = ''
|
||||||
while confirmed != 'y':
|
while confirmed != 'y':
|
||||||
passphrase = input('Type passphrase to hash (it will be echoed): ')
|
passphrase = input('Type passphrase to hash (it will be echoed): ')
|
||||||
if passphrase == '' or passphrase == 'q':
|
if passphrase in ('', 'q'):
|
||||||
print('ERROR: the passphrase cannot be empty or "q".')
|
print('ERROR: the passphrase cannot be empty or "q".')
|
||||||
continue
|
continue
|
||||||
print(f'Passphrase to be hashed -> {passphrase}')
|
print(f'Passphrase to be hashed -> {passphrase}')
|
||||||
@@ -45,7 +44,7 @@ def save_hash() -> None:
|
|||||||
salted_hash = build_hash(prompt())
|
salted_hash = build_hash(prompt())
|
||||||
with open(HASH_FILENAME, 'wb') as fp:
|
with open(HASH_FILENAME, 'wb') as fp:
|
||||||
fp.write(salted_hash)
|
fp.write(salted_hash)
|
||||||
print(f'Passphrase hash saved to', HASH_FILENAME)
|
print(f'Passphrase hash saved to {HASH_FILENAME}')
|
||||||
|
|
||||||
|
|
||||||
def load_hash() -> Tuple[bytes, bytes]:
|
def load_hash() -> Tuple[bytes, bytes]:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class FromTo(NamedTuple):
|
|||||||
to: str
|
to: str
|
||||||
|
|
||||||
|
|
||||||
def zip_replace(text: str, changes: Iterable[FromTo], count:int = -1) -> str:
|
def zip_replace(text: str, changes: Iterable[FromTo], count: int = -1) -> str:
|
||||||
for from_, to in changes:
|
for from_, to in changes:
|
||||||
text = text.replace(from_, to, count)
|
text = text.replace(from_, to, count)
|
||||||
return text
|
return text
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
values_map = [
|
values_map = [
|
||||||
(1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1),
|
(1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1),
|
||||||
( 'M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV','I')
|
( 'M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I')
|
||||||
]
|
]
|
||||||
|
|
||||||
def to_roman(arabic: int) -> str:
|
def to_roman(arabic: int) -> str:
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
# tag::BOOKDICT[]
|
|
||||||
from typing import TypedDict, List
|
|
||||||
import json
|
|
||||||
|
|
||||||
class BookDict(TypedDict):
|
|
||||||
isbn: str
|
|
||||||
title: str
|
|
||||||
authors: List[str]
|
|
||||||
pagecount: int
|
|
||||||
# end::BOOKDICT[]
|
|
||||||
|
|
||||||
# tag::TOXML[]
|
|
||||||
AUTHOR_EL = '<AUTHOR>{}</AUTHOR>'
|
|
||||||
|
|
||||||
def to_xml(book: BookDict) -> str: # <1>
|
|
||||||
elements: List[str] = [] # <2>
|
|
||||||
for key, value in book.items():
|
|
||||||
if isinstance(value, list): # <3>
|
|
||||||
elements.extend(
|
|
||||||
AUTHOR_EL.format(n) for n in value) # <4>
|
|
||||||
else:
|
|
||||||
tag = key.upper()
|
|
||||||
elements.append(f'<{tag}>{value}</{tag}>')
|
|
||||||
xml = '\n\t'.join(elements)
|
|
||||||
return f'<BOOK>\n\t{xml}\n</BOOK>'
|
|
||||||
# end::TOXML[]
|
|
||||||
|
|
||||||
# tag::FROMJSON[]
|
|
||||||
def from_json(data: str) -> BookDict:
|
|
||||||
whatever: BookDict = json.loads(data) # <1>
|
|
||||||
return whatever # <2>
|
|
||||||
# end::FROMJSON[]
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# tag::BOOKDICT[]
|
|
||||||
from typing import TypedDict, List
|
|
||||||
import json
|
|
||||||
|
|
||||||
class BookDict(TypedDict):
|
|
||||||
isbn: str
|
|
||||||
title: str
|
|
||||||
authors: List[str]
|
|
||||||
pagecount: int
|
|
||||||
# end::BOOKDICT[]
|
|
||||||
|
|
||||||
# tag::TOXML[]
|
|
||||||
AUTHOR_EL = '<AUTHOR>{}</AUTHOR>'
|
|
||||||
|
|
||||||
def to_xml(book: BookDict) -> str: # <1>
|
|
||||||
elements: List[str] = [] # <2>
|
|
||||||
for key, value in book.items():
|
|
||||||
if isinstance(value, list): # <3>
|
|
||||||
elements.extend(AUTHOR_EL.format(n)
|
|
||||||
for n in value)
|
|
||||||
else:
|
|
||||||
tag = key.upper()
|
|
||||||
elements.append(f'<{tag}>{value}</{tag}>')
|
|
||||||
xml = '\n\t'.join(elements)
|
|
||||||
return f'<BOOK>\n\t{xml}\n</BOOK>'
|
|
||||||
# end::TOXML[]
|
|
||||||
|
|
||||||
# tag::FROMJSON[]
|
|
||||||
def from_json(data: str) -> BookDict:
|
|
||||||
whatever = json.loads(data) # <1>
|
|
||||||
return whatever # <2>
|
|
||||||
# end::FROMJSON[]
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
from books import BookDict
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
def demo() -> None: # <1>
|
|
||||||
book = BookDict( # <2>
|
|
||||||
isbn='0134757599',
|
|
||||||
title='Refactoring, 2e',
|
|
||||||
authors=['Martin Fowler', 'Kent Beck'],
|
|
||||||
pagecount=478
|
|
||||||
)
|
|
||||||
authors = book['authors'] # <3>
|
|
||||||
if TYPE_CHECKING: # <4>
|
|
||||||
reveal_type(authors) # <5>
|
|
||||||
authors = 'Bob' # <6>
|
|
||||||
book['weight'] = 4.2
|
|
||||||
del book['title']
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
demo()
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
from books import to_xml, from_json
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
def demo() -> None:
|
|
||||||
NOT_BOOK_JSON = """
|
|
||||||
{"title": "Andromeda Strain",
|
|
||||||
"flavor": "pistachio",
|
|
||||||
"authors": true}
|
|
||||||
"""
|
|
||||||
not_book = from_json(NOT_BOOK_JSON) # <1>
|
|
||||||
if TYPE_CHECKING: # <2>
|
|
||||||
reveal_type(not_book)
|
|
||||||
reveal_type(not_book['authors'])
|
|
||||||
|
|
||||||
print(not_book) # <3>
|
|
||||||
print(not_book['flavor']) # <4>
|
|
||||||
|
|
||||||
xml = to_xml(not_book) # <5>
|
|
||||||
print(xml) # <6>
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
demo()
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import json
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from books import BookDict, to_xml, from_json
|
|
||||||
|
|
||||||
XML_SAMPLE = """
|
|
||||||
<BOOK>
|
|
||||||
\t<ISBN>0134757599</ISBN>
|
|
||||||
\t<TITLE>Refactoring, 2e</TITLE>
|
|
||||||
\t<AUTHOR>Martin Fowler</AUTHOR>
|
|
||||||
\t<AUTHOR>Kent Beck</AUTHOR>
|
|
||||||
\t<PAGECOUNT>478</PAGECOUNT>
|
|
||||||
</BOOK>
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
|
|
||||||
# using plain dicts
|
|
||||||
|
|
||||||
def test_1() -> None:
|
|
||||||
xml = to_xml({
|
|
||||||
'isbn': '0134757599',
|
|
||||||
'title': 'Refactoring, 2e',
|
|
||||||
'authors': ['Martin Fowler', 'Kent Beck'],
|
|
||||||
'pagecount': 478,
|
|
||||||
})
|
|
||||||
assert xml == XML_SAMPLE
|
|
||||||
|
|
||||||
def test_2() -> None:
|
|
||||||
xml = to_xml(dict(
|
|
||||||
isbn='0134757599',
|
|
||||||
title='Refactoring, 2e',
|
|
||||||
authors=['Martin Fowler', 'Kent Beck'],
|
|
||||||
pagecount=478))
|
|
||||||
assert xml == XML_SAMPLE
|
|
||||||
|
|
||||||
def test_5() -> None:
|
|
||||||
book_data: BookDict = dict(
|
|
||||||
isbn='0134757599',
|
|
||||||
title='Refactoring, 2e',
|
|
||||||
authors=['Martin Fowler', 'Kent Beck'],
|
|
||||||
pagecount=478
|
|
||||||
)
|
|
||||||
xml = to_xml(book_data)
|
|
||||||
assert xml == XML_SAMPLE
|
|
||||||
|
|
||||||
def test_6() -> None:
|
|
||||||
book_data = dict(
|
|
||||||
isbn='0134757599',
|
|
||||||
title='Refactoring, 2e',
|
|
||||||
authors=['Martin Fowler', 'Kent Beck'],
|
|
||||||
pagecount=478
|
|
||||||
)
|
|
||||||
xml = to_xml(cast(BookDict, book_data)) # cast needed
|
|
||||||
assert xml == XML_SAMPLE
|
|
||||||
|
|
||||||
def test_4() -> None:
|
|
||||||
xml = to_xml(BookDict(
|
|
||||||
isbn='0134757599',
|
|
||||||
title='Refactoring, 2e',
|
|
||||||
authors=['Martin Fowler', 'Kent Beck'],
|
|
||||||
pagecount=478))
|
|
||||||
assert xml == XML_SAMPLE
|
|
||||||
|
|
||||||
def test_7() -> None:
|
|
||||||
book_data = BookDict(
|
|
||||||
isbn='0134757599',
|
|
||||||
title='Refactoring, 2e',
|
|
||||||
authors=['Martin Fowler', 'Kent Beck'],
|
|
||||||
pagecount=478
|
|
||||||
)
|
|
||||||
xml = to_xml(book_data)
|
|
||||||
assert xml == XML_SAMPLE
|
|
||||||
|
|
||||||
def test_8() -> None:
|
|
||||||
book_data: BookDict = {
|
|
||||||
'isbn': '0134757599',
|
|
||||||
'title': 'Refactoring, 2e',
|
|
||||||
'authors': ['Martin Fowler', 'Kent Beck'],
|
|
||||||
'pagecount': 478,
|
|
||||||
}
|
|
||||||
xml = to_xml(book_data)
|
|
||||||
assert xml == XML_SAMPLE
|
|
||||||
|
|
||||||
BOOK_JSON = """
|
|
||||||
{"isbn": "0134757599",
|
|
||||||
"title": "Refactoring, 2e",
|
|
||||||
"authors": ["Martin Fowler", "Kent Beck"],
|
|
||||||
"pagecount": 478}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_load_book_0() -> None:
|
|
||||||
book_data: BookDict = json.loads(BOOK_JSON) # typed var
|
|
||||||
xml = to_xml(book_data)
|
|
||||||
assert xml == XML_SAMPLE
|
|
||||||
|
|
||||||
def test_load_book() -> None:
|
|
||||||
book_data = from_json(BOOK_JSON)
|
|
||||||
xml = to_xml(book_data)
|
|
||||||
assert xml == XML_SAMPLE
|
|
||||||
|
|
||||||
|
|
||||||
NOT_BOOK_JSON = """
|
|
||||||
{"isbn": 3.141592653589793
|
|
||||||
"title": [1, 2, 3],
|
|
||||||
"authors": ["Martin Fowler", "Kent Beck"],
|
|
||||||
"flavor": "strawberry"}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_load_not_book() -> None:
|
|
||||||
book_data: BookDict = json.loads(BOOK_JSON) # typed var
|
|
||||||
xml = to_xml(book_data)
|
|
||||||
assert xml == XML_SAMPLE
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
from books import BookDict, to_xml
|
|
||||||
|
|
||||||
XML_SAMPLE = """
|
|
||||||
<BOOK>
|
|
||||||
\t<ISBN>0134757599</ISBN>
|
|
||||||
\t<TITLE>Refactoring, 2e</TITLE>
|
|
||||||
\t<AUTHOR>Martin Fowler</AUTHOR>
|
|
||||||
\t<AUTHOR>Kent Beck</AUTHOR>
|
|
||||||
\t<PAGECOUNT>478</PAGECOUNT>
|
|
||||||
</BOOK>
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
def test_3() -> None:
|
|
||||||
xml = to_xml(BookDict(dict([ # Expected keyword arguments, {...}, or dict(...) in TypedDict constructor
|
|
||||||
('isbn', '0134757599'),
|
|
||||||
('title', 'Refactoring, 2e'),
|
|
||||||
('authors', ['Martin Fowler', 'Kent Beck']),
|
|
||||||
('pagecount', 478),
|
|
||||||
])))
|
|
||||||
assert xml == XML_SAMPLE
|
|
||||||
@@ -41,4 +41,3 @@ if __name__ == '__main__':
|
|||||||
for i in range(3):
|
for i in range(3):
|
||||||
snooze(.123)
|
snooze(.123)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def main(): # <8>
|
|||||||
f2()
|
f2()
|
||||||
f3()
|
f3()
|
||||||
|
|
||||||
if __name__=='__main__':
|
if __name__ == '__main__':
|
||||||
main() # <9>
|
main() # <9>
|
||||||
|
|
||||||
# end::REGISTRATION[]
|
# end::REGISTRATION[]
|
||||||
|
|||||||
@@ -56,8 +56,7 @@ def test_large_order_promo_no_discount(customer_fidelity_0, cart_plain) -> None:
|
|||||||
|
|
||||||
|
|
||||||
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)
|
cart = [LineItem(str(item_code), 1, 1.0) for item_code in range(10)]
|
||||||
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.0
|
||||||
assert order.due() == 9.3
|
assert order.due() == 9.3
|
||||||
|
|||||||
@@ -89,4 +89,3 @@ def best_promo(order):
|
|||||||
|
|
||||||
# end::STRATEGY_BEST3[]
|
# end::STRATEGY_BEST3[]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class Vector:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
components = reprlib.repr(self._components) # <3>
|
components = reprlib.repr(self._components) # <3>
|
||||||
components = components[components.find('['):-1] # <4>
|
components = components[components.find('['):-1] # <4>
|
||||||
return 'Vector({})'.format(components)
|
return f'Vector({components})'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(tuple(self))
|
return str(tuple(self))
|
||||||
@@ -113,7 +113,7 @@ class Vector:
|
|||||||
return tuple(self) == tuple(other)
|
return tuple(self) == tuple(other)
|
||||||
|
|
||||||
def __abs__(self):
|
def __abs__(self):
|
||||||
return math.sqrt(sum(x * x for x in self)) # <6>
|
return math.hypot(*self) # <6>
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return bool(abs(self))
|
return bool(abs(self))
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class Vector:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
components = reprlib.repr(self._components)
|
components = reprlib.repr(self._components)
|
||||||
components = components[components.find('['):-1]
|
components = components[components.find('['):-1]
|
||||||
return 'Vector({})'.format(components)
|
return f'Vector({components})'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(tuple(self))
|
return str(tuple(self))
|
||||||
@@ -140,7 +140,7 @@ class Vector:
|
|||||||
return tuple(self) == tuple(other)
|
return tuple(self) == tuple(other)
|
||||||
|
|
||||||
def __abs__(self):
|
def __abs__(self):
|
||||||
return math.sqrt(sum(x * x for x in self))
|
return math.hypot(*self)
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return bool(abs(self))
|
return bool(abs(self))
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ class Vector:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
components = reprlib.repr(self._components)
|
components = reprlib.repr(self._components)
|
||||||
components = components[components.find('['):-1]
|
components = components[components.find('['):-1]
|
||||||
return 'Vector({})'.format(components)
|
return f'Vector({components})'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(tuple(self))
|
return str(tuple(self))
|
||||||
@@ -183,7 +183,7 @@ class Vector:
|
|||||||
return tuple(self) == tuple(other)
|
return tuple(self) == tuple(other)
|
||||||
|
|
||||||
def __abs__(self):
|
def __abs__(self):
|
||||||
return math.sqrt(sum(x * x for x in self))
|
return math.hypot(*self)
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return bool(abs(self))
|
return bool(abs(self))
|
||||||
@@ -207,8 +207,8 @@ class Vector:
|
|||||||
pos = cls.shortcut_names.find(name) # <3>
|
pos = cls.shortcut_names.find(name) # <3>
|
||||||
if 0 <= pos < len(self._components): # <4>
|
if 0 <= pos < len(self._components): # <4>
|
||||||
return self._components[pos]
|
return self._components[pos]
|
||||||
msg = '{.__name__!r} object has no attribute {!r}' # <5>
|
msg = f'{cls.__name__!r} object has no attribute {name!r}' # <5>
|
||||||
raise AttributeError(msg.format(cls, name))
|
raise AttributeError(msg)
|
||||||
# end::VECTOR_V3_GETATTR[]
|
# end::VECTOR_V3_GETATTR[]
|
||||||
|
|
||||||
# tag::VECTOR_V3_SETATTR[]
|
# tag::VECTOR_V3_SETATTR[]
|
||||||
@@ -216,7 +216,7 @@ class Vector:
|
|||||||
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.shortcut_names: # <2>
|
||||||
error = 'readonly attribute {attr_name!r}'
|
error = 'read-only 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:
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class Vector:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
components = reprlib.repr(self._components)
|
components = reprlib.repr(self._components)
|
||||||
components = components[components.find('['):-1]
|
components = components[components.find('['):-1]
|
||||||
return 'Vector({})'.format(components)
|
return f'Vector({components})'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(tuple(self))
|
return str(tuple(self))
|
||||||
@@ -184,7 +184,7 @@ class Vector:
|
|||||||
return functools.reduce(operator.xor, hashes, 0)
|
return functools.reduce(operator.xor, hashes, 0)
|
||||||
|
|
||||||
def __abs__(self):
|
def __abs__(self):
|
||||||
return math.sqrt(sum(x * x for x in self))
|
return math.hypot(*self)
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return bool(abs(self))
|
return bool(abs(self))
|
||||||
@@ -207,8 +207,8 @@ class Vector:
|
|||||||
pos = cls.shortcut_names.find(name)
|
pos = cls.shortcut_names.find(name)
|
||||||
if 0 <= pos < len(self._components):
|
if 0 <= pos < len(self._components):
|
||||||
return self._components[pos]
|
return self._components[pos]
|
||||||
msg = '{.__name__!r} object has no attribute {!r}'
|
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||||
raise AttributeError(msg.format(cls, name))
|
raise AttributeError(msg)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def frombytes(cls, octets):
|
def frombytes(cls, octets):
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ class Vector:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
components = reprlib.repr(self._components)
|
components = reprlib.repr(self._components)
|
||||||
components = components[components.find('['):-1]
|
components = components[components.find('['):-1]
|
||||||
return 'Vector({})'.format(components)
|
return f'Vector({components})'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(tuple(self))
|
return str(tuple(self))
|
||||||
@@ -227,7 +227,7 @@ class Vector:
|
|||||||
return functools.reduce(operator.xor, hashes, 0)
|
return functools.reduce(operator.xor, hashes, 0)
|
||||||
|
|
||||||
def __abs__(self):
|
def __abs__(self):
|
||||||
return math.sqrt(sum(x * x for x in self))
|
return math.hypot(*self)
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return bool(abs(self))
|
return bool(abs(self))
|
||||||
@@ -250,11 +250,11 @@ class Vector:
|
|||||||
pos = cls.shortcut_names.find(name)
|
pos = cls.shortcut_names.find(name)
|
||||||
if 0 <= pos < len(self._components):
|
if 0 <= pos < len(self._components):
|
||||||
return self._components[pos]
|
return self._components[pos]
|
||||||
msg = '{.__name__!r} object has no attribute {!r}'
|
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||||
raise AttributeError(msg.format(cls, name))
|
raise AttributeError(msg)
|
||||||
|
|
||||||
def angle(self, n): # <2>
|
def angle(self, n): # <2>
|
||||||
r = math.sqrt(sum(x * x for x in self[n:]))
|
r = math.hypot(*self[n:])
|
||||||
a = math.atan2(r, self[n-1])
|
a = math.atan2(r, self[n-1])
|
||||||
if (n == len(self) - 1) and (self[-1] < 0):
|
if (n == len(self) - 1) and (self[-1] < 0):
|
||||||
return math.pi * 2 - a
|
return math.pi * 2 - a
|
||||||
|
|||||||
@@ -53,4 +53,4 @@ def test_double_nparray() -> None:
|
|||||||
def test_double_none() -> None:
|
def test_double_none() -> None:
|
||||||
given = None
|
given = None
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
result = double(given)
|
double(given)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ class Tombola(abc.ABC): # <1>
|
|||||||
"""Return `True` if there's at least 1 item, `False` otherwise."""
|
"""Return `True` if there's at least 1 item, `False` otherwise."""
|
||||||
return bool(self.inspect()) # <5>
|
return bool(self.inspect()) # <5>
|
||||||
|
|
||||||
|
|
||||||
def inspect(self):
|
def inspect(self):
|
||||||
"""Return a sorted tuple with the items currently inside."""
|
"""Return a sorted tuple with the items currently inside."""
|
||||||
items = []
|
items = []
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from typing import Any, Iterable, TYPE_CHECKING
|
|||||||
|
|
||||||
from randompick import RandomPicker # <1>
|
from randompick import RandomPicker # <1>
|
||||||
|
|
||||||
class SimplePicker(): # <2>
|
class SimplePicker: # <2>
|
||||||
def __init__(self, items: Iterable) -> None:
|
def __init__(self, items: Iterable) -> None:
|
||||||
self._items = list(items)
|
self._items = list(items)
|
||||||
random.shuffle(self._items)
|
random.shuffle(self._items)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Protocol, runtime_checkable, Any, Iterable
|
from typing import Protocol, runtime_checkable
|
||||||
from randompick import RandomPicker
|
from randompick import RandomPicker
|
||||||
|
|
||||||
@runtime_checkable # <1>
|
@runtime_checkable # <1>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import random
|
import random
|
||||||
from typing import Any, Iterable, TYPE_CHECKING
|
from typing import Any, Iterable
|
||||||
|
|
||||||
from randompickload import LoadableRandomPicker
|
from randompickload import LoadableRandomPicker
|
||||||
|
|
||||||
class SimplePicker():
|
class SimplePicker:
|
||||||
def __init__(self, items: Iterable) -> None:
|
def __init__(self, items: Iterable) -> None:
|
||||||
self._items = list(items)
|
self._items = list(items)
|
||||||
random.shuffle(self._items)
|
random.shuffle(self._items)
|
||||||
@@ -11,7 +11,7 @@ class SimplePicker():
|
|||||||
def pick(self) -> Any:
|
def pick(self) -> Any:
|
||||||
return self._items.pop()
|
return self._items.pop()
|
||||||
|
|
||||||
class LoadablePicker(): # <1>
|
class LoadablePicker: # <1>
|
||||||
def __init__(self, items: Iterable) -> None:
|
def __init__(self, items: Iterable) -> None:
|
||||||
self.load(items)
|
self.load(items)
|
||||||
|
|
||||||
|
|||||||
@@ -2,25 +2,25 @@ from typing import TypeVar, Generic
|
|||||||
|
|
||||||
|
|
||||||
class Beverage:
|
class Beverage:
|
||||||
"""Any beverage"""
|
"""Any beverage."""
|
||||||
|
|
||||||
|
|
||||||
class Juice(Beverage):
|
class Juice(Beverage):
|
||||||
"""Any fruit juice"""
|
"""Any fruit juice."""
|
||||||
|
|
||||||
|
|
||||||
class OrangeJuice(Juice):
|
class OrangeJuice(Juice):
|
||||||
"""Delicious juice from Brazilian oranges"""
|
"""Delicious juice from Brazilian oranges."""
|
||||||
|
|
||||||
|
|
||||||
BeverageT = TypeVar('BeverageT', covariant=True)
|
T_co = TypeVar('T_co', covariant=True)
|
||||||
|
|
||||||
|
|
||||||
class BeverageDispenser(Generic[BeverageT]):
|
class BeverageDispenser(Generic[T_co]):
|
||||||
def __init__(self, beverage: BeverageT) -> None:
|
def __init__(self, beverage: T_co) -> None:
|
||||||
self.beverage = beverage
|
self.beverage = beverage
|
||||||
|
|
||||||
def dispense(self) -> BeverageT:
|
def dispense(self) -> T_co:
|
||||||
return self.beverage
|
return self.beverage
|
||||||
|
|
||||||
|
|
||||||
@@ -36,11 +36,11 @@ class Compostable(Biodegradable):
|
|||||||
"""Compostable garbage."""
|
"""Compostable garbage."""
|
||||||
|
|
||||||
|
|
||||||
GarbageT = TypeVar('GarbageT', contravariant=True)
|
T_contra = TypeVar('T_contra', contravariant=True)
|
||||||
|
|
||||||
|
|
||||||
class TrashCan(Generic[GarbageT]):
|
class TrashCan(Generic[T_contra]):
|
||||||
def put(self, trash) -> None:
|
def put(self, trash: T_contra) -> None:
|
||||||
"""Store trash until dumped..."""
|
"""Store trash until dumped..."""
|
||||||
|
|
||||||
|
|
||||||
@@ -48,35 +48,48 @@ class Cafeteria:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
dispenser: BeverageDispenser[Juice],
|
dispenser: BeverageDispenser[Juice],
|
||||||
trash_can: TrashCan[Biodegradable]
|
trash_can: TrashCan[Biodegradable],
|
||||||
):
|
):
|
||||||
"""Initialize..."""
|
"""Initialize..."""
|
||||||
|
|
||||||
|
|
||||||
beverage_dispenser = BeverageDispenser(Beverage())
|
################################################ exact types
|
||||||
juice_dispenser = BeverageDispenser(Juice())
|
|
||||||
orange_juice_dispenser = BeverageDispenser(OrangeJuice())
|
|
||||||
|
|
||||||
trash_can: TrashCan[Garbage] = TrashCan()
|
juice_dispenser = BeverageDispenser(Juice())
|
||||||
bio_can: TrashCan[Biodegradable] = TrashCan()
|
bio_can: TrashCan[Biodegradable] = TrashCan()
|
||||||
compost_can: TrashCan[Compostable] = TrashCan()
|
|
||||||
|
|
||||||
arnold_hall = Cafeteria(juice_dispenser, bio_can)
|
arnold_hall = Cafeteria(juice_dispenser, bio_can)
|
||||||
|
|
||||||
######################## covariance on 1st argument
|
|
||||||
arnold_hall = Cafeteria(orange_juice_dispenser, trash_can)
|
################################################ covariant dispenser
|
||||||
|
|
||||||
|
orange_juice_dispenser = BeverageDispenser(OrangeJuice())
|
||||||
|
|
||||||
|
arnold_hall = Cafeteria(orange_juice_dispenser, bio_can)
|
||||||
|
|
||||||
|
|
||||||
|
################################################ non-covariant dispenser
|
||||||
|
|
||||||
|
beverage_dispenser = BeverageDispenser(Beverage())
|
||||||
|
|
||||||
## Argument 1 to "Cafeteria" has
|
## Argument 1 to "Cafeteria" has
|
||||||
## incompatible type "BeverageDispenser[Beverage]"
|
## incompatible type "BeverageDispenser[Beverage]"
|
||||||
## expected "BeverageDispenser[Juice]"
|
## expected "BeverageDispenser[Juice]"
|
||||||
# arnold_hall = Cafeteria(beverage_dispenser, trash_can)
|
# arnold_hall = Cafeteria(beverage_dispenser, bio_can)
|
||||||
|
|
||||||
|
|
||||||
######################## contravariance on 2nd argument
|
################################################ contravariant trash
|
||||||
|
|
||||||
|
trash_can: TrashCan[Garbage] = TrashCan()
|
||||||
|
|
||||||
|
arnold_hall = Cafeteria(juice_dispenser, trash_can)
|
||||||
|
|
||||||
|
|
||||||
|
################################################ non-contravariant trash
|
||||||
|
|
||||||
|
compost_can: TrashCan[Compostable] = TrashCan()
|
||||||
|
|
||||||
## Argument 2 to "Cafeteria" has
|
## Argument 2 to "Cafeteria" has
|
||||||
## incompatible type "TrashCan[Compostable]"
|
## incompatible type "TrashCan[Compostable]"
|
||||||
## expected "TrashCan[Biodegradable]"
|
## expected "TrashCan[Biodegradable]"
|
||||||
# arnold_hall = Cafeteria(juice_dispenser, compost_can)
|
# arnold_hall = Cafeteria(juice_dispenser, compost_can)
|
||||||
|
|
||||||
arnold_hall = Cafeteria(juice_dispenser, trash_can)
|
|
||||||
|
|||||||
@@ -29,14 +29,6 @@ class Box(Generic[T]):
|
|||||||
return self.contents
|
return self.contents
|
||||||
|
|
||||||
|
|
||||||
T_contra = TypeVar('T_contra', contravariant=True)
|
|
||||||
|
|
||||||
|
|
||||||
class InBox(Generic[T_contra]):
|
|
||||||
def put(self, item: T) -> None:
|
|
||||||
self.contents = item
|
|
||||||
|
|
||||||
|
|
||||||
T_co = TypeVar('T_co', covariant=True)
|
T_co = TypeVar('T_co', covariant=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -46,3 +38,11 @@ class OutBox(Generic[T_co]):
|
|||||||
|
|
||||||
def get(self) -> Any:
|
def get(self) -> Any:
|
||||||
return self.contents
|
return self.contents
|
||||||
|
|
||||||
|
|
||||||
|
T_contra = TypeVar('T_contra', contravariant=True)
|
||||||
|
|
||||||
|
|
||||||
|
class InBox(Generic[T_contra]):
|
||||||
|
def put(self, item: T) -> None:
|
||||||
|
self.contents = item
|
||||||
|
|||||||
@@ -53,34 +53,29 @@ Tests for __iadd__:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# tag::ADDABLE_BINGO[]
|
# tag::ADDABLE_BINGO[]
|
||||||
import itertools # <1>
|
|
||||||
|
|
||||||
from tombola import Tombola
|
from tombola import Tombola
|
||||||
from bingo import BingoCage
|
from bingo import BingoCage
|
||||||
|
|
||||||
|
|
||||||
class AddableBingoCage(BingoCage): # <2>
|
class AddableBingoCage(BingoCage): # <1>
|
||||||
|
|
||||||
def __add__(self, other):
|
def __add__(self, other):
|
||||||
if isinstance(other, Tombola): # <3>
|
if isinstance(other, Tombola): # <2>
|
||||||
return AddableBingoCage(self.inspect() + other.inspect())
|
return AddableBingoCage(self.inspect() + other.inspect())
|
||||||
else:
|
else:
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
def __iadd__(self, other):
|
def __iadd__(self, other):
|
||||||
if isinstance(other, Tombola):
|
if isinstance(other, Tombola):
|
||||||
other_iterable = other.inspect() # <4>
|
other_iterable = other.inspect() # <3>
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
other_iterable = iter(other) # <5>
|
other_iterable = iter(other) # <4>
|
||||||
except TypeError: # <6>
|
except TypeError: # <5>
|
||||||
self_cls = type(self).__name__
|
self_cls = type(self).__name__
|
||||||
msg = "right operand in += must be {!r} or an iterable"
|
msg = "right operand in += must be {!r} or an iterable"
|
||||||
raise TypeError(msg.format(self_cls))
|
raise TypeError(msg.format(self_cls))
|
||||||
self.load(other_iterable) # <7>
|
self.load(other_iterable) # <6>
|
||||||
return self # <8>
|
return self # <7>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# end::ADDABLE_BINGO[]
|
# end::ADDABLE_BINGO[]
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ class Tombola(abc.ABC): # <1>
|
|||||||
"""Return `True` if there's at least 1 item, `False` otherwise."""
|
"""Return `True` if there's at least 1 item, `False` otherwise."""
|
||||||
return bool(self.inspect()) # <5>
|
return bool(self.inspect()) # <5>
|
||||||
|
|
||||||
|
|
||||||
def inspect(self):
|
def inspect(self):
|
||||||
"""Return a sorted tuple with the items currently inside."""
|
"""Return a sorted tuple with the items currently inside."""
|
||||||
items = []
|
items = []
|
||||||
@@ -31,5 +30,5 @@ class Tombola(abc.ABC): # <1>
|
|||||||
self.load(items) # <7>
|
self.load(items) # <7>
|
||||||
return tuple(sorted(items))
|
return tuple(sorted(items))
|
||||||
|
|
||||||
|
|
||||||
# END TOMBOLA_ABC
|
# END TOMBOLA_ABC
|
||||||
|
|
||||||
|
|||||||
@@ -1,431 +0,0 @@
|
|||||||
"""
|
|
||||||
A multi-dimensional ``Vector`` class, take 9: operator ``@``
|
|
||||||
|
|
||||||
WARNING: This example requires Python 3.5 or later.
|
|
||||||
|
|
||||||
A ``Vector`` is built from an iterable of numbers::
|
|
||||||
|
|
||||||
>>> Vector([3.1, 4.2])
|
|
||||||
Vector([3.1, 4.2])
|
|
||||||
>>> Vector((3, 4, 5))
|
|
||||||
Vector([3.0, 4.0, 5.0])
|
|
||||||
>>> Vector(range(10))
|
|
||||||
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
|
|
||||||
|
|
||||||
|
|
||||||
Tests with 2-dimensions (same results as ``vector2d_v1.py``)::
|
|
||||||
|
|
||||||
>>> v1 = Vector([3, 4])
|
|
||||||
>>> x, y = v1
|
|
||||||
>>> x, y
|
|
||||||
(3.0, 4.0)
|
|
||||||
>>> v1
|
|
||||||
Vector([3.0, 4.0])
|
|
||||||
>>> v1_clone = eval(repr(v1))
|
|
||||||
>>> v1 == v1_clone
|
|
||||||
True
|
|
||||||
>>> print(v1)
|
|
||||||
(3.0, 4.0)
|
|
||||||
>>> octets = bytes(v1)
|
|
||||||
>>> octets
|
|
||||||
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
|
|
||||||
>>> abs(v1)
|
|
||||||
5.0
|
|
||||||
>>> bool(v1), bool(Vector([0, 0]))
|
|
||||||
(True, False)
|
|
||||||
|
|
||||||
|
|
||||||
Test of ``.frombytes()`` class method:
|
|
||||||
|
|
||||||
>>> v1_clone = Vector.frombytes(bytes(v1))
|
|
||||||
>>> v1_clone
|
|
||||||
Vector([3.0, 4.0])
|
|
||||||
>>> v1 == v1_clone
|
|
||||||
True
|
|
||||||
|
|
||||||
|
|
||||||
Tests with 3-dimensions::
|
|
||||||
|
|
||||||
>>> v1 = Vector([3, 4, 5])
|
|
||||||
>>> x, y, z = v1
|
|
||||||
>>> x, y, z
|
|
||||||
(3.0, 4.0, 5.0)
|
|
||||||
>>> v1
|
|
||||||
Vector([3.0, 4.0, 5.0])
|
|
||||||
>>> v1_clone = eval(repr(v1))
|
|
||||||
>>> v1 == v1_clone
|
|
||||||
True
|
|
||||||
>>> print(v1)
|
|
||||||
(3.0, 4.0, 5.0)
|
|
||||||
>>> abs(v1) # doctest:+ELLIPSIS
|
|
||||||
7.071067811...
|
|
||||||
>>> bool(v1), bool(Vector([0, 0, 0]))
|
|
||||||
(True, False)
|
|
||||||
|
|
||||||
|
|
||||||
Tests with many dimensions::
|
|
||||||
|
|
||||||
>>> v7 = Vector(range(7))
|
|
||||||
>>> v7
|
|
||||||
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
|
|
||||||
>>> abs(v7) # doctest:+ELLIPSIS
|
|
||||||
9.53939201...
|
|
||||||
|
|
||||||
|
|
||||||
Test of ``.__bytes__`` and ``.frombytes()`` methods::
|
|
||||||
|
|
||||||
>>> v1 = Vector([3, 4, 5])
|
|
||||||
>>> v1_clone = Vector.frombytes(bytes(v1))
|
|
||||||
>>> v1_clone
|
|
||||||
Vector([3.0, 4.0, 5.0])
|
|
||||||
>>> v1 == v1_clone
|
|
||||||
True
|
|
||||||
|
|
||||||
|
|
||||||
Tests of sequence behavior::
|
|
||||||
|
|
||||||
>>> v1 = Vector([3, 4, 5])
|
|
||||||
>>> len(v1)
|
|
||||||
3
|
|
||||||
>>> v1[0], v1[len(v1)-1], v1[-1]
|
|
||||||
(3.0, 5.0, 5.0)
|
|
||||||
|
|
||||||
|
|
||||||
Test of slicing::
|
|
||||||
|
|
||||||
>>> v7 = Vector(range(7))
|
|
||||||
>>> v7[-1]
|
|
||||||
6.0
|
|
||||||
>>> v7[1:4]
|
|
||||||
Vector([1.0, 2.0, 3.0])
|
|
||||||
>>> v7[-1:]
|
|
||||||
Vector([6.0])
|
|
||||||
>>> v7[1,2]
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
TypeError: Vector indices must be integers
|
|
||||||
|
|
||||||
|
|
||||||
Tests of dynamic attribute access::
|
|
||||||
|
|
||||||
>>> v7 = Vector(range(10))
|
|
||||||
>>> v7.x
|
|
||||||
0.0
|
|
||||||
>>> v7.y, v7.z, v7.t
|
|
||||||
(1.0, 2.0, 3.0)
|
|
||||||
|
|
||||||
Dynamic attribute lookup failures::
|
|
||||||
|
|
||||||
>>> v7.k
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
AttributeError: 'Vector' object has no attribute 'k'
|
|
||||||
>>> v3 = Vector(range(3))
|
|
||||||
>>> v3.t
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
AttributeError: 'Vector' object has no attribute 't'
|
|
||||||
>>> v3.spam
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
AttributeError: 'Vector' object has no attribute 'spam'
|
|
||||||
|
|
||||||
|
|
||||||
Tests of hashing::
|
|
||||||
|
|
||||||
>>> v1 = Vector([3, 4])
|
|
||||||
>>> v2 = Vector([3.1, 4.2])
|
|
||||||
>>> v3 = Vector([3, 4, 5])
|
|
||||||
>>> v6 = Vector(range(6))
|
|
||||||
>>> hash(v1), hash(v3), hash(v6)
|
|
||||||
(7, 2, 1)
|
|
||||||
|
|
||||||
|
|
||||||
Most hash codes of non-integers vary from a 32-bit to 64-bit Python build::
|
|
||||||
|
|
||||||
>>> import sys
|
|
||||||
>>> hash(v2) == (384307168202284039 if sys.maxsize > 2**32 else 357915986)
|
|
||||||
True
|
|
||||||
|
|
||||||
|
|
||||||
Tests of ``format()`` with Cartesian coordinates in 2D::
|
|
||||||
|
|
||||||
>>> v1 = Vector([3, 4])
|
|
||||||
>>> format(v1)
|
|
||||||
'(3.0, 4.0)'
|
|
||||||
>>> format(v1, '.2f')
|
|
||||||
'(3.00, 4.00)'
|
|
||||||
>>> format(v1, '.3e')
|
|
||||||
'(3.000e+00, 4.000e+00)'
|
|
||||||
|
|
||||||
|
|
||||||
Tests of ``format()`` with Cartesian coordinates in 3D and 7D::
|
|
||||||
|
|
||||||
>>> v3 = Vector([3, 4, 5])
|
|
||||||
>>> format(v3)
|
|
||||||
'(3.0, 4.0, 5.0)'
|
|
||||||
>>> format(Vector(range(7)))
|
|
||||||
'(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)'
|
|
||||||
|
|
||||||
|
|
||||||
Tests of ``format()`` with spherical coordinates in 2D, 3D and 4D::
|
|
||||||
|
|
||||||
>>> format(Vector([1, 1]), 'h') # doctest:+ELLIPSIS
|
|
||||||
'<1.414213..., 0.785398...>'
|
|
||||||
>>> format(Vector([1, 1]), '.3eh')
|
|
||||||
'<1.414e+00, 7.854e-01>'
|
|
||||||
>>> format(Vector([1, 1]), '0.5fh')
|
|
||||||
'<1.41421, 0.78540>'
|
|
||||||
>>> format(Vector([1, 1, 1]), 'h') # doctest:+ELLIPSIS
|
|
||||||
'<1.73205..., 0.95531..., 0.78539...>'
|
|
||||||
>>> format(Vector([2, 2, 2]), '.3eh')
|
|
||||||
'<3.464e+00, 9.553e-01, 7.854e-01>'
|
|
||||||
>>> format(Vector([0, 0, 0]), '0.5fh')
|
|
||||||
'<0.00000, 0.00000, 0.00000>'
|
|
||||||
>>> format(Vector([-1, -1, -1, -1]), 'h') # doctest:+ELLIPSIS
|
|
||||||
'<2.0, 2.09439..., 2.18627..., 3.92699...>'
|
|
||||||
>>> format(Vector([2, 2, 2, 2]), '.3eh')
|
|
||||||
'<4.000e+00, 1.047e+00, 9.553e-01, 7.854e-01>'
|
|
||||||
>>> format(Vector([0, 1, 0, 0]), '0.5fh')
|
|
||||||
'<1.00000, 1.57080, 0.00000, 0.00000>'
|
|
||||||
|
|
||||||
|
|
||||||
Basic tests of operator ``+``::
|
|
||||||
|
|
||||||
>>> v1 = Vector([3, 4, 5])
|
|
||||||
>>> v2 = Vector([6, 7, 8])
|
|
||||||
>>> v1 + v2
|
|
||||||
Vector([9.0, 11.0, 13.0])
|
|
||||||
>>> v1 + v2 == Vector([3+6, 4+7, 5+8])
|
|
||||||
True
|
|
||||||
>>> v3 = Vector([1, 2])
|
|
||||||
>>> v1 + v3 # short vectors are filled with 0.0 on addition
|
|
||||||
Vector([4.0, 6.0, 5.0])
|
|
||||||
|
|
||||||
|
|
||||||
Tests of ``+`` with mixed types::
|
|
||||||
|
|
||||||
>>> v1 + (10, 20, 30)
|
|
||||||
Vector([13.0, 24.0, 35.0])
|
|
||||||
>>> from vector2d_v3 import Vector2d
|
|
||||||
>>> v2d = Vector2d(1, 2)
|
|
||||||
>>> v1 + v2d
|
|
||||||
Vector([4.0, 6.0, 5.0])
|
|
||||||
|
|
||||||
|
|
||||||
Tests of ``+`` with mixed types, swapped operands::
|
|
||||||
|
|
||||||
>>> (10, 20, 30) + v1
|
|
||||||
Vector([13.0, 24.0, 35.0])
|
|
||||||
>>> from vector2d_v3 import Vector2d
|
|
||||||
>>> v2d = Vector2d(1, 2)
|
|
||||||
>>> v2d + v1
|
|
||||||
Vector([4.0, 6.0, 5.0])
|
|
||||||
|
|
||||||
|
|
||||||
Tests of ``+`` with an unsuitable operand:
|
|
||||||
|
|
||||||
>>> v1 + 1
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
TypeError: unsupported operand type(s) for +: 'Vector' and 'int'
|
|
||||||
>>> v1 + 'ABC'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
TypeError: unsupported operand type(s) for +: 'Vector' and 'str'
|
|
||||||
|
|
||||||
|
|
||||||
Basic tests of operator ``*``::
|
|
||||||
|
|
||||||
>>> v1 = Vector([1, 2, 3])
|
|
||||||
>>> v1 * 10
|
|
||||||
Vector([10.0, 20.0, 30.0])
|
|
||||||
>>> 10 * v1
|
|
||||||
Vector([10.0, 20.0, 30.0])
|
|
||||||
|
|
||||||
|
|
||||||
Tests of ``*`` with unusual but valid operands::
|
|
||||||
|
|
||||||
>>> v1 * True
|
|
||||||
Vector([1.0, 2.0, 3.0])
|
|
||||||
>>> from fractions import Fraction
|
|
||||||
>>> v1 * Fraction(1, 3) # doctest:+ELLIPSIS
|
|
||||||
Vector([0.3333..., 0.6666..., 1.0])
|
|
||||||
|
|
||||||
|
|
||||||
Tests of ``*`` with unsuitable operands::
|
|
||||||
|
|
||||||
>>> v1 * (1, 2)
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
TypeError: can't multiply sequence by non-int of type 'Vector'
|
|
||||||
|
|
||||||
|
|
||||||
Tests of operator `==`::
|
|
||||||
|
|
||||||
>>> va = Vector(range(1, 4))
|
|
||||||
>>> vb = Vector([1.0, 2.0, 3.0])
|
|
||||||
>>> va == vb
|
|
||||||
True
|
|
||||||
>>> vc = Vector([1, 2])
|
|
||||||
>>> from vector2d_v3 import Vector2d
|
|
||||||
>>> v2d = Vector2d(1, 2)
|
|
||||||
>>> vc == v2d
|
|
||||||
True
|
|
||||||
>>> va == (1, 2, 3)
|
|
||||||
False
|
|
||||||
|
|
||||||
|
|
||||||
Tests of operator `!=`::
|
|
||||||
|
|
||||||
>>> va != vb
|
|
||||||
False
|
|
||||||
>>> vc != v2d
|
|
||||||
False
|
|
||||||
>>> va != (1, 2, 3)
|
|
||||||
True
|
|
||||||
|
|
||||||
|
|
||||||
Tests for operator `@` (Python >= 3.5), computing the dot product::
|
|
||||||
|
|
||||||
>>> va = Vector([1, 2, 3])
|
|
||||||
>>> vz = Vector([5, 6, 7])
|
|
||||||
>>> va @ vz == 38.0 # 1*5 + 2*6 + 3*7
|
|
||||||
True
|
|
||||||
>>> [10, 20, 30] @ vz
|
|
||||||
380.0
|
|
||||||
>>> va @ 3
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
TypeError: unsupported operand type(s) for @: 'Vector' and 'int'
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from array import array
|
|
||||||
import reprlib
|
|
||||||
import math
|
|
||||||
import functools
|
|
||||||
import operator
|
|
||||||
import itertools
|
|
||||||
import numbers
|
|
||||||
|
|
||||||
|
|
||||||
class Vector:
|
|
||||||
typecode = 'd'
|
|
||||||
|
|
||||||
def __init__(self, components):
|
|
||||||
self._components = array(self.typecode, components)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return iter(self._components)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
components = reprlib.repr(self._components)
|
|
||||||
components = components[components.find('['):-1]
|
|
||||||
return 'Vector({})'.format(components)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(tuple(self))
|
|
||||||
|
|
||||||
def __bytes__(self):
|
|
||||||
return (bytes([ord(self.typecode)]) +
|
|
||||||
bytes(self._components))
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if isinstance(other, Vector):
|
|
||||||
return (len(self) == len(other) and
|
|
||||||
all(a == b for a, b in zip(self, other)))
|
|
||||||
else:
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
hashes = (hash(x) for x in self)
|
|
||||||
return functools.reduce(operator.xor, hashes, 0)
|
|
||||||
|
|
||||||
def __abs__(self):
|
|
||||||
return math.sqrt(sum(x * x for x in self))
|
|
||||||
|
|
||||||
def __bool__(self):
|
|
||||||
return bool(abs(self))
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self._components)
|
|
||||||
|
|
||||||
def __getitem__(self, index):
|
|
||||||
cls = type(self)
|
|
||||||
if isinstance(index, slice):
|
|
||||||
return cls(self._components[index])
|
|
||||||
elif isinstance(index, int):
|
|
||||||
return self._components[index]
|
|
||||||
else:
|
|
||||||
msg = '{.__name__} indices must be integers'
|
|
||||||
raise TypeError(msg.format(cls))
|
|
||||||
|
|
||||||
shortcut_names = 'xyzt'
|
|
||||||
|
|
||||||
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]
|
|
||||||
msg = '{.__name__!r} object has no attribute {!r}'
|
|
||||||
raise AttributeError(msg.format(cls, name))
|
|
||||||
|
|
||||||
def angle(self, n):
|
|
||||||
r = math.sqrt(sum(x * x for x in self[n:]))
|
|
||||||
a = math.atan2(r, self[n-1])
|
|
||||||
if (n == len(self) - 1) and (self[-1] < 0):
|
|
||||||
return math.pi * 2 - a
|
|
||||||
else:
|
|
||||||
return a
|
|
||||||
|
|
||||||
def angles(self):
|
|
||||||
return (self.angle(n) for n in range(1, len(self)))
|
|
||||||
|
|
||||||
def __format__(self, fmt_spec=''):
|
|
||||||
if fmt_spec.endswith('h'): # hyperspherical coordinates
|
|
||||||
fmt_spec = fmt_spec[:-1]
|
|
||||||
coords = itertools.chain([abs(self)],
|
|
||||||
self.angles())
|
|
||||||
outer_fmt = '<{}>'
|
|
||||||
else:
|
|
||||||
coords = self
|
|
||||||
outer_fmt = '({})'
|
|
||||||
components = (format(c, fmt_spec) for c in coords)
|
|
||||||
return outer_fmt.format(', '.join(components))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def frombytes(cls, octets):
|
|
||||||
typecode = chr(octets[0])
|
|
||||||
memv = memoryview(octets[1:]).cast(typecode)
|
|
||||||
return cls(memv)
|
|
||||||
|
|
||||||
def __add__(self, other):
|
|
||||||
try:
|
|
||||||
pairs = itertools.zip_longest(self, other, fillvalue=0.0)
|
|
||||||
return Vector(a + b for a, b in pairs)
|
|
||||||
except TypeError:
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
def __radd__(self, other):
|
|
||||||
return self + other
|
|
||||||
|
|
||||||
def __mul__(self, scalar):
|
|
||||||
if isinstance(scalar, numbers.Real):
|
|
||||||
return Vector(n * scalar for n in self)
|
|
||||||
else:
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
def __rmul__(self, scalar):
|
|
||||||
return self * scalar
|
|
||||||
|
|
||||||
def __matmul__(self, other):
|
|
||||||
try:
|
|
||||||
return sum(a * b for a, b in zip(self, other))
|
|
||||||
except TypeError:
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
def __rmatmul__(self, other):
|
|
||||||
return self @ other # this only works in Python 3.5
|
|
||||||
@@ -264,7 +264,7 @@ class Vector:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
components = reprlib.repr(self._components)
|
components = reprlib.repr(self._components)
|
||||||
components = components[components.find('['):-1]
|
components = components[components.find('['):-1]
|
||||||
return 'Vector({})'.format(components)
|
return f'Vector({components})'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(tuple(self))
|
return str(tuple(self))
|
||||||
@@ -283,7 +283,7 @@ class Vector:
|
|||||||
|
|
||||||
# tag::VECTOR_V6_UNARY[]
|
# tag::VECTOR_V6_UNARY[]
|
||||||
def __abs__(self):
|
def __abs__(self):
|
||||||
return math.sqrt(sum(x * x for x in self))
|
return math.hypot(*self)
|
||||||
|
|
||||||
def __neg__(self):
|
def __neg__(self):
|
||||||
return Vector(-x for x in self) # <1>
|
return Vector(-x for x in self) # <1>
|
||||||
@@ -313,11 +313,11 @@ class Vector:
|
|||||||
pos = cls.shortcut_names.find(name)
|
pos = cls.shortcut_names.find(name)
|
||||||
if 0 <= pos < len(self._components):
|
if 0 <= pos < len(self._components):
|
||||||
return self._components[pos]
|
return self._components[pos]
|
||||||
msg = '{.__name__!r} object has no attribute {!r}'
|
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||||
raise AttributeError(msg.format(cls, name))
|
raise AttributeError(msg)
|
||||||
|
|
||||||
def angle(self, n):
|
def angle(self, n):
|
||||||
r = math.sqrt(sum(x * x for x in self[n:]))
|
r = math.hypot(*self[n:])
|
||||||
a = math.atan2(r, self[n-1])
|
a = math.atan2(r, self[n-1])
|
||||||
if (n == len(self) - 1) and (self[-1] < 0):
|
if (n == len(self) - 1) and (self[-1] < 0):
|
||||||
return math.pi * 2 - a
|
return math.pi * 2 - a
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ class Vector:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
components = reprlib.repr(self._components)
|
components = reprlib.repr(self._components)
|
||||||
components = components[components.find('['):-1]
|
components = components[components.find('['):-1]
|
||||||
return 'Vector({})'.format(components)
|
return f'Vector({components})'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(tuple(self))
|
return str(tuple(self))
|
||||||
@@ -334,7 +334,7 @@ class Vector:
|
|||||||
return functools.reduce(operator.xor, hashes, 0)
|
return functools.reduce(operator.xor, hashes, 0)
|
||||||
|
|
||||||
def __abs__(self):
|
def __abs__(self):
|
||||||
return math.sqrt(sum(x * x for x in self))
|
return math.hypot(*self)
|
||||||
|
|
||||||
def __neg__(self):
|
def __neg__(self):
|
||||||
return Vector(-x for x in self)
|
return Vector(-x for x in self)
|
||||||
@@ -363,11 +363,11 @@ class Vector:
|
|||||||
pos = cls.shortcut_names.find(name)
|
pos = cls.shortcut_names.find(name)
|
||||||
if 0 <= pos < len(self._components):
|
if 0 <= pos < len(self._components):
|
||||||
return self._components[pos]
|
return self._components[pos]
|
||||||
msg = '{.__name__!r} object has no attribute {!r}'
|
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||||
raise AttributeError(msg.format(cls, name))
|
raise AttributeError(msg)
|
||||||
|
|
||||||
def angle(self, n):
|
def angle(self, n):
|
||||||
r = math.sqrt(sum(x * x for x in self[n:]))
|
r = math.hypot(*self[n:])
|
||||||
a = math.atan2(r, self[n-1])
|
a = math.atan2(r, self[n-1])
|
||||||
if (n == len(self) - 1) and (self[-1] < 0):
|
if (n == len(self) - 1) and (self[-1] < 0):
|
||||||
return math.pi * 2 - a
|
return math.pi * 2 - a
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ class Vector:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
components = reprlib.repr(self._components)
|
components = reprlib.repr(self._components)
|
||||||
components = components[components.find('['):-1]
|
components = components[components.find('['):-1]
|
||||||
return 'Vector({})'.format(components)
|
return f'Vector({components})'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(tuple(self))
|
return str(tuple(self))
|
||||||
@@ -340,7 +340,7 @@ class Vector:
|
|||||||
return functools.reduce(operator.xor, hashes, 0)
|
return functools.reduce(operator.xor, hashes, 0)
|
||||||
|
|
||||||
def __abs__(self):
|
def __abs__(self):
|
||||||
return math.sqrt(sum(x * x for x in self))
|
return math.hypot(*self)
|
||||||
|
|
||||||
def __neg__(self):
|
def __neg__(self):
|
||||||
return Vector(-x for x in self)
|
return Vector(-x for x in self)
|
||||||
@@ -369,11 +369,11 @@ class Vector:
|
|||||||
pos = cls.shortcut_names.find(name)
|
pos = cls.shortcut_names.find(name)
|
||||||
if 0 <= pos < len(self._components):
|
if 0 <= pos < len(self._components):
|
||||||
return self._components[pos]
|
return self._components[pos]
|
||||||
msg = '{.__name__!r} object has no attribute {!r}'
|
msg = f'{cls.__name__!r} object has no attribute {name!r}'
|
||||||
raise AttributeError(msg.format(cls, name))
|
raise AttributeError(msg)
|
||||||
|
|
||||||
def angle(self, n):
|
def angle(self, n):
|
||||||
r = math.sqrt(sum(x * x for x in self[n:]))
|
r = math.hypot(*self[n:])
|
||||||
a = math.atan2(r, self[n-1])
|
a = math.atan2(r, self[n-1])
|
||||||
if (n == len(self) - 1) and (self[-1] < 0):
|
if (n == len(self) - 1) and (self[-1] < 0):
|
||||||
return math.pi * 2 - a
|
return math.pi * 2 - a
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ if __name__ == '__main__':
|
|||||||
while abs(delta) <= epsilon:
|
while abs(delta) <= epsilon:
|
||||||
delta = next(ap0) - next(ap1)
|
delta = next(ap0) - next(ap1)
|
||||||
frac = next(ap_frac)
|
frac = next(ap_frac)
|
||||||
iteration +=1
|
iteration += 1
|
||||||
|
|
||||||
print('iteration: {}\tfraction: {}\tepsilon: {}\tdelta: {}'.
|
print('iteration: {}\tfraction: {}\tepsilon: {}\tdelta: {}'.
|
||||||
format(iteration, frac, epsilon, delta))
|
format(iteration, frac, epsilon, delta))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# tag::COLUMNIZE[]
|
# tag::COLUMNIZE[]
|
||||||
from typing import Sequence, Tuple, Iterator
|
from typing import Sequence, Tuple, Iterator
|
||||||
|
|
||||||
def columnize(sequence: Sequence[str], num_columns: int = 0) -> Iterator[Tuple[str, ...]]:
|
def columnize(sequence: Sequence[str], num_columns: int = 0) -> Iterator[Tuple[str, ...]]:
|
||||||
if num_columns == 0:
|
if num_columns == 0:
|
||||||
|
|||||||
@@ -44,8 +44,9 @@ def fibonacci():
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
for x, y in zip(Fibonacci(), fibonacci()):
|
for x, y in zip(Fibonacci(), fibonacci()):
|
||||||
assert x == y, '%s != %s' % (x, y)
|
assert x == y, f'{x} != {y}'
|
||||||
print(x)
|
print(x)
|
||||||
if x > 10**10:
|
if x > 10**10:
|
||||||
break
|
break
|
||||||
print('etc...')
|
print('etc...')
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Sentence: iterate over words using a generator function
|
|||||||
import re
|
import re
|
||||||
import reprlib
|
import reprlib
|
||||||
|
|
||||||
RE_WORD = re.compile('r\w+')
|
RE_WORD = re.compile(r'\w+')
|
||||||
|
|
||||||
|
|
||||||
class Sentence:
|
class Sentence:
|
||||||
@@ -15,7 +15,7 @@ class Sentence:
|
|||||||
self.text = text # <1>
|
self.text = text # <1>
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Sentence(%s)' % reprlib.repr(self.text)
|
return f'Sentence({reprlib.repr(self.text)})'
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
for match in RE_WORD.finditer(self.text): # <2>
|
for match in RE_WORD.finditer(self.text): # <2>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class Sentence:
|
|||||||
self.text = text
|
self.text = text
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Sentence(%s)' % reprlib.repr(self.text)
|
return f'Sentence({reprlib.repr(self.text)})'
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return (match.group() for match in RE_WORD.finditer(self.text))
|
return (match.group() for match in RE_WORD.finditer(self.text))
|
||||||
@@ -29,7 +29,7 @@ def main():
|
|||||||
filename = sys.argv[1]
|
filename = sys.argv[1]
|
||||||
word_number = int(sys.argv[2])
|
word_number = int(sys.argv[2])
|
||||||
except (IndexError, ValueError):
|
except (IndexError, ValueError):
|
||||||
print('Usage: %s <file-name> <word-number>' % sys.argv[0])
|
print(f'Usage: {sys.argv[0]} <file-name> <word-number>')
|
||||||
sys.exit(2) # command line usage error
|
sys.exit(2) # command line usage error
|
||||||
with open(filename, 'rt', encoding='utf-8') as text_file:
|
with open(filename, 'rt', encoding='utf-8') as text_file:
|
||||||
s = Sentence(text_file.read())
|
s = Sentence(text_file.read())
|
||||||
@@ -38,7 +38,7 @@ def main():
|
|||||||
print(word)
|
print(word)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
warnings.warn('last word is #%d, "%s"' % (n, word))
|
warnings.warn(f'last word is #{n}, {word!r}')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class Sentence:
|
|||||||
self.words = RE_WORD.findall(text)
|
self.words = RE_WORD.findall(text)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Sentence(%s)' % reprlib.repr(self.text)
|
return f'Sentence({reprlib.repr(self.text)})'
|
||||||
|
|
||||||
def __iter__(self): # <1>
|
def __iter__(self): # <1>
|
||||||
return SentenceIterator(self.words) # <2>
|
return SentenceIterator(self.words) # <2>
|
||||||
@@ -50,7 +50,7 @@ def main():
|
|||||||
filename = sys.argv[1]
|
filename = sys.argv[1]
|
||||||
word_number = int(sys.argv[2])
|
word_number = int(sys.argv[2])
|
||||||
except (IndexError, ValueError):
|
except (IndexError, ValueError):
|
||||||
print('Usage: %s <file-name> <word-number>' % sys.argv[0])
|
print(f'Usage: {sys.argv[0]} <file-name> <word-number>')
|
||||||
sys.exit(2) # command line usage error
|
sys.exit(2) # command line usage error
|
||||||
with open(filename, 'rt', encoding='utf-8') as text_file:
|
with open(filename, 'rt', encoding='utf-8') as text_file:
|
||||||
s = Sentence(text_file.read())
|
s = Sentence(text_file.read())
|
||||||
@@ -59,7 +59,7 @@ def main():
|
|||||||
print(word)
|
print(word)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
warnings.warn('last word is #%d, "%s"' % (n, word))
|
warnings.warn(f'last word is #{n}, {word!r}')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ class Sentence:
|
|||||||
self.text = text
|
self.text = text
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Sentence(%s)' % reprlib.repr(self.text)
|
return f'Sentence({reprlib.repr(self.text)})'
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
word_iter = RE_WORD.finditer(self.text) # <1>
|
word_iter = RE_WORD.finditer(self.text) # <1>
|
||||||
return SentenceIter(word_iter) # <2>
|
return SentenceIter(word_iter) # <2>
|
||||||
|
|
||||||
|
|
||||||
class SentenceIter():
|
class SentenceIter:
|
||||||
|
|
||||||
def __init__(self, word_iter):
|
def __init__(self, word_iter):
|
||||||
self.word_iter = word_iter # <3>
|
self.word_iter = word_iter # <3>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
from tree import tree
|
from tree import tree
|
||||||
|
|
||||||
SPACES = ' ' * 4
|
SPACES = ' ' * 4
|
||||||
HLINE = '\u2500' # ─ BOX DRAWINGS LIGHT HORIZONTAL
|
HLINE = '\u2500' # ─ BOX DRAWINGS LIGHT HORIZONTAL
|
||||||
HLINE2 = HLINE * 2
|
HLINE2 = HLINE * 2
|
||||||
ELBOW = f'\u2514{HLINE2} ' # └ BOX DRAWINGS LIGHT UP AND RIGHT
|
ELBOW = f'\u2514{HLINE2} ' # └ BOX DRAWINGS LIGHT UP AND RIGHT
|
||||||
TEE = f'\u251C{HLINE2} ' # ├ BOX DRAWINGS LIGHT VERTICAL AND RIGHT
|
TEE = f'\u251C{HLINE2} ' # ├ BOX DRAWINGS LIGHT VERTICAL AND RIGHT
|
||||||
PIPE = f'\u2502 ' # │ BOX DRAWINGS LIGHT VERTICAL
|
PIPE = '\u2502 ' # │ BOX DRAWINGS LIGHT VERTICAL
|
||||||
|
|
||||||
|
|
||||||
def render_lines(tree_iter):
|
def render_lines(tree_iter):
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from pretty_tree import tree, render_lines
|
from pretty_tree import tree, render_lines
|
||||||
|
|
||||||
def test_1_level():
|
def test_1_level():
|
||||||
@@ -7,7 +5,7 @@ def test_1_level():
|
|||||||
expected = [
|
expected = [
|
||||||
'BrokenPipeError',
|
'BrokenPipeError',
|
||||||
]
|
]
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_2_levels_1_leaf():
|
def test_2_levels_1_leaf():
|
||||||
@@ -16,7 +14,7 @@ def test_2_levels_1_leaf():
|
|||||||
'IndentationError',
|
'IndentationError',
|
||||||
'└── TabError',
|
'└── TabError',
|
||||||
]
|
]
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_3_levels_1_leaf():
|
def test_3_levels_1_leaf():
|
||||||
@@ -29,7 +27,7 @@ def test_3_levels_1_leaf():
|
|||||||
'└── Y',
|
'└── Y',
|
||||||
' └── Z',
|
' └── Z',
|
||||||
]
|
]
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_4_levels_1_leaf():
|
def test_4_levels_1_leaf():
|
||||||
@@ -98,4 +96,5 @@ def test_4_levels_4_leaves():
|
|||||||
]
|
]
|
||||||
|
|
||||||
result = list(render_lines(tree(A)))
|
result = list(render_lines(tree(A)))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ def test_1_level():
|
|||||||
class One: pass
|
class One: pass
|
||||||
expected = [('One', 0, True)]
|
expected = [('One', 0, True)]
|
||||||
result = list(tree(One))
|
result = list(tree(One))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_2_levels_2_leaves():
|
def test_2_levels_2_leaves():
|
||||||
@@ -18,7 +18,7 @@ def test_2_levels_2_leaves():
|
|||||||
('Leaf2', 1, True),
|
('Leaf2', 1, True),
|
||||||
]
|
]
|
||||||
result = list(tree(Branch))
|
result = list(tree(Branch))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_3_levels_1_leaf():
|
def test_3_levels_1_leaf():
|
||||||
@@ -31,7 +31,7 @@ def test_3_levels_1_leaf():
|
|||||||
('Z', 2, True),
|
('Z', 2, True),
|
||||||
]
|
]
|
||||||
result = list(tree(X))
|
result = list(tree(X))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_4_levels_1_leaf():
|
def test_4_levels_1_leaf():
|
||||||
@@ -47,7 +47,7 @@ def test_4_levels_1_leaf():
|
|||||||
]
|
]
|
||||||
|
|
||||||
result = list(tree(Level0))
|
result = list(tree(Level0))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_4_levels_3_leaves():
|
def test_4_levels_3_leaves():
|
||||||
@@ -69,7 +69,7 @@ def test_4_levels_3_leaves():
|
|||||||
]
|
]
|
||||||
|
|
||||||
result = list(tree(A))
|
result = list(tree(A))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_many_levels_1_leaf():
|
def test_many_levels_1_leaf():
|
||||||
@@ -87,4 +87,4 @@ def test_many_levels_1_leaf():
|
|||||||
assert len(result) == level_count
|
assert len(result) == level_count
|
||||||
assert result[0] == ('Root', 0, True)
|
assert result[0] == ('Root', 0, True)
|
||||||
assert result[-1] == ('Sub99', 99, True)
|
assert result[-1] == ('Sub99', 99, True)
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ def tree(cls, level=0, last_in_level=True):
|
|||||||
yield cls.__name__, level, last_in_level
|
yield cls.__name__, level, last_in_level
|
||||||
subclasses = cls.__subclasses__()
|
subclasses = cls.__subclasses__()
|
||||||
if subclasses:
|
if subclasses:
|
||||||
last = subclasses[-1]
|
last = subclasses[-1]
|
||||||
for sub_cls in subclasses:
|
for sub_cls in subclasses:
|
||||||
yield from tree(sub_cls, level+1, sub_cls is last)
|
yield from tree(sub_cls, level+1, sub_cls is last)
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ def test_1_level():
|
|||||||
class One: pass
|
class One: pass
|
||||||
expected = ['One']
|
expected = ['One']
|
||||||
result = list(tree(One))
|
result = list(tree(One))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ def display(cls):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
display(BaseException)
|
display(BaseException)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ def test_1_level():
|
|||||||
class One: pass
|
class One: pass
|
||||||
expected = [('One', 0)]
|
expected = [('One', 0)]
|
||||||
result = list(tree(One))
|
result = list(tree(One))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_2_levels_2_leaves():
|
def test_2_levels_2_leaves():
|
||||||
@@ -18,4 +18,4 @@ def test_2_levels_2_leaves():
|
|||||||
('Leaf2', 1),
|
('Leaf2', 1),
|
||||||
]
|
]
|
||||||
result = list(tree(Branch))
|
result = list(tree(Branch))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ def test_1_level():
|
|||||||
class One: pass
|
class One: pass
|
||||||
expected = [('One', 0)]
|
expected = [('One', 0)]
|
||||||
result = list(tree(One))
|
result = list(tree(One))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_2_levels_2_leaves():
|
def test_2_levels_2_leaves():
|
||||||
@@ -18,4 +18,4 @@ def test_2_levels_2_leaves():
|
|||||||
('Leaf2', 1),
|
('Leaf2', 1),
|
||||||
]
|
]
|
||||||
result = list(tree(Branch))
|
result = list(tree(Branch))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ def test_1_level():
|
|||||||
class One: pass
|
class One: pass
|
||||||
expected = [('One', 0)]
|
expected = [('One', 0)]
|
||||||
result = list(tree(One))
|
result = list(tree(One))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_2_levels_2_leaves():
|
def test_2_levels_2_leaves():
|
||||||
@@ -18,7 +18,7 @@ def test_2_levels_2_leaves():
|
|||||||
('Leaf2', 1),
|
('Leaf2', 1),
|
||||||
]
|
]
|
||||||
result = list(tree(Branch))
|
result = list(tree(Branch))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_3_levels_1_leaf():
|
def test_3_levels_1_leaf():
|
||||||
@@ -31,4 +31,4 @@ def test_3_levels_1_leaf():
|
|||||||
('Z', 2),
|
('Z', 2),
|
||||||
]
|
]
|
||||||
result = list(tree(X))
|
result = list(tree(X))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ def test_1_level():
|
|||||||
class One: pass
|
class One: pass
|
||||||
expected = [('One', 0)]
|
expected = [('One', 0)]
|
||||||
result = list(tree(One))
|
result = list(tree(One))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_2_levels_2_leaves():
|
def test_2_levels_2_leaves():
|
||||||
@@ -18,7 +18,7 @@ def test_2_levels_2_leaves():
|
|||||||
('Leaf2', 1),
|
('Leaf2', 1),
|
||||||
]
|
]
|
||||||
result = list(tree(Branch))
|
result = list(tree(Branch))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_3_levels_1_leaf():
|
def test_3_levels_1_leaf():
|
||||||
@@ -31,7 +31,7 @@ def test_3_levels_1_leaf():
|
|||||||
('Z', 2),
|
('Z', 2),
|
||||||
]
|
]
|
||||||
result = list(tree(X))
|
result = list(tree(X))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_4_levels_1_leaf():
|
def test_4_levels_1_leaf():
|
||||||
@@ -47,7 +47,7 @@ def test_4_levels_1_leaf():
|
|||||||
]
|
]
|
||||||
|
|
||||||
result = list(tree(Level0))
|
result = list(tree(Level0))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_4_levels_3_leaves():
|
def test_4_levels_3_leaves():
|
||||||
@@ -69,4 +69,5 @@ def test_4_levels_3_leaves():
|
|||||||
]
|
]
|
||||||
|
|
||||||
result = list(tree(A))
|
result = list(tree(A))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ def test_1_level():
|
|||||||
class One: pass
|
class One: pass
|
||||||
expected = [('One', 0)]
|
expected = [('One', 0)]
|
||||||
result = list(tree(One))
|
result = list(tree(One))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_2_levels_2_leaves():
|
def test_2_levels_2_leaves():
|
||||||
@@ -18,7 +18,7 @@ def test_2_levels_2_leaves():
|
|||||||
('Leaf2', 1),
|
('Leaf2', 1),
|
||||||
]
|
]
|
||||||
result = list(tree(Branch))
|
result = list(tree(Branch))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_3_levels_1_leaf():
|
def test_3_levels_1_leaf():
|
||||||
@@ -31,7 +31,7 @@ def test_3_levels_1_leaf():
|
|||||||
('Z', 2),
|
('Z', 2),
|
||||||
]
|
]
|
||||||
result = list(tree(X))
|
result = list(tree(X))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_4_levels_1_leaf():
|
def test_4_levels_1_leaf():
|
||||||
@@ -47,7 +47,7 @@ def test_4_levels_1_leaf():
|
|||||||
]
|
]
|
||||||
|
|
||||||
result = list(tree(Level0))
|
result = list(tree(Level0))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_4_levels_3_leaves():
|
def test_4_levels_3_leaves():
|
||||||
@@ -69,7 +69,7 @@ def test_4_levels_3_leaves():
|
|||||||
]
|
]
|
||||||
|
|
||||||
result = list(tree(A))
|
result = list(tree(A))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_many_levels_1_leaf():
|
def test_many_levels_1_leaf():
|
||||||
@@ -87,4 +87,5 @@ def test_many_levels_1_leaf():
|
|||||||
assert len(result) == level_count
|
assert len(result) == level_count
|
||||||
assert result[0] == ('Root', 0)
|
assert result[0] == ('Root', 0)
|
||||||
assert result[-1] == ('Sub99', 99)
|
assert result[-1] == ('Sub99', 99)
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ def test_1_level():
|
|||||||
class One: pass
|
class One: pass
|
||||||
expected = [('One', 0)]
|
expected = [('One', 0)]
|
||||||
result = list(tree(One))
|
result = list(tree(One))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_2_levels_2_leaves():
|
def test_2_levels_2_leaves():
|
||||||
@@ -18,7 +18,7 @@ def test_2_levels_2_leaves():
|
|||||||
('Leaf2', 1),
|
('Leaf2', 1),
|
||||||
]
|
]
|
||||||
result = list(tree(Branch))
|
result = list(tree(Branch))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_3_levels_1_leaf():
|
def test_3_levels_1_leaf():
|
||||||
@@ -31,7 +31,7 @@ def test_3_levels_1_leaf():
|
|||||||
('Z', 2),
|
('Z', 2),
|
||||||
]
|
]
|
||||||
result = list(tree(X))
|
result = list(tree(X))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_4_levels_1_leaf():
|
def test_4_levels_1_leaf():
|
||||||
@@ -47,7 +47,7 @@ def test_4_levels_1_leaf():
|
|||||||
]
|
]
|
||||||
|
|
||||||
result = list(tree(Level0))
|
result = list(tree(Level0))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_4_levels_3_leaves():
|
def test_4_levels_3_leaves():
|
||||||
@@ -69,7 +69,7 @@ def test_4_levels_3_leaves():
|
|||||||
]
|
]
|
||||||
|
|
||||||
result = list(tree(A))
|
result = list(tree(A))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|
||||||
def test_many_levels_1_leaf():
|
def test_many_levels_1_leaf():
|
||||||
@@ -87,4 +87,5 @@ def test_many_levels_1_leaf():
|
|||||||
assert len(result) == level_count
|
assert len(result) == level_count
|
||||||
assert result[0] == ('Root', 0)
|
assert result[0] == ('Root', 0)
|
||||||
assert result[-1] == ('Sub99', 99)
|
assert result[-1] == ('Sub99', 99)
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from functools import wraps
|
|||||||
def coroutine(func):
|
def coroutine(func):
|
||||||
"""Decorator: primes `func` by advancing to first `yield`"""
|
"""Decorator: primes `func` by advancing to first `yield`"""
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def primer(*args,**kwargs): # <1>
|
def primer(*args, **kwargs): # <1>
|
||||||
gen = func(*args,**kwargs) # <2>
|
gen = func(*args, **kwargs) # <2>
|
||||||
next(gen) # <3>
|
next(gen) # <3>
|
||||||
return gen # <4>
|
return gen # <4>
|
||||||
return primer
|
return primer
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ See longer sample run at the end of this module.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import random
|
|
||||||
import collections
|
|
||||||
import queue
|
|
||||||
import argparse
|
import argparse
|
||||||
import time
|
import collections
|
||||||
|
import random
|
||||||
|
import queue
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_NUMBER_OF_TAXIS = 3
|
DEFAULT_NUMBER_OF_TAXIS = 3
|
||||||
DEFAULT_END_TIME = 180
|
DEFAULT_END_TIME = 180
|
||||||
@@ -126,8 +126,8 @@ def compute_duration(previous_action):
|
|||||||
elif previous_action == 'going home':
|
elif previous_action == 'going home':
|
||||||
interval = 1
|
interval = 1
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unknown previous_action: %s' % previous_action)
|
raise ValueError(f'Unknown previous_action: {previous_action}')
|
||||||
return int(random.expovariate(1/interval)) + 1
|
return int(random.expovariate(1 / interval)) + 1
|
||||||
|
|
||||||
|
|
||||||
def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS,
|
def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS,
|
||||||
@@ -136,7 +136,7 @@ def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS,
|
|||||||
if seed is not None:
|
if seed is not None:
|
||||||
random.seed(seed) # get reproducible results
|
random.seed(seed) # get reproducible results
|
||||||
|
|
||||||
taxis = {i: taxi_process(i, (i+1)*2, i*DEPARTURE_INTERVAL)
|
taxis = {i: taxi_process(i, (i + 1) * 2, i * DEPARTURE_INTERVAL)
|
||||||
for i in range(num_taxis)}
|
for i in range(num_taxis)}
|
||||||
sim = Simulator(taxis)
|
sim = Simulator(taxis)
|
||||||
sim.run(end_time)
|
sim.run(end_time)
|
||||||
|
|||||||
@@ -27,11 +27,10 @@ See explanation and longer sample run at the end of this module.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import argparse
|
||||||
import random
|
|
||||||
import collections
|
import collections
|
||||||
import queue
|
import queue
|
||||||
import argparse
|
import random
|
||||||
|
|
||||||
DEFAULT_NUMBER_OF_TAXIS = 3
|
DEFAULT_NUMBER_OF_TAXIS = 3
|
||||||
DEFAULT_END_TIME = 80
|
DEFAULT_END_TIME = 80
|
||||||
@@ -44,7 +43,7 @@ Event = collections.namedtuple('Event', 'time proc action')
|
|||||||
|
|
||||||
def compute_delay(interval):
|
def compute_delay(interval):
|
||||||
"""Compute action delay using exponential distribution"""
|
"""Compute action delay using exponential distribution"""
|
||||||
return int(random.expovariate(1/interval)) + 1
|
return int(random.expovariate(1 / interval)) + 1
|
||||||
|
|
||||||
# BEGIN TAXI_PROCESS
|
# BEGIN TAXI_PROCESS
|
||||||
def taxi_process(ident, trips, start_time=0): # <1>
|
def taxi_process(ident, trips, start_time=0): # <1>
|
||||||
@@ -68,7 +67,6 @@ class Simulator:
|
|||||||
self.events = queue.PriorityQueue()
|
self.events = queue.PriorityQueue()
|
||||||
self.procs = dict(procs_map)
|
self.procs = dict(procs_map)
|
||||||
|
|
||||||
|
|
||||||
def run(self, end_time): # <1>
|
def run(self, end_time): # <1>
|
||||||
"""Schedule and display events until time is up"""
|
"""Schedule and display events until time is up"""
|
||||||
# schedule the first event for each cab
|
# schedule the first event for each cab
|
||||||
@@ -108,7 +106,7 @@ def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS,
|
|||||||
if seed is not None:
|
if seed is not None:
|
||||||
random.seed(seed) # get reproducible results
|
random.seed(seed) # get reproducible results
|
||||||
|
|
||||||
taxis = {i: taxi_process(i, (i+1)*2, i*DEPARTURE_INTERVAL)
|
taxis = {i: taxi_process(i, (i + 1) * 2, i * DEPARTURE_INTERVAL)
|
||||||
for i in range(num_taxis)}
|
for i in range(num_taxis)}
|
||||||
sim = Simulator(taxis)
|
sim = Simulator(taxis)
|
||||||
sim.run(end_time)
|
sim.run(end_time)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ certifi==2020.12.5
|
|||||||
chardet==4.0.0
|
chardet==4.0.0
|
||||||
idna==2.10
|
idna==2.10
|
||||||
requests==2.25.1
|
requests==2.25.1
|
||||||
urllib3==1.26.4
|
urllib3==1.26.3
|
||||||
tqdm==4.56.2
|
tqdm==4.56.2
|
||||||
multidict==5.1.0
|
multidict==5.1.0
|
||||||
yarl==1.6.3
|
yarl==1.6.3
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
from curio import TaskGroup
|
|
||||||
import curio.socket as socket
|
|
||||||
from collections.abc import Iterable, AsyncIterator
|
from collections.abc import Iterable, AsyncIterator
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
from curio import TaskGroup
|
||||||
|
import curio.socket as socket
|
||||||
|
|
||||||
|
|
||||||
class Result(NamedTuple):
|
class Result(NamedTuple):
|
||||||
domain: str
|
domain: str
|
||||||
found: bool
|
found: bool
|
||||||
|
|
||||||
|
|
||||||
async def probe(domain: str) -> Result:
|
async def probe(domain: str) -> Result:
|
||||||
try:
|
try:
|
||||||
await socket.getaddrinfo(domain, None)
|
await socket.getaddrinfo(domain, None)
|
||||||
except socket.gaierror:
|
except socket.gaierror:
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ schedule_v2.py: property to get venue linked to an event
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# tag::SCHEDULE2_RECORD[]
|
# tag::SCHEDULE2_RECORD[]
|
||||||
import json
|
|
||||||
import inspect # <1>
|
import inspect # <1>
|
||||||
|
import json
|
||||||
|
|
||||||
JSON_PATH = 'data/osconfeed.json'
|
JSON_PATH = 'data/osconfeed.json'
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ schedule_v3.py: property to get list of event speakers
|
|||||||
# end::SCHEDULE3_DEMO[]
|
# end::SCHEDULE3_DEMO[]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
|
||||||
import inspect
|
import inspect
|
||||||
|
import json
|
||||||
|
|
||||||
JSON_PATH = 'data/osconfeed.json'
|
JSON_PATH = 'data/osconfeed.json'
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ schedule_v4.py: homegrown cached property for speakers
|
|||||||
# end::SCHEDULE4_DEMO[]
|
# end::SCHEDULE4_DEMO[]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
|
||||||
import inspect
|
import inspect
|
||||||
|
import json
|
||||||
|
|
||||||
JSON_PATH = 'data/osconfeed.json'
|
JSON_PATH = 'data/osconfeed.json'
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class Quantity:
|
|||||||
msg = f'{self.storage_name} must be > 0'
|
msg = f'{self.storage_name} must be > 0'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
# no __get__ needed
|
# no __get__ needed # <4>
|
||||||
|
|
||||||
class LineItem:
|
class LineItem:
|
||||||
weight = Quantity() # <5>
|
weight = Quantity() # <5>
|
||||||
|
|||||||
@@ -32,5 +32,5 @@ class NonBlank(Validated):
|
|||||||
value = value.strip()
|
value = value.strip()
|
||||||
if len(value) == 0:
|
if len(value) == 0:
|
||||||
raise ValueError(f'{name} cannot be blank')
|
raise ValueError(f'{name} cannot be blank')
|
||||||
return value # <8>
|
return value # <2>
|
||||||
# end::MODEL_V5_VALIDATED_SUB[]
|
# end::MODEL_V5_VALIDATED_SUB[]
|
||||||
|
|||||||
@@ -5,20 +5,20 @@ Overriding descriptor (a.k.a. data descriptor or enforced descriptor):
|
|||||||
|
|
||||||
>>> obj = Managed() # <1>
|
>>> obj = Managed() # <1>
|
||||||
>>> obj.over # <2>
|
>>> obj.over # <2>
|
||||||
-> Overriding.__get__(<Overriding object>, <Managed object>,
|
-> Overriding.__get__(<Overriding object>, <Managed object>,
|
||||||
<class Managed>)
|
<class Managed>)
|
||||||
>>> Managed.over # <3>
|
>>> Managed.over # <3>
|
||||||
-> Overriding.__get__(<Overriding object>, None, <class Managed>)
|
-> Overriding.__get__(<Overriding object>, None, <class Managed>)
|
||||||
>>> obj.over = 7 # <4>
|
>>> obj.over = 7 # <4>
|
||||||
-> Overriding.__set__(<Overriding object>, <Managed object>, 7)
|
-> Overriding.__set__(<Overriding object>, <Managed object>, 7)
|
||||||
>>> obj.over # <5>
|
>>> obj.over # <5>
|
||||||
-> Overriding.__get__(<Overriding object>, <Managed object>,
|
-> Overriding.__get__(<Overriding object>, <Managed object>,
|
||||||
<class Managed>)
|
<class Managed>)
|
||||||
>>> obj.__dict__['over'] = 8 # <6>
|
>>> obj.__dict__['over'] = 8 # <6>
|
||||||
>>> vars(obj) # <7>
|
>>> vars(obj) # <7>
|
||||||
{'over': 8}
|
{'over': 8}
|
||||||
>>> obj.over # <8>
|
>>> obj.over # <8>
|
||||||
-> Overriding.__get__(<Overriding object>, <Managed object>,
|
-> Overriding.__get__(<Overriding object>, <Managed object>,
|
||||||
<class Managed>)
|
<class Managed>)
|
||||||
|
|
||||||
# end::DESCR_KINDS_DEMO1[]
|
# end::DESCR_KINDS_DEMO1[]
|
||||||
@@ -50,7 +50,7 @@ Non-overriding descriptor (a.k.a. non-data descriptor or shadowable descriptor):
|
|||||||
|
|
||||||
>>> obj = Managed()
|
>>> obj = Managed()
|
||||||
>>> obj.non_over # <1>
|
>>> obj.non_over # <1>
|
||||||
-> NonOverriding.__get__(<NonOverriding object>, <Managed object>,
|
-> NonOverriding.__get__(<NonOverriding object>, <Managed object>,
|
||||||
<class Managed>)
|
<class Managed>)
|
||||||
>>> obj.non_over = 7 # <2>
|
>>> obj.non_over = 7 # <2>
|
||||||
>>> obj.non_over # <3>
|
>>> obj.non_over # <3>
|
||||||
@@ -59,7 +59,7 @@ Non-overriding descriptor (a.k.a. non-data descriptor or shadowable descriptor):
|
|||||||
-> NonOverriding.__get__(<NonOverriding object>, None, <class Managed>)
|
-> NonOverriding.__get__(<NonOverriding object>, None, <class Managed>)
|
||||||
>>> del obj.non_over # <5>
|
>>> del obj.non_over # <5>
|
||||||
>>> obj.non_over # <6>
|
>>> obj.non_over # <6>
|
||||||
-> NonOverriding.__get__(<NonOverriding object>, <Managed object>,
|
-> NonOverriding.__get__(<NonOverriding object>, <Managed object>,
|
||||||
<class Managed>)
|
<class Managed>)
|
||||||
|
|
||||||
# end::DESCR_KINDS_DEMO3[]
|
# end::DESCR_KINDS_DEMO3[]
|
||||||
|
|||||||
22
25-class-metaprog/autoconst/autoconst.py
Normal file
22
25-class-metaprog/autoconst/autoconst.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# tag::WilyDict[]
|
||||||
|
class WilyDict(dict):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.__next_value = 0
|
||||||
|
|
||||||
|
def __missing__(self, key):
|
||||||
|
if key.startswith('__') and key.endswith('__'):
|
||||||
|
raise KeyError(key)
|
||||||
|
self[key] = value = self.__next_value
|
||||||
|
self.__next_value += 1
|
||||||
|
return value
|
||||||
|
# end::WilyDict[]
|
||||||
|
|
||||||
|
# tag::AUTOCONST[]
|
||||||
|
class AutoConstMeta(type):
|
||||||
|
def __prepare__(name, bases, **kwargs):
|
||||||
|
return WilyDict()
|
||||||
|
|
||||||
|
class AutoConst(metaclass=AutoConstMeta):
|
||||||
|
pass
|
||||||
|
# end::AUTOCONST[]
|
||||||
55
25-class-metaprog/autoconst/autoconst_demo.py
Executable file
55
25-class-metaprog/autoconst/autoconst_demo.py
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Testing ``WilyDict``::
|
||||||
|
|
||||||
|
>>> from autoconst import WilyDict
|
||||||
|
>>> wd = WilyDict()
|
||||||
|
>>> len(wd)
|
||||||
|
0
|
||||||
|
>>> wd['first']
|
||||||
|
0
|
||||||
|
>>> wd
|
||||||
|
{'first': 0}
|
||||||
|
>>> wd['second']
|
||||||
|
1
|
||||||
|
>>> wd['third']
|
||||||
|
2
|
||||||
|
>>> len(wd)
|
||||||
|
3
|
||||||
|
>>> wd
|
||||||
|
{'first': 0, 'second': 1, 'third': 2}
|
||||||
|
>>> wd['__magic__']
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
KeyError: '__magic__'
|
||||||
|
|
||||||
|
Testing ``AutoConst``::
|
||||||
|
|
||||||
|
>>> from autoconst import AutoConst
|
||||||
|
|
||||||
|
# tag::AUTOCONST[]
|
||||||
|
>>> class Flavor(AutoConst):
|
||||||
|
... banana
|
||||||
|
... coconut
|
||||||
|
... vanilla
|
||||||
|
...
|
||||||
|
>>> Flavor.vanilla
|
||||||
|
2
|
||||||
|
>>> Flavor.banana, Flavor.coconut
|
||||||
|
(0, 1)
|
||||||
|
|
||||||
|
# end::AUTOCONST[]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from autoconst import AutoConst
|
||||||
|
|
||||||
|
|
||||||
|
class Flavor(AutoConst):
|
||||||
|
banana
|
||||||
|
coconut
|
||||||
|
vanilla
|
||||||
|
|
||||||
|
|
||||||
|
print('Flavor.vanilla ==', Flavor.vanilla)
|
||||||
34
25-class-metaprog/bulkfood/README.md
Normal file
34
25-class-metaprog/bulkfood/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Legacy Class Descriptor and Metaclass Examples
|
||||||
|
|
||||||
|
Examples from _Fluent Python, First Edition_—Chapter 21, _Class Metaprogramming_,
|
||||||
|
that are mentioned in _Fluent Python, Second Edition_—Chapter 25, _Class Metaprogramming_.
|
||||||
|
|
||||||
|
These examples were developed with Python 3.4.
|
||||||
|
They run correctly in Python 3.9, but now it is easier to fullfill the same requirements
|
||||||
|
without resorting to class decorators or metaclasses.
|
||||||
|
|
||||||
|
I have preserved them here as examples of class metaprogramming techniques
|
||||||
|
that you may find in legacy code, and that can be refactored to simpler code
|
||||||
|
using a base class with `__init_subclass__` and decorators implementing `__set_name__`.
|
||||||
|
|
||||||
|
## Suggested Exercise
|
||||||
|
|
||||||
|
If you'd like to practice the concepts presented in chapters 24 and 25 of
|
||||||
|
_Fluent Python, Second Edition_,
|
||||||
|
you may to refactor the most advanced example, `model_v8.py` with these changes:
|
||||||
|
|
||||||
|
1. Simplify the `AutoStorage` descriptor by implementing `__set_name__`.
|
||||||
|
This will allow you to simplify the `EntityMeta` metaclass as well.
|
||||||
|
|
||||||
|
2. Rewrite the `Entity` class to use `__init_subclass__` instead of the `EntityMeta` metaclass—which you can then delete.
|
||||||
|
|
||||||
|
Nothing should change in the `bulkfood_v8.py` code, and its doctests should still pass.
|
||||||
|
|
||||||
|
To run the doctests while refactoring, it's often convenient to pass the `-f` option,
|
||||||
|
to exit the test runner on the first failing test.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ python3 -m doctest -f bulkfood_v8.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Enjoy!
|
||||||
84
25-class-metaprog/bulkfood/bulkfood_v6.py
Normal file
84
25-class-metaprog/bulkfood/bulkfood_v6.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
"""
|
||||||
|
|
||||||
|
A line item for a bulk food order has description, weight and price fields::
|
||||||
|
|
||||||
|
>>> raisins = LineItem('Golden raisins', 10, 6.95)
|
||||||
|
>>> raisins.weight, raisins.description, raisins.price
|
||||||
|
(10, 'Golden raisins', 6.95)
|
||||||
|
|
||||||
|
A ``subtotal`` method gives the total price for that line item::
|
||||||
|
|
||||||
|
>>> raisins.subtotal()
|
||||||
|
69.5
|
||||||
|
|
||||||
|
The weight of a ``LineItem`` must be greater than 0::
|
||||||
|
|
||||||
|
>>> raisins.weight = -20
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: value must be > 0
|
||||||
|
|
||||||
|
No change was made::
|
||||||
|
|
||||||
|
>>> raisins.weight
|
||||||
|
10
|
||||||
|
|
||||||
|
The value of the attributes managed by the descriptors are stored in
|
||||||
|
alternate attributes, created by the descriptors in each ``LineItem``
|
||||||
|
instance::
|
||||||
|
|
||||||
|
# tag::LINEITEM_V6_DEMO[]
|
||||||
|
>>> raisins = LineItem('Golden raisins', 10, 6.95)
|
||||||
|
>>> dir(raisins)[:3]
|
||||||
|
['_NonBlank#description', '_Quantity#price', '_Quantity#weight']
|
||||||
|
>>> LineItem.description.storage_name
|
||||||
|
'_NonBlank#description'
|
||||||
|
>>> raisins.description
|
||||||
|
'Golden raisins'
|
||||||
|
>>> getattr(raisins, '_NonBlank#description')
|
||||||
|
'Golden raisins'
|
||||||
|
|
||||||
|
# end::LINEITEM_V6_DEMO[]
|
||||||
|
|
||||||
|
If the descriptor is accessed in the class, the descriptor object is
|
||||||
|
returned:
|
||||||
|
|
||||||
|
>>> LineItem.weight # doctest: +ELLIPSIS
|
||||||
|
<model_v6.Quantity object at 0x...>
|
||||||
|
>>> LineItem.weight.storage_name
|
||||||
|
'_Quantity#weight'
|
||||||
|
|
||||||
|
|
||||||
|
The `NonBlank` descriptor prevents empty or blank strings to be used
|
||||||
|
for the description:
|
||||||
|
|
||||||
|
>>> br_nuts = LineItem('Brazil Nuts', 10, 34.95)
|
||||||
|
>>> br_nuts.description = ' '
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: value cannot be empty or blank
|
||||||
|
>>> void = LineItem('', 1, 1)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: value cannot be empty or blank
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::LINEITEM_V6[]
|
||||||
|
import model_v6 as model
|
||||||
|
|
||||||
|
@model.entity # <1>
|
||||||
|
class LineItem:
|
||||||
|
description = model.NonBlank()
|
||||||
|
weight = model.Quantity()
|
||||||
|
price = model.Quantity()
|
||||||
|
|
||||||
|
def __init__(self, description, weight, price):
|
||||||
|
self.description = description
|
||||||
|
self.weight = weight
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def subtotal(self):
|
||||||
|
return self.weight * self.price
|
||||||
|
# end::LINEITEM_V6[]
|
||||||
79
25-class-metaprog/bulkfood/bulkfood_v7.py
Normal file
79
25-class-metaprog/bulkfood/bulkfood_v7.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
|
||||||
|
A line item for a bulk food order has description, weight and price fields::
|
||||||
|
|
||||||
|
>>> raisins = LineItem('Golden raisins', 10, 6.95)
|
||||||
|
>>> raisins.weight, raisins.description, raisins.price
|
||||||
|
(10, 'Golden raisins', 6.95)
|
||||||
|
|
||||||
|
A ``subtotal`` method gives the total price for that line item::
|
||||||
|
|
||||||
|
>>> raisins.subtotal()
|
||||||
|
69.5
|
||||||
|
|
||||||
|
The weight of a ``LineItem`` must be greater than 0::
|
||||||
|
|
||||||
|
>>> raisins.weight = -20
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: value must be > 0
|
||||||
|
|
||||||
|
No change was made::
|
||||||
|
|
||||||
|
>>> raisins.weight
|
||||||
|
10
|
||||||
|
|
||||||
|
The value of the attributes managed by the descriptors are stored in
|
||||||
|
alternate attributes, created by the descriptors in each ``LineItem``
|
||||||
|
instance::
|
||||||
|
|
||||||
|
>>> raisins = LineItem('Golden raisins', 10, 6.95)
|
||||||
|
>>> dir(raisins)[:3]
|
||||||
|
['_NonBlank#description', '_Quantity#price', '_Quantity#weight']
|
||||||
|
>>> LineItem.description.storage_name
|
||||||
|
'_NonBlank#description'
|
||||||
|
>>> raisins.description
|
||||||
|
'Golden raisins'
|
||||||
|
>>> getattr(raisins, '_NonBlank#description')
|
||||||
|
'Golden raisins'
|
||||||
|
|
||||||
|
If the descriptor is accessed in the class, the descriptor object is
|
||||||
|
returned:
|
||||||
|
|
||||||
|
>>> LineItem.weight # doctest: +ELLIPSIS
|
||||||
|
<model_v7.Quantity object at 0x...>
|
||||||
|
>>> LineItem.weight.storage_name
|
||||||
|
'_Quantity#weight'
|
||||||
|
|
||||||
|
|
||||||
|
The `NonBlank` descriptor prevents empty or blank strings to be used
|
||||||
|
for the description:
|
||||||
|
|
||||||
|
>>> br_nuts = LineItem('Brazil Nuts', 10, 34.95)
|
||||||
|
>>> br_nuts.description = ' '
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: value cannot be empty or blank
|
||||||
|
>>> void = LineItem('', 1, 1)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: value cannot be empty or blank
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::LINEITEM_V7[]
|
||||||
|
import model_v7 as model
|
||||||
|
|
||||||
|
class LineItem(model.Entity): # <1>
|
||||||
|
description = model.NonBlank()
|
||||||
|
weight = model.Quantity()
|
||||||
|
price = model.Quantity()
|
||||||
|
|
||||||
|
def __init__(self, description, weight, price):
|
||||||
|
self.description = description
|
||||||
|
self.weight = weight
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def subtotal(self):
|
||||||
|
return self.weight * self.price
|
||||||
|
# end::LINEITEM_V7[]
|
||||||
86
25-class-metaprog/bulkfood/bulkfood_v8.py
Normal file
86
25-class-metaprog/bulkfood/bulkfood_v8.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
"""
|
||||||
|
|
||||||
|
A line item for a bulk food order has description, weight and price fields::
|
||||||
|
|
||||||
|
>>> raisins = LineItem('Golden raisins', 10, 6.95)
|
||||||
|
>>> raisins.weight, raisins.description, raisins.price
|
||||||
|
(10, 'Golden raisins', 6.95)
|
||||||
|
|
||||||
|
A ``subtotal`` method gives the total price for that line item::
|
||||||
|
|
||||||
|
>>> raisins.subtotal()
|
||||||
|
69.5
|
||||||
|
|
||||||
|
The weight of a ``LineItem`` must be greater than 0::
|
||||||
|
|
||||||
|
>>> raisins.weight = -20
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: value must be > 0
|
||||||
|
|
||||||
|
No change was made::
|
||||||
|
|
||||||
|
>>> raisins.weight
|
||||||
|
10
|
||||||
|
|
||||||
|
>>> raisins = LineItem('Golden raisins', 10, 6.95)
|
||||||
|
>>> dir(raisins)[:3]
|
||||||
|
['_NonBlank#description', '_Quantity#price', '_Quantity#weight']
|
||||||
|
>>> LineItem.description.storage_name
|
||||||
|
'_NonBlank#description'
|
||||||
|
>>> raisins.description
|
||||||
|
'Golden raisins'
|
||||||
|
>>> getattr(raisins, '_NonBlank#description')
|
||||||
|
'Golden raisins'
|
||||||
|
|
||||||
|
If the descriptor is accessed in the class, the descriptor object is
|
||||||
|
returned:
|
||||||
|
|
||||||
|
>>> LineItem.weight # doctest: +ELLIPSIS
|
||||||
|
<model_v8.Quantity object at 0x...>
|
||||||
|
>>> LineItem.weight.storage_name
|
||||||
|
'_Quantity#weight'
|
||||||
|
|
||||||
|
|
||||||
|
The `NonBlank` descriptor prevents empty or blank strings to be used
|
||||||
|
for the description:
|
||||||
|
|
||||||
|
>>> br_nuts = LineItem('Brazil Nuts', 10, 34.95)
|
||||||
|
>>> br_nuts.description = ' '
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: value cannot be empty or blank
|
||||||
|
>>> void = LineItem('', 1, 1)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: value cannot be empty or blank
|
||||||
|
|
||||||
|
|
||||||
|
Fields can be retrieved in the order they were declared:
|
||||||
|
|
||||||
|
# tag::LINEITEM_V8_DEMO[]
|
||||||
|
>>> for name in LineItem.field_names():
|
||||||
|
... print(name)
|
||||||
|
...
|
||||||
|
description
|
||||||
|
weight
|
||||||
|
price
|
||||||
|
|
||||||
|
# end::LINEITEM_V8_DEMO[]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import model_v8 as model
|
||||||
|
|
||||||
|
class LineItem(model.Entity):
|
||||||
|
description = model.NonBlank()
|
||||||
|
weight = model.Quantity()
|
||||||
|
price = model.Quantity()
|
||||||
|
|
||||||
|
def __init__(self, description, weight, price):
|
||||||
|
self.description = description
|
||||||
|
self.weight = weight
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def subtotal(self):
|
||||||
|
return self.weight * self.price
|
||||||
60
25-class-metaprog/bulkfood/model_v6.py
Normal file
60
25-class-metaprog/bulkfood/model_v6.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import abc
|
||||||
|
|
||||||
|
|
||||||
|
class AutoStorage:
|
||||||
|
__counter = 0
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
cls = self.__class__
|
||||||
|
prefix = cls.__name__
|
||||||
|
index = cls.__counter
|
||||||
|
self.storage_name = '_{}#{}'.format(prefix, index)
|
||||||
|
cls.__counter += 1
|
||||||
|
|
||||||
|
def __get__(self, instance, owner):
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
else:
|
||||||
|
return getattr(instance, self.storage_name)
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
setattr(instance, self.storage_name, value)
|
||||||
|
|
||||||
|
|
||||||
|
class Validated(abc.ABC, AutoStorage):
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
value = self.validate(instance, value)
|
||||||
|
super().__set__(instance, value)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def validate(self, instance, value):
|
||||||
|
"""return validated value or raise ValueError"""
|
||||||
|
|
||||||
|
|
||||||
|
class Quantity(Validated):
|
||||||
|
"""a number greater than zero"""
|
||||||
|
|
||||||
|
def validate(self, instance, value):
|
||||||
|
if value <= 0:
|
||||||
|
raise ValueError('value must be > 0')
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class NonBlank(Validated):
|
||||||
|
"""a string with at least one non-space character"""
|
||||||
|
|
||||||
|
def validate(self, instance, value):
|
||||||
|
value = value.strip()
|
||||||
|
if len(value) == 0:
|
||||||
|
raise ValueError('value cannot be empty or blank')
|
||||||
|
return value
|
||||||
|
|
||||||
|
# tag::MODEL_V6[]
|
||||||
|
def entity(cls): # <1>
|
||||||
|
for key, attr in cls.__dict__.items(): # <2>
|
||||||
|
if isinstance(attr, Validated): # <3>
|
||||||
|
type_name = type(attr).__name__
|
||||||
|
attr.storage_name = '_{}#{}'.format(type_name, key) # <4>
|
||||||
|
return cls # <5>
|
||||||
|
# end::MODEL_V6[]
|
||||||
66
25-class-metaprog/bulkfood/model_v7.py
Normal file
66
25-class-metaprog/bulkfood/model_v7.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import abc
|
||||||
|
|
||||||
|
|
||||||
|
class AutoStorage:
|
||||||
|
__counter = 0
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
cls = self.__class__
|
||||||
|
prefix = cls.__name__
|
||||||
|
index = cls.__counter
|
||||||
|
self.storage_name = '_{}#{}'.format(prefix, index)
|
||||||
|
cls.__counter += 1
|
||||||
|
|
||||||
|
def __get__(self, instance, owner):
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
else:
|
||||||
|
return getattr(instance, self.storage_name)
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
setattr(instance, self.storage_name, value)
|
||||||
|
|
||||||
|
|
||||||
|
class Validated(abc.ABC, AutoStorage):
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
value = self.validate(instance, value)
|
||||||
|
super().__set__(instance, value)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def validate(self, instance, value):
|
||||||
|
"""return validated value or raise ValueError"""
|
||||||
|
|
||||||
|
|
||||||
|
class Quantity(Validated):
|
||||||
|
"""a number greater than zero"""
|
||||||
|
|
||||||
|
def validate(self, instance, value):
|
||||||
|
if value <= 0:
|
||||||
|
raise ValueError('value must be > 0')
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class NonBlank(Validated):
|
||||||
|
"""a string with at least one non-space character"""
|
||||||
|
|
||||||
|
def validate(self, instance, value):
|
||||||
|
value = value.strip()
|
||||||
|
if len(value) == 0:
|
||||||
|
raise ValueError('value cannot be empty or blank')
|
||||||
|
return value
|
||||||
|
|
||||||
|
# tag::MODEL_V7[]
|
||||||
|
class EntityMeta(type):
|
||||||
|
"""Metaclass for business entities with validated fields"""
|
||||||
|
|
||||||
|
def __init__(cls, name, bases, attr_dict):
|
||||||
|
super().__init__(name, bases, attr_dict) # <1>
|
||||||
|
for key, attr in attr_dict.items(): # <2>
|
||||||
|
if isinstance(attr, Validated):
|
||||||
|
type_name = type(attr).__name__
|
||||||
|
attr.storage_name = '_{}#{}'.format(type_name, key)
|
||||||
|
|
||||||
|
class Entity(metaclass=EntityMeta): # <3>
|
||||||
|
"""Business entity with validated fields"""
|
||||||
|
# end::MODEL_V7[]
|
||||||
80
25-class-metaprog/bulkfood/model_v8.py
Normal file
80
25-class-metaprog/bulkfood/model_v8.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import abc
|
||||||
|
import collections
|
||||||
|
|
||||||
|
|
||||||
|
class AutoStorage:
|
||||||
|
__counter = 0
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
cls = self.__class__
|
||||||
|
prefix = cls.__name__
|
||||||
|
index = cls.__counter
|
||||||
|
self.storage_name = '_{}#{}'.format(prefix, index)
|
||||||
|
cls.__counter += 1
|
||||||
|
|
||||||
|
def __get__(self, instance, owner):
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
else:
|
||||||
|
return getattr(instance, self.storage_name)
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
setattr(instance, self.storage_name, value)
|
||||||
|
|
||||||
|
|
||||||
|
class Validated(abc.ABC, AutoStorage):
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
value = self.validate(instance, value)
|
||||||
|
super().__set__(instance, value)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def validate(self, instance, value):
|
||||||
|
"""return validated value or raise ValueError"""
|
||||||
|
|
||||||
|
|
||||||
|
class Quantity(Validated):
|
||||||
|
"""a number greater than zero"""
|
||||||
|
|
||||||
|
def validate(self, instance, value):
|
||||||
|
if value <= 0:
|
||||||
|
raise ValueError('value must be > 0')
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class NonBlank(Validated):
|
||||||
|
"""a string with at least one non-space character"""
|
||||||
|
|
||||||
|
def validate(self, instance, value):
|
||||||
|
value = value.strip()
|
||||||
|
if len(value) == 0:
|
||||||
|
raise ValueError('value cannot be empty or blank')
|
||||||
|
return value
|
||||||
|
|
||||||
|
# tag::MODEL_V8[]
|
||||||
|
class EntityMeta(type):
|
||||||
|
"""Metaclass for business entities with validated fields"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __prepare__(cls, name, bases):
|
||||||
|
return collections.OrderedDict() # <1>
|
||||||
|
|
||||||
|
def __init__(cls, name, bases, attr_dict):
|
||||||
|
super().__init__(name, bases, attr_dict)
|
||||||
|
cls._field_names = [] # <2>
|
||||||
|
for key, attr in attr_dict.items(): # <3>
|
||||||
|
if isinstance(attr, Validated):
|
||||||
|
type_name = type(attr).__name__
|
||||||
|
attr.storage_name = '_{}#{}'.format(type_name, key)
|
||||||
|
cls._field_names.append(key) # <4>
|
||||||
|
|
||||||
|
|
||||||
|
class Entity(metaclass=EntityMeta):
|
||||||
|
"""Business entity with validated fields"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def field_names(cls): # <5>
|
||||||
|
for name in cls._field_names:
|
||||||
|
yield name
|
||||||
|
|
||||||
|
# end::MODEL_V8[]
|
||||||
@@ -8,13 +8,13 @@ used to create an instance, and provides a nice ``__repr__``::
|
|||||||
... class Movie:
|
... class Movie:
|
||||||
... title: str
|
... title: str
|
||||||
... year: int
|
... year: int
|
||||||
... megabucks: float
|
... box_office: float
|
||||||
...
|
...
|
||||||
>>> movie = Movie(title='The Godfather', year=1972, megabucks=137) # <3>
|
>>> movie = Movie(title='The Godfather', year=1972, box_office=137)
|
||||||
>>> movie.title
|
>>> movie.title
|
||||||
'The Godfather'
|
'The Godfather'
|
||||||
>>> movie # <4>
|
>>> movie
|
||||||
Movie(title='The Godfather', year=1972, megabucks=137.0)
|
Movie(title='The Godfather', year=1972, box_office=137.0)
|
||||||
|
|
||||||
# end::MOVIE_DEFINITION[]
|
# end::MOVIE_DEFINITION[]
|
||||||
|
|
||||||
@@ -23,14 +23,14 @@ including during instantiation::
|
|||||||
|
|
||||||
# tag::MOVIE_TYPE_VALIDATION[]
|
# tag::MOVIE_TYPE_VALIDATION[]
|
||||||
|
|
||||||
>>> movie.year = 'MCMLXXII' # <1>
|
>>> blockbuster = Movie(title='Avatar', year=2009, box_office='billions')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: 'billions' is not compatible with box_office:float
|
||||||
|
>>> movie.year = 'MCMLXXII'
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
TypeError: 'MCMLXXII' is not compatible with year:int
|
TypeError: 'MCMLXXII' is not compatible with year:int
|
||||||
>>> blockbuster = Movie(title='Avatar', year=2009, megabucks='billions') # <2>
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
TypeError: 'billions' is not compatible with megabucks:float
|
|
||||||
|
|
||||||
# end::MOVIE_TYPE_VALIDATION[]
|
# end::MOVIE_TYPE_VALIDATION[]
|
||||||
|
|
||||||
@@ -40,13 +40,13 @@ default values::
|
|||||||
# tag::MOVIE_DEFAULTS[]
|
# tag::MOVIE_DEFAULTS[]
|
||||||
|
|
||||||
>>> Movie(title='Life of Brian')
|
>>> Movie(title='Life of Brian')
|
||||||
Movie(title='Life of Brian', year=0, megabucks=0.0)
|
Movie(title='Life of Brian', year=0, box_office=0.0)
|
||||||
|
|
||||||
# end::MOVIE_DEFAULTS[]
|
# end::MOVIE_DEFAULTS[]
|
||||||
|
|
||||||
Providing extra arguments to the constructor is not allowed::
|
Providing extra arguments to the constructor is not allowed::
|
||||||
|
|
||||||
>>> blockbuster = Movie(title='Avatar', year=2009, megabucks=2000,
|
>>> blockbuster = Movie(title='Avatar', year=2009, box_office=2000,
|
||||||
... director='James Cameron')
|
... director='James Cameron')
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
@@ -62,109 +62,90 @@ Creating new attributes at runtime is restricted as well::
|
|||||||
The `_as_dict` instance creates a `dict` from the attributes of a `Movie` object::
|
The `_as_dict` instance creates a `dict` from the attributes of a `Movie` object::
|
||||||
|
|
||||||
>>> movie._asdict()
|
>>> movie._asdict()
|
||||||
{'title': 'The Godfather', 'year': 1972, 'megabucks': 137.0}
|
{'title': 'The Godfather', 'year': 1972, 'box_office': 137.0}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections.abc import Callable # <1>
|
from collections.abc import Callable # <1>
|
||||||
from typing import Any, NoReturn, get_type_hints
|
from typing import Any, NoReturn, get_type_hints
|
||||||
|
|
||||||
MISSING = object() # <2>
|
|
||||||
|
|
||||||
|
|
||||||
class Field:
|
class Field:
|
||||||
def __init__(self, name: str, constructor: Callable) -> None: # <3>
|
def __init__(self, name: str, constructor: Callable) -> None: # <2>
|
||||||
|
if not callable(constructor) or constructor is type(None):
|
||||||
|
raise TypeError(f'{name!r} type hint must be callable')
|
||||||
self.name = name
|
self.name = name
|
||||||
self.constructor = constructor
|
self.constructor = constructor
|
||||||
|
|
||||||
def __set__(self, instance: Any, value: Any) -> None: # <4>
|
def __set__(self, instance: Any, value: Any) -> None: # <3>
|
||||||
if value is MISSING: # <5>
|
if value is ...: # <4>
|
||||||
value = self.constructor()
|
value = self.constructor()
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
value = self.constructor(value) # <6>
|
value = self.constructor(value) # <5>
|
||||||
except (TypeError, ValueError) as e:
|
except (TypeError, ValueError) as e:
|
||||||
type_name = self.constructor.__name__
|
type_name = self.constructor.__name__
|
||||||
msg = (
|
msg = (
|
||||||
f'{value!r} is not compatible with {self.name}:{type_name}'
|
f'{value!r} is not compatible with {self.name}:{type_name}'
|
||||||
)
|
)
|
||||||
raise TypeError(msg) from e
|
raise TypeError(msg) from e
|
||||||
instance.__dict__[self.name] = value # <7>
|
instance.__dict__[self.name] = value # <6>
|
||||||
|
|
||||||
|
|
||||||
# tag::CHECKED_DECORATOR_TOP[]
|
# tag::CHECKED_DECORATOR[]
|
||||||
_methods_to_inject: list[Callable] = []
|
def checked(cls: type) -> type: # <1>
|
||||||
_classmethods_to_inject: list[Callable] = []
|
for name, constructor in _fields(cls).items(): # <2>
|
||||||
|
setattr(cls, name, Field(name, constructor)) # <3>
|
||||||
|
|
||||||
def checked(cls: type) -> type: # <2>
|
cls._fields = classmethod(_fields) #type: ignore # <4>
|
||||||
for func in _methods_to_inject:
|
|
||||||
name = func.__name__
|
|
||||||
setattr(cls, name, func) # <5>
|
|
||||||
|
|
||||||
for func in _classmethods_to_inject:
|
instance_methods = ( # <5>
|
||||||
name = func.__name__
|
__init__,
|
||||||
setattr(cls, name, classmethod(func)) # <5>
|
__repr__,
|
||||||
|
__setattr__,
|
||||||
|
_asdict,
|
||||||
|
__flag_unknown_attrs,
|
||||||
|
)
|
||||||
|
for method in instance_methods: # <6>
|
||||||
|
setattr(cls, method.__name__, method)
|
||||||
|
|
||||||
for name, constructor in _fields(cls).items(): # <4>
|
return cls # <7>
|
||||||
setattr(cls, name, Field(name, constructor)) # <5>
|
# end::CHECKED_DECORATOR[]
|
||||||
|
|
||||||
return cls
|
# tag::CHECKED_METHODS[]
|
||||||
|
def _fields(cls: type) -> dict[str, type]:
|
||||||
|
|
||||||
def _method(func: Callable) -> Callable:
|
|
||||||
_methods_to_inject.append(func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
|
|
||||||
def _classmethod(func: Callable) -> Callable:
|
|
||||||
_classmethods_to_inject.append(func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
# tag::CHECKED_METHODS_TOP[]
|
|
||||||
@_classmethod
|
|
||||||
def _fields(cls: type) -> dict[str, type]: # <1>
|
|
||||||
return get_type_hints(cls)
|
return get_type_hints(cls)
|
||||||
|
|
||||||
@_method
|
|
||||||
def __init__(self: Any, **kwargs: Any) -> None:
|
def __init__(self: Any, **kwargs: Any) -> None:
|
||||||
for name in self._fields(): # <6>
|
for name in self._fields():
|
||||||
value = kwargs.pop(name, MISSING) # <7>
|
value = kwargs.pop(name, ...)
|
||||||
setattr(self, name, value) # <8>
|
setattr(self, name, value)
|
||||||
if kwargs: # <9>
|
if kwargs:
|
||||||
self.__flag_unknown_attrs(*kwargs) # <10>
|
self.__flag_unknown_attrs(*kwargs)
|
||||||
|
|
||||||
@_method
|
def __setattr__(self: Any, name: str, value: Any) -> None:
|
||||||
def __setattr__(self: Any, name: str, value: Any) -> None: # <11>
|
if name in self._fields():
|
||||||
if name in self._fields(): # <12>
|
|
||||||
cls = self.__class__
|
cls = self.__class__
|
||||||
descriptor = getattr(cls, name)
|
descriptor = getattr(cls, name)
|
||||||
descriptor.__set__(self, value) # <13>
|
descriptor.__set__(self, value)
|
||||||
else: # <14>
|
else:
|
||||||
self.__flag_unknown_attrs(name)
|
self.__flag_unknown_attrs(name)
|
||||||
# end::CHECKED_METHODS_TOP[]
|
|
||||||
|
|
||||||
# tag::CHECKED_METHODS_BOTTOM[]
|
def __flag_unknown_attrs(self: Any, *names: str) -> NoReturn:
|
||||||
@_method
|
|
||||||
def __flag_unknown_attrs(self: Any, *names: str) -> NoReturn: # <1>
|
|
||||||
plural = 's' if len(names) > 1 else ''
|
plural = 's' if len(names) > 1 else ''
|
||||||
extra = ', '.join(f'{name!r}' for name in names)
|
extra = ', '.join(f'{name!r}' for name in names)
|
||||||
cls_name = repr(self.__class__.__name__)
|
cls_name = repr(self.__class__.__name__)
|
||||||
raise AttributeError(f'{cls_name} has no attribute{plural} {extra}')
|
raise AttributeError(f'{cls_name} has no attribute{plural} {extra}')
|
||||||
|
|
||||||
|
def _asdict(self: Any) -> dict[str, Any]:
|
||||||
@_method
|
|
||||||
def _asdict(self: Any) -> dict[str, Any]: # <2>
|
|
||||||
return {
|
return {
|
||||||
name: getattr(self, name)
|
name: getattr(self, name)
|
||||||
for name, attr in self.__class__.__dict__.items()
|
for name, attr in self.__class__.__dict__.items()
|
||||||
if isinstance(attr, Field)
|
if isinstance(attr, Field)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __repr__(self: Any) -> str:
|
||||||
@_method
|
|
||||||
def __repr__(self: Any) -> str: # <3>
|
|
||||||
kwargs = ', '.join(
|
kwargs = ', '.join(
|
||||||
f'{key}={value!r}' for key, value in self._asdict().items()
|
f'{key}={value!r}' for key, value in self._asdict().items()
|
||||||
)
|
)
|
||||||
return f'{self.__class__.__name__}({kwargs})'
|
return f'{self.__class__.__name__}({kwargs})'
|
||||||
# end::CHECKED_METHODS_BOTTOM[]
|
# end::CHECKED_METHODS[]
|
||||||
26
25-class-metaprog/checked/decorator/checkeddeco_demo.py
Executable file
26
25-class-metaprog/checked/decorator/checkeddeco_demo.py
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from checkeddeco import checked
|
||||||
|
|
||||||
|
@checked
|
||||||
|
class Movie:
|
||||||
|
title: str
|
||||||
|
year: int
|
||||||
|
box_office: float
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# No static type checker can understand this...
|
||||||
|
movie = Movie(title='The Godfather', year=1972, box_office=137) # type: ignore
|
||||||
|
print(movie.title)
|
||||||
|
print(movie)
|
||||||
|
try:
|
||||||
|
# remove the "type: ignore" comment to see Mypy correctly spot the error
|
||||||
|
movie.year = 'MCMLXXII' # type: ignore
|
||||||
|
except TypeError as e:
|
||||||
|
print(e)
|
||||||
|
try:
|
||||||
|
# Again, no static type checker can understand this...
|
||||||
|
blockbuster = Movie(title='Avatar', year=2009, box_office='billions') # type: ignore
|
||||||
|
except TypeError as e:
|
||||||
|
print(e)
|
||||||
@@ -38,3 +38,13 @@ def test_constructor_attribute_error():
|
|||||||
felix = Cat(name='Felix', weight=3.2, age=7)
|
felix = Cat(name='Felix', weight=3.2, age=7)
|
||||||
|
|
||||||
assert str(e.value) == "'Cat' has no attribute 'age'"
|
assert str(e.value) == "'Cat' has no attribute 'age'"
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_invalid_constructor():
|
||||||
|
with pytest.raises(TypeError) as e:
|
||||||
|
@checked
|
||||||
|
class Cat:
|
||||||
|
name: str
|
||||||
|
weight: None
|
||||||
|
|
||||||
|
assert str(e.value) == "'weight' type hint must be callable"
|
||||||
8
25-class-metaprog/checked/checkedlib_demo.py → 25-class-metaprog/checked/initsub/checked_demo.py
Normal file → Executable file
8
25-class-metaprog/checked/checkedlib_demo.py → 25-class-metaprog/checked/initsub/checked_demo.py
Normal file → Executable file
@@ -1,13 +1,15 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from checkedlib import Checked
|
from checkedlib import Checked
|
||||||
|
|
||||||
class Movie(Checked):
|
class Movie(Checked):
|
||||||
title: str
|
title: str
|
||||||
year: int
|
year: int
|
||||||
megabucks: float
|
box_office: float
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
movie = Movie(title='The Godfather', year=1972, megabucks=137)
|
movie = Movie(title='The Godfather', year=1972, box_office=137)
|
||||||
print(movie.title)
|
print(movie.title)
|
||||||
print(movie)
|
print(movie)
|
||||||
try:
|
try:
|
||||||
@@ -16,6 +18,6 @@ if __name__ == '__main__':
|
|||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
print(e)
|
print(e)
|
||||||
try:
|
try:
|
||||||
blockbuster = Movie(title='Avatar', year=2009, megabucks='billions')
|
blockbuster = Movie(title='Avatar', year=2009, box_office='billions')
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
print(e)
|
print(e)
|
||||||
@@ -7,29 +7,29 @@ used to create an instance, and provides a nice ``__repr__``::
|
|||||||
>>> class Movie(Checked): # <1>
|
>>> class Movie(Checked): # <1>
|
||||||
... title: str # <2>
|
... title: str # <2>
|
||||||
... year: int
|
... year: int
|
||||||
... megabucks: float
|
... box_office: float
|
||||||
...
|
...
|
||||||
>>> movie = Movie(title='The Godfather', year=1972, megabucks=137) # <3>
|
>>> movie = Movie(title='The Godfather', year=1972, box_office=137) # <3>
|
||||||
>>> movie.title
|
>>> movie.title
|
||||||
'The Godfather'
|
'The Godfather'
|
||||||
>>> movie # <4>
|
>>> movie # <4>
|
||||||
Movie(title='The Godfather', year=1972, megabucks=137.0)
|
Movie(title='The Godfather', year=1972, box_office=137.0)
|
||||||
|
|
||||||
# end::MOVIE_DEFINITION[]
|
# end::MOVIE_DEFINITION[]
|
||||||
|
|
||||||
The type of arguments is runtime checked when an attribute is set,
|
The type of arguments is runtime checked during instantiation
|
||||||
including during instantiation::
|
and when an attribute is set::
|
||||||
|
|
||||||
# tag::MOVIE_TYPE_VALIDATION[]
|
# tag::MOVIE_TYPE_VALIDATION[]
|
||||||
|
|
||||||
>>> movie.year = 'MCMLXXII' # <1>
|
>>> blockbuster = Movie(title='Avatar', year=2009, box_office='billions')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: 'billions' is not compatible with box_office:float
|
||||||
|
>>> movie.year = 'MCMLXXII'
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
TypeError: 'MCMLXXII' is not compatible with year:int
|
TypeError: 'MCMLXXII' is not compatible with year:int
|
||||||
>>> blockbuster = Movie(title='Avatar', year=2009, megabucks='billions') # <2>
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
TypeError: 'billions' is not compatible with megabucks:float
|
|
||||||
|
|
||||||
# end::MOVIE_TYPE_VALIDATION[]
|
# end::MOVIE_TYPE_VALIDATION[]
|
||||||
|
|
||||||
@@ -39,29 +39,29 @@ default values::
|
|||||||
# tag::MOVIE_DEFAULTS[]
|
# tag::MOVIE_DEFAULTS[]
|
||||||
|
|
||||||
>>> Movie(title='Life of Brian')
|
>>> Movie(title='Life of Brian')
|
||||||
Movie(title='Life of Brian', year=0, megabucks=0.0)
|
Movie(title='Life of Brian', year=0, box_office=0.0)
|
||||||
|
|
||||||
# end::MOVIE_DEFAULTS[]
|
# end::MOVIE_DEFAULTS[]
|
||||||
|
|
||||||
Providing extra arguments to the constructor is not allowed::
|
Providing extra arguments to the constructor is not allowed::
|
||||||
|
|
||||||
>>> blockbuster = Movie(title='Avatar', year=2009, megabucks=2000,
|
>>> blockbuster = Movie(title='Avatar', year=2009, box_office=2000,
|
||||||
... director='James Cameron')
|
... director='James Cameron')
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
AttributeError: 'Movie' has no attribute 'director'
|
AttributeError: 'Movie' object has no attribute 'director'
|
||||||
|
|
||||||
Creating new attributes at runtime is restricted as well::
|
Creating new attributes at runtime is restricted as well::
|
||||||
|
|
||||||
>>> movie.director = 'Francis Ford Coppola'
|
>>> movie.director = 'Francis Ford Coppola'
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
AttributeError: 'Movie' has no attribute 'director'
|
AttributeError: 'Movie' object has no attribute 'director'
|
||||||
|
|
||||||
The `_as_dict` instance creates a `dict` from the attributes of a `Movie` object::
|
The `_as_dict` instance creates a `dict` from the attributes of a `Movie` object::
|
||||||
|
|
||||||
>>> movie._asdict()
|
>>> movie._asdict()
|
||||||
{'title': 'The Godfather', 'year': 1972, 'megabucks': 137.0}
|
{'title': 'The Godfather', 'year': 1972, 'box_office': 137.0}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -69,27 +69,25 @@ The `_as_dict` instance creates a `dict` from the attributes of a `Movie` object
|
|||||||
from collections.abc import Callable # <1>
|
from collections.abc import Callable # <1>
|
||||||
from typing import Any, NoReturn, get_type_hints
|
from typing import Any, NoReturn, get_type_hints
|
||||||
|
|
||||||
MISSING = object() # <2>
|
|
||||||
|
|
||||||
|
|
||||||
class Field:
|
class Field:
|
||||||
def __init__(self, name: str, constructor: Callable) -> None: # <3>
|
def __init__(self, name: str, constructor: Callable) -> None: # <2>
|
||||||
|
if not callable(constructor) or constructor is type(None): # <3>
|
||||||
|
raise TypeError(f'{name!r} type hint must be callable')
|
||||||
self.name = name
|
self.name = name
|
||||||
self.constructor = constructor
|
self.constructor = constructor
|
||||||
|
|
||||||
def __set__(self, instance: 'Checked', value: Any) -> None: # <4>
|
def __set__(self, instance: Any, value: Any) -> None:
|
||||||
if value is MISSING: # <5>
|
if value is ...: # <4>
|
||||||
value = self.constructor()
|
value = self.constructor()
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
value = self.constructor(value) # <6>
|
value = self.constructor(value) # <5>
|
||||||
except (TypeError, ValueError) as e:
|
except (TypeError, ValueError) as e: # <6>
|
||||||
type_name = self.constructor.__name__
|
type_name = self.constructor.__name__
|
||||||
msg = f'{value!r} is not compatible with {self.name}:{type_name}'
|
msg = f'{value!r} is not compatible with {self.name}:{type_name}'
|
||||||
raise TypeError(msg) from e
|
raise TypeError(msg) from e
|
||||||
instance.__dict__[self.name] = value # <7>
|
instance.__dict__[self.name] = value # <7>
|
||||||
|
|
||||||
|
|
||||||
# end::CHECKED_FIELD[]
|
# end::CHECKED_FIELD[]
|
||||||
|
|
||||||
# tag::CHECKED_TOP[]
|
# tag::CHECKED_TOP[]
|
||||||
@@ -105,36 +103,36 @@ class Checked:
|
|||||||
|
|
||||||
def __init__(self, **kwargs: Any) -> None:
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
for name in self._fields(): # <6>
|
for name in self._fields(): # <6>
|
||||||
value = kwargs.pop(name, MISSING) # <7>
|
value = kwargs.pop(name, ...) # <7>
|
||||||
setattr(self, name, value) # <8>
|
setattr(self, name, value) # <8>
|
||||||
if kwargs: # <9>
|
if kwargs: # <9>
|
||||||
self.__flag_unknown_attrs(*kwargs) # <10>
|
self.__flag_unknown_attrs(*kwargs) # <10>
|
||||||
|
|
||||||
def __setattr__(self, name: str, value: Any) -> None: # <11>
|
|
||||||
if name in self._fields(): # <12>
|
|
||||||
cls = self.__class__
|
|
||||||
descriptor = getattr(cls, name)
|
|
||||||
descriptor.__set__(self, value) # <13>
|
|
||||||
else: # <14>
|
|
||||||
self.__flag_unknown_attrs(name)
|
|
||||||
|
|
||||||
# end::CHECKED_TOP[]
|
# end::CHECKED_TOP[]
|
||||||
|
|
||||||
# tag::CHECKED_BOTTOM[]
|
# tag::CHECKED_BOTTOM[]
|
||||||
def __flag_unknown_attrs(self, *names: str) -> NoReturn: # <1>
|
def __setattr__(self, name: str, value: Any) -> None: # <1>
|
||||||
|
if name in self._fields(): # <2>
|
||||||
|
cls = self.__class__
|
||||||
|
descriptor = getattr(cls, name)
|
||||||
|
descriptor.__set__(self, value) # <3>
|
||||||
|
else: # <4>
|
||||||
|
self.__flag_unknown_attrs(name)
|
||||||
|
|
||||||
|
def __flag_unknown_attrs(self, *names: str) -> NoReturn: # <5>
|
||||||
plural = 's' if len(names) > 1 else ''
|
plural = 's' if len(names) > 1 else ''
|
||||||
extra = ', '.join(f'{name!r}' for name in names)
|
extra = ', '.join(f'{name!r}' for name in names)
|
||||||
cls_name = repr(self.__class__.__name__)
|
cls_name = repr(self.__class__.__name__)
|
||||||
raise AttributeError(f'{cls_name} has no attribute{plural} {extra}')
|
raise AttributeError(f'{cls_name} object has no attribute{plural} {extra}')
|
||||||
|
|
||||||
def _asdict(self) -> dict[str, Any]: # <2>
|
def _asdict(self) -> dict[str, Any]: # <6>
|
||||||
return {
|
return {
|
||||||
name: getattr(self, name)
|
name: getattr(self, name)
|
||||||
for name, attr in self.__class__.__dict__.items()
|
for name, attr in self.__class__.__dict__.items()
|
||||||
if isinstance(attr, Field)
|
if isinstance(attr, Field)
|
||||||
}
|
}
|
||||||
|
|
||||||
def __repr__(self) -> str: # <3>
|
def __repr__(self) -> str: # <7>
|
||||||
kwargs = ', '.join(
|
kwargs = ', '.join(
|
||||||
f'{key}={value!r}' for key, value in self._asdict().items()
|
f'{key}={value!r}' for key, value in self._asdict().items()
|
||||||
)
|
)
|
||||||
59
25-class-metaprog/checked/initsub/checkedlib_test.py
Normal file
59
25-class-metaprog/checked/initsub/checkedlib_test.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
from checkedlib import Checked
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_validation_type_error():
|
||||||
|
class Cat(Checked):
|
||||||
|
name: str
|
||||||
|
weight: float
|
||||||
|
|
||||||
|
with pytest.raises(TypeError) as e:
|
||||||
|
felix = Cat(name='Felix', weight=None)
|
||||||
|
|
||||||
|
assert str(e.value) == 'None is not compatible with weight:float'
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_validation_value_error():
|
||||||
|
class Cat(Checked):
|
||||||
|
name: str
|
||||||
|
weight: float
|
||||||
|
|
||||||
|
with pytest.raises(TypeError) as e:
|
||||||
|
felix = Cat(name='Felix', weight='half stone')
|
||||||
|
|
||||||
|
assert str(e.value) == "'half stone' is not compatible with weight:float"
|
||||||
|
|
||||||
|
|
||||||
|
def test_constructor_attribute_error():
|
||||||
|
class Cat(Checked):
|
||||||
|
name: str
|
||||||
|
weight: float
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError) as e:
|
||||||
|
felix = Cat(name='Felix', weight=3.2, age=7)
|
||||||
|
|
||||||
|
assert str(e.value) == "'Cat' object has no attribute 'age'"
|
||||||
|
|
||||||
|
|
||||||
|
def test_assignment_attribute_error():
|
||||||
|
class Cat(Checked):
|
||||||
|
name: str
|
||||||
|
weight: float
|
||||||
|
|
||||||
|
felix = Cat(name='Felix', weight=3.2)
|
||||||
|
with pytest.raises(AttributeError) as e:
|
||||||
|
felix.color = 'tan'
|
||||||
|
|
||||||
|
assert str(e.value) == "'Cat' object has no attribute 'color'"
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_invalid_constructor():
|
||||||
|
with pytest.raises(TypeError) as e:
|
||||||
|
class Cat(Checked):
|
||||||
|
name: str
|
||||||
|
weight: None
|
||||||
|
|
||||||
|
assert str(e.value) == "'weight' type hint must be callable"
|
||||||
|
|
||||||
19
25-class-metaprog/checkeddeco/checkeddeco_demo.py → 25-class-metaprog/checked/metaclass/checked_demo.py
Normal file → Executable file
19
25-class-metaprog/checkeddeco/checkeddeco_demo.py → 25-class-metaprog/checked/metaclass/checked_demo.py
Normal file → Executable file
@@ -1,22 +1,25 @@
|
|||||||
from checkeddeco import checked
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
@checked
|
# tag::MOVIE_DEMO[]
|
||||||
class Movie:
|
from checkedlib import Checked
|
||||||
|
|
||||||
|
class Movie(Checked):
|
||||||
title: str
|
title: str
|
||||||
year: int
|
year: int
|
||||||
megabucks: float
|
box_office: float
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
movie = Movie(title='The Godfather', year=1972, megabucks=137)
|
movie = Movie(title='The Godfather', year=1972, box_office=137)
|
||||||
print(movie.title)
|
|
||||||
print(movie)
|
print(movie)
|
||||||
|
print(movie.title)
|
||||||
|
# end::MOVIE_DEMO[]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# remove the "type: ignore" comment to see Mypy error
|
# remove the "type: ignore" comment to see Mypy error
|
||||||
movie.year = 'MCMLXXII' # type: ignore
|
movie.year = 'MCMLXXII' # type: ignore
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
print(e)
|
print(e)
|
||||||
try:
|
try:
|
||||||
blockbuster = Movie(title='Avatar', year=2009, megabucks='billions')
|
blockbuster = Movie(title='Avatar', year=2009, box_office='billions')
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
print(e)
|
print(e)
|
||||||
148
25-class-metaprog/checked/metaclass/checkedlib.py
Normal file
148
25-class-metaprog/checked/metaclass/checkedlib.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
"""
|
||||||
|
A ``Checked`` subclass definition requires that keyword arguments are
|
||||||
|
used to create an instance, and provides a nice ``__repr__``::
|
||||||
|
|
||||||
|
# tag::MOVIE_DEFINITION[]
|
||||||
|
|
||||||
|
>>> class Movie(Checked): # <1>
|
||||||
|
... title: str # <2>
|
||||||
|
... year: int
|
||||||
|
... box_office: float
|
||||||
|
...
|
||||||
|
>>> movie = Movie(title='The Godfather', year=1972, box_office=137) # <3>
|
||||||
|
>>> movie.title
|
||||||
|
'The Godfather'
|
||||||
|
>>> movie # <4>
|
||||||
|
Movie(title='The Godfather', year=1972, box_office=137.0)
|
||||||
|
|
||||||
|
# end::MOVIE_DEFINITION[]
|
||||||
|
|
||||||
|
The type of arguments is runtime checked during instantiation
|
||||||
|
and when an attribute is set::
|
||||||
|
|
||||||
|
# tag::MOVIE_TYPE_VALIDATION[]
|
||||||
|
|
||||||
|
>>> blockbuster = Movie(title='Avatar', year=2009, box_office='billions')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: 'billions' is not compatible with box_office:float
|
||||||
|
>>> movie.year = 'MCMLXXII'
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: 'MCMLXXII' is not compatible with year:int
|
||||||
|
|
||||||
|
# end::MOVIE_TYPE_VALIDATION[]
|
||||||
|
|
||||||
|
Attributes not passed as arguments to the constructor are initialized with
|
||||||
|
default values::
|
||||||
|
|
||||||
|
# tag::MOVIE_DEFAULTS[]
|
||||||
|
|
||||||
|
>>> Movie(title='Life of Brian')
|
||||||
|
Movie(title='Life of Brian', year=0, box_office=0.0)
|
||||||
|
|
||||||
|
# end::MOVIE_DEFAULTS[]
|
||||||
|
|
||||||
|
Providing extra arguments to the constructor is not allowed::
|
||||||
|
|
||||||
|
>>> blockbuster = Movie(title='Avatar', year=2009, box_office=2000,
|
||||||
|
... director='James Cameron')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Movie' object has no attribute 'director'
|
||||||
|
|
||||||
|
Creating new attributes at runtime is restricted as well::
|
||||||
|
|
||||||
|
>>> movie.director = 'Francis Ford Coppola'
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Movie' object has no attribute 'director'
|
||||||
|
|
||||||
|
The `_as_dict` instance creates a `dict` from the attributes of a `Movie` object::
|
||||||
|
|
||||||
|
>>> movie._asdict()
|
||||||
|
{'title': 'The Godfather', 'year': 1972, 'box_office': 137.0}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any, NoReturn, get_type_hints
|
||||||
|
|
||||||
|
# tag::CHECKED_FIELD[]
|
||||||
|
class Field:
|
||||||
|
def __init__(self, name: str, constructor: Callable) -> None:
|
||||||
|
if not callable(constructor) or constructor is type(None):
|
||||||
|
raise TypeError(f'{name!r} type hint must be callable')
|
||||||
|
self.name = name
|
||||||
|
self.storage_name = '_' + name # <1>
|
||||||
|
self.constructor = constructor
|
||||||
|
|
||||||
|
def __get__(self, instance, owner=None): # <2>
|
||||||
|
return getattr(instance, self.storage_name) # <3>
|
||||||
|
|
||||||
|
def __set__(self, instance: Any, value: Any) -> None:
|
||||||
|
if value is ...:
|
||||||
|
value = self.constructor()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
value = self.constructor(value)
|
||||||
|
except (TypeError, ValueError) as e:
|
||||||
|
type_name = self.constructor.__name__
|
||||||
|
msg = f'{value!r} is not compatible with {self.name}:{type_name}'
|
||||||
|
raise TypeError(msg) from e
|
||||||
|
setattr(instance, self.storage_name, value) # <4>
|
||||||
|
# end::CHECKED_FIELD[]
|
||||||
|
|
||||||
|
# tag::CHECKED_META[]
|
||||||
|
class CheckedMeta(type):
|
||||||
|
|
||||||
|
def __new__(meta_cls, cls_name, bases, cls_dict): # <1>
|
||||||
|
if '__slots__' not in cls_dict: # <2>
|
||||||
|
slots = []
|
||||||
|
type_hints = cls_dict.get('__annotations__', {}) # <3>
|
||||||
|
for name, constructor in type_hints.items(): # <4>
|
||||||
|
field = Field(name, constructor) # <5>
|
||||||
|
cls_dict[name] = field # <6>
|
||||||
|
slots.append(field.storage_name) # <7>
|
||||||
|
|
||||||
|
cls_dict['__slots__'] = slots # <8>
|
||||||
|
|
||||||
|
return super().__new__(
|
||||||
|
meta_cls, cls_name, bases, cls_dict) # <9>
|
||||||
|
# end::CHECKED_META[]
|
||||||
|
|
||||||
|
# tag::CHECKED_CLASS[]
|
||||||
|
class Checked(metaclass=CheckedMeta):
|
||||||
|
__slots__ = () # skip CheckedMeta.__new__ processing
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _fields(cls) -> dict[str, type]:
|
||||||
|
return get_type_hints(cls)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
|
for name in self._fields():
|
||||||
|
value = kwargs.pop(name, ...)
|
||||||
|
setattr(self, name, value)
|
||||||
|
if kwargs:
|
||||||
|
self.__flag_unknown_attrs(*kwargs)
|
||||||
|
|
||||||
|
def __flag_unknown_attrs(self, *names: str) -> NoReturn:
|
||||||
|
plural = 's' if len(names) > 1 else ''
|
||||||
|
extra = ', '.join(f'{name!r}' for name in names)
|
||||||
|
cls_name = repr(self.__class__.__name__)
|
||||||
|
raise AttributeError(f'{cls_name} object has no attribute{plural} {extra}')
|
||||||
|
|
||||||
|
def _asdict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
name: getattr(self, name)
|
||||||
|
for name, attr in self.__class__.__dict__.items()
|
||||||
|
if isinstance(attr, Field)
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
kwargs = ', '.join(
|
||||||
|
f'{key}={value!r}' for key, value in self._asdict().items()
|
||||||
|
)
|
||||||
|
return f'{self.__class__.__name__}({kwargs})'
|
||||||
|
|
||||||
|
# end::CHECKED_CLASS[]
|
||||||
@@ -34,4 +34,25 @@ def test_constructor_attribute_error():
|
|||||||
with pytest.raises(AttributeError) as e:
|
with pytest.raises(AttributeError) as e:
|
||||||
felix = Cat(name='Felix', weight=3.2, age=7)
|
felix = Cat(name='Felix', weight=3.2, age=7)
|
||||||
|
|
||||||
assert str(e.value) == "'Cat' has no attribute 'age'"
|
assert str(e.value) == "'Cat' object has no attribute 'age'"
|
||||||
|
|
||||||
|
|
||||||
|
def test_assignment_attribute_error():
|
||||||
|
class Cat(Checked):
|
||||||
|
name: str
|
||||||
|
weight: float
|
||||||
|
|
||||||
|
felix = Cat(name='Felix', weight=3.2)
|
||||||
|
with pytest.raises(AttributeError) as e:
|
||||||
|
felix.color = 'tan'
|
||||||
|
|
||||||
|
assert str(e.value) == "'Cat' object has no attribute 'color'"
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_invalid_constructor():
|
||||||
|
with pytest.raises(TypeError) as e:
|
||||||
|
class Cat(Checked):
|
||||||
|
name: str
|
||||||
|
weight: None
|
||||||
|
|
||||||
|
assert str(e.value) == "'weight' type hint must be callable"
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# tag::BEGINNING[]
|
|
||||||
print('<[100]> evalsupport module start')
|
|
||||||
|
|
||||||
def deco_alpha(cls):
|
|
||||||
print('<[200]> deco_alpha')
|
|
||||||
|
|
||||||
def inner_1(self):
|
|
||||||
print('<[300]> deco_alpha:inner_1')
|
|
||||||
|
|
||||||
cls.method_y = inner_1
|
|
||||||
return cls
|
|
||||||
|
|
||||||
# end::BEGINNING[]
|
|
||||||
# tag::META_ALEPH[]
|
|
||||||
class MetaAleph(type):
|
|
||||||
print('<[400]> MetaAleph body')
|
|
||||||
|
|
||||||
def __init__(cls, name, bases, dic):
|
|
||||||
print('<[500]> MetaAleph.__init__')
|
|
||||||
|
|
||||||
def inner_2(self):
|
|
||||||
print('<[600]> MetaAleph.__init__:inner_2')
|
|
||||||
|
|
||||||
cls.method_z = inner_2
|
|
||||||
|
|
||||||
# end::META_ALEPH[]
|
|
||||||
# tag::END[]
|
|
||||||
print('<[700]> evalsupport module end')
|
|
||||||
# end::END[]
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
from evalsupport import deco_alpha
|
|
||||||
|
|
||||||
print('<[1]> evaltime module start')
|
|
||||||
|
|
||||||
|
|
||||||
class ClassOne():
|
|
||||||
print('<[2]> ClassOne body')
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
print('<[3]> ClassOne.__init__')
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
print('<[4]> ClassOne.__del__')
|
|
||||||
|
|
||||||
def method_x(self):
|
|
||||||
print('<[5]> ClassOne.method_x')
|
|
||||||
|
|
||||||
class ClassTwo(object):
|
|
||||||
print('<[6]> ClassTwo body')
|
|
||||||
|
|
||||||
|
|
||||||
@deco_alpha
|
|
||||||
class ClassThree():
|
|
||||||
print('<[7]> ClassThree body')
|
|
||||||
|
|
||||||
def method_y(self):
|
|
||||||
print('<[8]> ClassThree.method_y')
|
|
||||||
|
|
||||||
|
|
||||||
class ClassFour(ClassThree):
|
|
||||||
print('<[9]> ClassFour body')
|
|
||||||
|
|
||||||
def method_y(self):
|
|
||||||
print('<[10]> ClassFour.method_y')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print('<[11]> ClassOne tests', 30 * '.')
|
|
||||||
one = ClassOne()
|
|
||||||
one.method_x()
|
|
||||||
print('<[12]> ClassThree tests', 30 * '.')
|
|
||||||
three = ClassThree()
|
|
||||||
three.method_y()
|
|
||||||
print('<[13]> ClassFour tests', 30 * '.')
|
|
||||||
four = ClassFour()
|
|
||||||
four.method_y()
|
|
||||||
|
|
||||||
|
|
||||||
print('<[14]> evaltime module end')
|
|
||||||
50
25-class-metaprog/evaltime/builderlib.py
Normal file
50
25-class-metaprog/evaltime/builderlib.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# tag::BUILDERLIB_TOP[]
|
||||||
|
print('@ builderlib module start')
|
||||||
|
|
||||||
|
class Builder: # <1>
|
||||||
|
print('@ Builder body')
|
||||||
|
|
||||||
|
def __init_subclass__(cls): # <2>
|
||||||
|
print(f'@ Builder.__init_subclass__({cls!r})')
|
||||||
|
|
||||||
|
def inner_0(self): # <3>
|
||||||
|
print(f'@ SuperA.__init_subclass__:inner_0({self!r})')
|
||||||
|
|
||||||
|
cls.method_a = inner_0
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
print(f'@ Builder.__init__({self!r})')
|
||||||
|
|
||||||
|
|
||||||
|
def deco(cls): # <4>
|
||||||
|
print(f'@ deco({cls!r})')
|
||||||
|
|
||||||
|
def inner_1(self): # <5>
|
||||||
|
print(f'@ deco:inner_1({self!r})')
|
||||||
|
|
||||||
|
cls.method_b = inner_1
|
||||||
|
return cls # <6>
|
||||||
|
# end::BUILDERLIB_TOP[]
|
||||||
|
|
||||||
|
# tag::BUILDERLIB_BOTTOM[]
|
||||||
|
class Descriptor: # <1>
|
||||||
|
print('@ Descriptor body')
|
||||||
|
|
||||||
|
def __init__(self): # <2>
|
||||||
|
print(f'@ Descriptor.__init__({self!r})')
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name): # <3>
|
||||||
|
args = (self, owner, name)
|
||||||
|
print(f'@ Descriptor.__set_name__{args!r}')
|
||||||
|
|
||||||
|
def __set__(self, instance, value): # <4>
|
||||||
|
args = (self, instance, value)
|
||||||
|
print(f'@ Descriptor.__set__{args!r}')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<Descriptor instance>'
|
||||||
|
|
||||||
|
|
||||||
|
print('@ builderlib module end')
|
||||||
|
# end::BUILDERLIB_BOTTOM[]
|
||||||
30
25-class-metaprog/evaltime/evaldemo.py
Executable file
30
25-class-metaprog/evaltime/evaldemo.py
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from builderlib import Builder, deco, Descriptor
|
||||||
|
|
||||||
|
print('# evaldemo module start')
|
||||||
|
|
||||||
|
@deco # <1>
|
||||||
|
class Klass(Builder): # <2>
|
||||||
|
print('# Klass body')
|
||||||
|
|
||||||
|
attr = Descriptor() # <3>
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
print(f'# Klass.__init__({self!r})')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<Klass instance>'
|
||||||
|
|
||||||
|
|
||||||
|
def main(): # <4>
|
||||||
|
obj = Klass()
|
||||||
|
obj.method_a()
|
||||||
|
obj.method_b()
|
||||||
|
obj.attr = 999
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
||||||
|
print('# evaldemo module end')
|
||||||
33
25-class-metaprog/evaltime/evaldemo_meta.py
Executable file
33
25-class-metaprog/evaltime/evaldemo_meta.py
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from builderlib import Builder, deco, Descriptor
|
||||||
|
from metalib import MetaKlass # <1>
|
||||||
|
|
||||||
|
print('# evaldemo_meta module start')
|
||||||
|
|
||||||
|
@deco
|
||||||
|
class Klass(Builder, metaclass=MetaKlass): # <2>
|
||||||
|
print('# Klass body')
|
||||||
|
|
||||||
|
attr = Descriptor()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
print(f'# Klass.__init__({self!r})')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<Klass instance>'
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
obj = Klass()
|
||||||
|
obj.method_a()
|
||||||
|
obj.method_b()
|
||||||
|
obj.method_c() # <3>
|
||||||
|
obj.attr = 999
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
||||||
|
print('# evaldemo_meta module end')
|
||||||
43
25-class-metaprog/evaltime/metalib.py
Normal file
43
25-class-metaprog/evaltime/metalib.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# tag::METALIB_TOP[]
|
||||||
|
print('% metalib module start')
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
|
class NosyDict(collections.UserDict):
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
args = (self, key, value)
|
||||||
|
print(f'% NosyDict.__setitem__{args!r}')
|
||||||
|
super().__setitem__(key, value)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<NosyDict instance>'
|
||||||
|
# end::METALIB_TOP[]
|
||||||
|
|
||||||
|
# tag::METALIB_BOTTOM[]
|
||||||
|
class MetaKlass(type):
|
||||||
|
print('% MetaKlass body')
|
||||||
|
|
||||||
|
@classmethod # <1>
|
||||||
|
def __prepare__(meta_cls, cls_name, bases): # <2>
|
||||||
|
args = (meta_cls, cls_name, bases)
|
||||||
|
print(f'% MetaKlass.__prepare__{args!r}')
|
||||||
|
return NosyDict() # <3>
|
||||||
|
|
||||||
|
def __new__(meta_cls, cls_name, bases, cls_dict): # <4>
|
||||||
|
args = (meta_cls, cls_name, bases, cls_dict)
|
||||||
|
print(f'% MetaKlass.__new__{args!r}')
|
||||||
|
def inner_2(self):
|
||||||
|
print(f'% MetaKlass.__new__:inner_2({self!r})')
|
||||||
|
|
||||||
|
cls = super().__new__(meta_cls, cls_name, bases, cls_dict.data) # <5>
|
||||||
|
|
||||||
|
cls.method_c = inner_2 # <6>
|
||||||
|
|
||||||
|
return cls # <7>
|
||||||
|
|
||||||
|
def __repr__(cls): # <8>
|
||||||
|
cls_name = cls.__name__
|
||||||
|
return f"<class {cls_name!r} built by MetaKlass>"
|
||||||
|
|
||||||
|
print('% metalib module end')
|
||||||
|
# end::METALIB_BOTTOM[]
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
from evalsupport import deco_alpha
|
|
||||||
from evalsupport import MetaAleph
|
|
||||||
|
|
||||||
print('<[1]> evaltime_meta module start')
|
|
||||||
|
|
||||||
|
|
||||||
@deco_alpha
|
|
||||||
class ClassThree():
|
|
||||||
print('<[2]> ClassThree body')
|
|
||||||
|
|
||||||
def method_y(self):
|
|
||||||
print('<[3]> ClassThree.method_y')
|
|
||||||
|
|
||||||
|
|
||||||
class ClassFour(ClassThree):
|
|
||||||
print('<[4]> ClassFour body')
|
|
||||||
|
|
||||||
def method_y(self):
|
|
||||||
print('<[5]> ClassFour.method_y')
|
|
||||||
|
|
||||||
|
|
||||||
class ClassFive(metaclass=MetaAleph):
|
|
||||||
print('<[6]> ClassFive body')
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
print('<[7]> ClassFive.__init__')
|
|
||||||
|
|
||||||
def method_z(self):
|
|
||||||
print('<[8]> ClassFive.method_z')
|
|
||||||
|
|
||||||
|
|
||||||
class ClassSix(ClassFive):
|
|
||||||
print('<[9]> ClassSix body')
|
|
||||||
|
|
||||||
def method_z(self):
|
|
||||||
print('<[10]> ClassSix.method_z')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print('<[11]> ClassThree tests', 30 * '.')
|
|
||||||
three = ClassThree()
|
|
||||||
three.method_y()
|
|
||||||
print('<[12]> ClassFour tests', 30 * '.')
|
|
||||||
four = ClassFour()
|
|
||||||
four.method_y()
|
|
||||||
print('<[13]> ClassFive tests', 30 * '.')
|
|
||||||
five = ClassFive()
|
|
||||||
five.method_z()
|
|
||||||
print('<[14]> ClassSix tests', 30 * '.')
|
|
||||||
six = ClassSix()
|
|
||||||
six.method_z()
|
|
||||||
|
|
||||||
print('<[15]> evaltime_meta module end')
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user