updade from Atlas repo
This commit is contained in:
@@ -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+')
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ with open(sys.argv[1], encoding='utf-8') as fp:
|
|||||||
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+')
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ with open(sys.argv[1], encoding='utf-8') as fp:
|
|||||||
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():
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import sys, locale
|
import locale
|
||||||
|
import sys
|
||||||
|
|
||||||
expressions = """
|
expressions = """
|
||||||
locale.getpreferredencoding()
|
locale.getpreferredencoding()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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():
|
||||||
@@ -99,3 +97,4 @@ def test_4_levels_4_leaves():
|
|||||||
|
|
||||||
result = list(render_lines(tree(A)))
|
result = list(render_lines(tree(A)))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|||||||
@@ -70,3 +70,4 @@ def test_4_levels_3_leaves():
|
|||||||
|
|
||||||
result = list(tree(A))
|
result = list(tree(A))
|
||||||
assert expected == result
|
assert expected == result
|
||||||
|
|
||||||
|
|||||||
@@ -88,3 +88,4 @@ def test_many_levels_1_leaf():
|
|||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -88,3 +88,4 @@ def test_many_levels_1_leaf():
|
|||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +126,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,8 +1,9 @@
|
|||||||
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
|
||||||
|
|||||||
@@ -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[]
|
||||||
|
|||||||
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')
|
|
||||||
@@ -27,35 +27,48 @@ The factory also accepts a list or tuple of identifiers:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# tag::RECORD_FACTORY[]
|
|
||||||
def record_factory(cls_name, field_names):
|
|
||||||
try:
|
|
||||||
field_names = field_names.replace(',', ' ').split() # <1>
|
|
||||||
except AttributeError: # no .replace or .split
|
|
||||||
pass # assume it's already a sequence of strings
|
|
||||||
field_names = tuple(field_names) # <2>
|
|
||||||
if not all(s.isidentifier() for s in field_names):
|
|
||||||
raise ValueError('field_names must all be valid identifiers')
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs): # <3>
|
# tag::RECORD_FACTORY[]
|
||||||
|
from typing import Union, Any
|
||||||
|
from collections.abc import Iterable, Iterator
|
||||||
|
|
||||||
|
FieldNames = Union[str, Iterable[str]] # <1>
|
||||||
|
|
||||||
|
def record_factory(cls_name: str, field_names: FieldNames) -> type[tuple]: # <2>
|
||||||
|
|
||||||
|
slots = parse_identifiers(field_names) # <3>
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None: # <4>
|
||||||
attrs = dict(zip(self.__slots__, args))
|
attrs = dict(zip(self.__slots__, args))
|
||||||
attrs.update(kwargs)
|
attrs.update(kwargs)
|
||||||
for name, value in attrs.items():
|
for name, value in attrs.items():
|
||||||
setattr(self, name, value)
|
setattr(self, name, value)
|
||||||
|
|
||||||
def __iter__(self): # <4>
|
def __iter__(self) -> Iterator[Any]: # <5>
|
||||||
for name in self.__slots__:
|
for name in self.__slots__:
|
||||||
yield getattr(self, name)
|
yield getattr(self, name)
|
||||||
|
|
||||||
def __repr__(self): # <5>
|
def __repr__(self): # <6>
|
||||||
values = ', '.join('{}={!r}'.format(*i) for i
|
values = ', '.join(
|
||||||
in zip(self.__slots__, self))
|
'{}={!r}'.format(*i) for i in zip(self.__slots__, self)
|
||||||
return '{}({})'.format(self.__class__.__name__, values)
|
)
|
||||||
|
cls_name = self.__class__.__name__
|
||||||
|
return f'{cls_name}({values})'
|
||||||
|
|
||||||
cls_attrs = dict(__slots__ = field_names, # <6>
|
cls_attrs = dict( # <7>
|
||||||
|
__slots__=slots,
|
||||||
__init__=__init__,
|
__init__=__init__,
|
||||||
__iter__=__iter__,
|
__iter__=__iter__,
|
||||||
__repr__ = __repr__)
|
__repr__=__repr__,
|
||||||
|
)
|
||||||
|
|
||||||
return type(cls_name, (object,), cls_attrs) # <7>
|
return type(cls_name, (object,), cls_attrs) # <8>
|
||||||
|
|
||||||
|
|
||||||
|
def parse_identifiers(names: FieldNames) -> tuple[str, ...]:
|
||||||
|
if isinstance(names, str):
|
||||||
|
names = names.replace(',', ' ').split() # <9>
|
||||||
|
if not all(s.isidentifier() for s in names):
|
||||||
|
raise ValueError('names must all be valid identifiers')
|
||||||
|
return tuple(names)
|
||||||
# end::RECORD_FACTORY[]
|
# end::RECORD_FACTORY[]
|
||||||
|
|||||||
79
25-class-metaprog/factories_ducktyped.py
Normal file
79
25-class-metaprog/factories_ducktyped.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
record_factory: create simple classes just for holding data fields
|
||||||
|
|
||||||
|
# tag::RECORD_FACTORY_DEMO[]
|
||||||
|
>>> Dog = record_factory('Dog', 'name weight owner') # <1>
|
||||||
|
>>> rex = Dog('Rex', 30, 'Bob')
|
||||||
|
>>> rex # <2>
|
||||||
|
Dog(name='Rex', weight=30, owner='Bob')
|
||||||
|
>>> name, weight, _ = rex # <3>
|
||||||
|
>>> name, weight
|
||||||
|
('Rex', 30)
|
||||||
|
>>> "{2}'s dog weighs {1}kg".format(*rex) # <4>
|
||||||
|
"Bob's dog weighs 30kg"
|
||||||
|
>>> rex.weight = 32 # <5>
|
||||||
|
>>> rex
|
||||||
|
Dog(name='Rex', weight=32, owner='Bob')
|
||||||
|
>>> Dog.__mro__ # <6>
|
||||||
|
(<class 'factories_ducktyped.Dog'>, <class 'object'>)
|
||||||
|
|
||||||
|
# end::RECORD_FACTORY_DEMO[]
|
||||||
|
|
||||||
|
The factory also accepts a list or tuple of identifiers:
|
||||||
|
|
||||||
|
>>> Dog = record_factory('Dog', ['name', 'weight', 'owner'])
|
||||||
|
>>> Dog.__slots__
|
||||||
|
('name', 'weight', 'owner')
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# tag::RECORD_FACTORY[]
|
||||||
|
from typing import Union, Any
|
||||||
|
from collections.abc import Sequence, Iterator
|
||||||
|
|
||||||
|
FieldNames = Union[str, Sequence[str]]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_identifiers(names):
|
||||||
|
try:
|
||||||
|
names = names.replace(',', ' ').split() # <1>
|
||||||
|
except AttributeError: # no .replace or .split
|
||||||
|
pass # assume it's already a sequence of strings
|
||||||
|
if not all(s.isidentifier() for s in names):
|
||||||
|
raise ValueError('names must all be valid identifiers')
|
||||||
|
return tuple(names)
|
||||||
|
|
||||||
|
|
||||||
|
def record_factory(cls_name, field_names):
|
||||||
|
|
||||||
|
field_identifiers = parse_identifiers(field_names)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None: # <4>
|
||||||
|
attrs = dict(zip(self.__slots__, args))
|
||||||
|
attrs.update(kwargs)
|
||||||
|
for name, value in attrs.items():
|
||||||
|
setattr(self, name, value)
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[Any]: # <5>
|
||||||
|
for name in self.__slots__:
|
||||||
|
yield getattr(self, name)
|
||||||
|
|
||||||
|
def __repr__(self): # <6>
|
||||||
|
values = ', '.join(
|
||||||
|
'{}={!r}'.format(*i) for i in zip(self.__slots__, self)
|
||||||
|
)
|
||||||
|
cls_name = self.__class__.__name__
|
||||||
|
return f'{cls_name}({values})'
|
||||||
|
|
||||||
|
cls_attrs = dict(
|
||||||
|
__slots__=field_identifiers, # <7>
|
||||||
|
__init__=__init__,
|
||||||
|
__iter__=__iter__,
|
||||||
|
__repr__=__repr__,
|
||||||
|
)
|
||||||
|
|
||||||
|
return type(cls_name, (object,), cls_attrs) # <8>
|
||||||
|
|
||||||
|
|
||||||
|
# end::RECORD_FACTORY[]
|
||||||
115
25-class-metaprog/hours/hours.py
Normal file
115
25-class-metaprog/hours/hours.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"""
|
||||||
|
Abusing ``__class_getitem__`` to make a nano-DSL for working
|
||||||
|
with hours, minutes, and seconds--these last two in base 60.
|
||||||
|
|
||||||
|
``H`` is an alias for the ``Hours`` class::
|
||||||
|
|
||||||
|
>>> H[1]
|
||||||
|
1:00
|
||||||
|
>>> H[1:30]
|
||||||
|
1:30
|
||||||
|
>>> H[1::5]
|
||||||
|
1:00:05
|
||||||
|
>>> H[::5]
|
||||||
|
0:00:05
|
||||||
|
|
||||||
|
An ``H`` instance can be converted to a float number of hours::
|
||||||
|
|
||||||
|
>>> float(H[1:15])
|
||||||
|
1.25
|
||||||
|
>>> float(H[1:30:30]) # doctest: +ELLIPSIS
|
||||||
|
1.5083333...
|
||||||
|
>>> float(H[1::5]) # doctest: +ELLIPSIS
|
||||||
|
1.0013888...
|
||||||
|
|
||||||
|
The ``H`` constructor accepts hours, minutes, and/or seconds::
|
||||||
|
|
||||||
|
>>> H(1.5)
|
||||||
|
1:30
|
||||||
|
>>> H(1.9)
|
||||||
|
1:54
|
||||||
|
>>> H(1, 30, 30)
|
||||||
|
1:30:30
|
||||||
|
>>> H(s = 7205)
|
||||||
|
2:00:05
|
||||||
|
>>> H(1/3)
|
||||||
|
0:20
|
||||||
|
>>> H(1/1000)
|
||||||
|
0:00:03.6
|
||||||
|
|
||||||
|
An ``H`` instance is iterable, for convenient unpacking::
|
||||||
|
|
||||||
|
>>> hms = H[1:22:33]
|
||||||
|
>>> h, m, s = hms
|
||||||
|
>>> h, m, s
|
||||||
|
(1, 22, 33)
|
||||||
|
>>> tuple(hms)
|
||||||
|
(1, 22, 33)
|
||||||
|
|
||||||
|
|
||||||
|
``H`` instances can be added::
|
||||||
|
|
||||||
|
>>> H[1:45:12] + H[2:15:50]
|
||||||
|
4:01:02
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Tuple, Union
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(s: float) -> Tuple[int, int, float]:
|
||||||
|
h, r = divmod(s, 3600)
|
||||||
|
m, s = divmod(r, 60)
|
||||||
|
return int(h), int(m), s
|
||||||
|
|
||||||
|
|
||||||
|
def valid_base_60(n, unit):
|
||||||
|
if not (0 <= n < 60):
|
||||||
|
raise ValueError(f'invalid {unit} {n}')
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
class Hours:
|
||||||
|
h: int
|
||||||
|
_m: int
|
||||||
|
_s: float
|
||||||
|
|
||||||
|
def __class_getitem__(cls, parts: Union[slice, float]) -> 'Hours':
|
||||||
|
if isinstance(parts, slice):
|
||||||
|
h = parts.start or 0
|
||||||
|
m = valid_base_60(parts.stop or 0, 'minutes')
|
||||||
|
s = valid_base_60(parts.step or 0, 'seconds')
|
||||||
|
else:
|
||||||
|
h, m, s = normalize(parts * 3600)
|
||||||
|
return Hours(h, m, s)
|
||||||
|
|
||||||
|
def __init__(self, h: float = 0, m: float = 0, s: float = 0):
|
||||||
|
if h < 0 or m < 0 or s < 0:
|
||||||
|
raise ValueError('invalid negative argument')
|
||||||
|
self.h, self.m, self.s = normalize(h * 3600 + m * 60 + s)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
h, m, s = self
|
||||||
|
display_s = f'{s:06.3f}'
|
||||||
|
display_s = display_s.rstrip('0').rstrip('.')
|
||||||
|
if display_s == '00':
|
||||||
|
return f'{h}:{m:02d}'
|
||||||
|
return f'{h}:{m:02d}:{display_s}'
|
||||||
|
|
||||||
|
def __float__(self):
|
||||||
|
return self.h + self.m / 60 + self.s / 3600
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return repr(self) == repr(other)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield self.h
|
||||||
|
yield self.m
|
||||||
|
yield self.s
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
if not isinstance(other, Hours):
|
||||||
|
return NotImplemented
|
||||||
|
return Hours(*(a + b for a, b in zip(self, other)))
|
||||||
|
|
||||||
|
|
||||||
|
H = Hours
|
||||||
71
25-class-metaprog/hours/hours_test.py
Normal file
71
25-class-metaprog/hours/hours_test.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# content of test_expectation.py
|
||||||
|
from math import isclose
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from hours import normalize, H
|
||||||
|
|
||||||
|
HOURS_TO_HMS = [
|
||||||
|
[1, (1, 0, 0.0)],
|
||||||
|
[1.5, (1, 30, 0.0)],
|
||||||
|
[1.1, (1, 6, 0.0)],
|
||||||
|
[1.9, (1, 54, 0.0)],
|
||||||
|
[1.01, (1, 0, 36.0)],
|
||||||
|
[1.09, (1, 5, 24.0)],
|
||||||
|
[2 + 1/60, (2, 1, 0.0)],
|
||||||
|
[3 + 1/3600, (3, 0, 1.0)],
|
||||||
|
[1.251, (1, 15, 3.6)],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('hours, expected', HOURS_TO_HMS)
|
||||||
|
def test_normalize(hours, expected):
|
||||||
|
h, m, s = expected
|
||||||
|
got_h, got_m, got_s = normalize(hours * 3600)
|
||||||
|
assert (h, m) == (got_h, got_m)
|
||||||
|
assert isclose(s, got_s, abs_tol=1e-12)
|
||||||
|
got_hours = got_h + got_m / 60 + got_s / 3600
|
||||||
|
assert isclose(hours, got_hours)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('h, expected', [
|
||||||
|
(H[1], '1:00'),
|
||||||
|
(H[1:0], '1:00'),
|
||||||
|
(H[1:3], '1:03'),
|
||||||
|
(H[1:59], '1:59'),
|
||||||
|
(H[1:0:0], '1:00'),
|
||||||
|
(H[1:2:3], '1:02:03'),
|
||||||
|
(H[1:2:3.4], '1:02:03.4'),
|
||||||
|
(H[1:2:0.1], '1:02:00.1'),
|
||||||
|
(H[1:2:0.01], '1:02:00.01'),
|
||||||
|
(H[1:2:0.001], '1:02:00.001'),
|
||||||
|
(H[1:2:0.0001], '1:02'),
|
||||||
|
])
|
||||||
|
def test_repr(h, expected):
|
||||||
|
assert expected == repr(h), f'seconds: {h.s}'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('expected, hms', HOURS_TO_HMS)
|
||||||
|
def test_float(expected, hms):
|
||||||
|
got = float(H[slice(*hms)])
|
||||||
|
assert isclose(expected, got)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('hms, units', [
|
||||||
|
((0, 60, 0), 'minutes'),
|
||||||
|
((0, 0, 60), 'seconds'),
|
||||||
|
((0, 60, 60), 'minutes'),
|
||||||
|
])
|
||||||
|
def test_class_getitem_errors(hms, units):
|
||||||
|
with pytest.raises(ValueError) as excinfo:
|
||||||
|
H[slice(*hms)]
|
||||||
|
assert units in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('hms1, hms2, expected', [
|
||||||
|
(H[0:30], H[0:15], H[0:45]),
|
||||||
|
(H[0:30], H[0:30], H[1:00]),
|
||||||
|
(H[0:59:59], H[0:00:1], H[1:00]),
|
||||||
|
])
|
||||||
|
def test_add(hms1, hms2, expected):
|
||||||
|
assert expected == hms1 + hms2
|
||||||
20
25-class-metaprog/metabunch/README.md
Normal file
20
25-class-metaprog/metabunch/README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Examples from Python in a Nutshell, 3rd edition
|
||||||
|
|
||||||
|
The metaclass `MetaBunch` example in `original/bunch.py` is an exact copy of the
|
||||||
|
last example in the _How a Metaclass Creates a Class_ section of
|
||||||
|
_Chapter 4: Object Oriented Python_ from
|
||||||
|
[_Python in a Nutshell, 3rd edition_](https://learning.oreilly.com/library/view/python-in-a/9781491913833)
|
||||||
|
by Alex Martelli, Anna Ravenscroft, and Steve Holden.
|
||||||
|
|
||||||
|
The version in `pre3.6/bunch.py` is slightly simplified by taking advantage
|
||||||
|
of Python 3 `super()` and removing comments and docstrings,
|
||||||
|
to make it easier to compare to the `from3.6` version.
|
||||||
|
|
||||||
|
The version in `from3.6/bunch.py` is further simplified by taking advantage
|
||||||
|
of the order-preserving `dict` that appeared in Python 3.6,
|
||||||
|
as well as other simplifications,
|
||||||
|
such as leveraging closures in `__init__` and `__repr__`
|
||||||
|
to avoid adding a `__defaults__` mapping to the class.
|
||||||
|
|
||||||
|
The external behavior of all three versions is the same, and
|
||||||
|
the test files `bunch_test.py` are identical in the three directories.
|
||||||
77
25-class-metaprog/metabunch/from3.6/bunch.py
Normal file
77
25-class-metaprog/metabunch/from3.6/bunch.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
"""
|
||||||
|
The `MetaBunch` metaclass is a simplified version of the
|
||||||
|
last example in the _How a Metaclass Creates a Class_ section
|
||||||
|
of _Chapter 4: Object Oriented Python_ from
|
||||||
|
[_Python in a Nutshell, 3rd edition_](https://learning.oreilly.com/library/view/python-in-a/9781491913833)
|
||||||
|
by Alex Martelli, Anna Ravenscroft, and Steve Holden.
|
||||||
|
|
||||||
|
Here are a few tests. ``bunch_test.py`` has a few more.
|
||||||
|
|
||||||
|
# tag::BUNCH_POINT_DEMO_1[]
|
||||||
|
>>> class Point(Bunch):
|
||||||
|
... x = 0.0
|
||||||
|
... y = 0.0
|
||||||
|
... color = 'gray'
|
||||||
|
...
|
||||||
|
>>> Point(x=1.2, y=3, color='green')
|
||||||
|
Point(x=1.2, y=3, color='green')
|
||||||
|
>>> p = Point()
|
||||||
|
>>> p.x, p.y, p.color
|
||||||
|
(0.0, 0.0, 'gray')
|
||||||
|
>>> p
|
||||||
|
Point()
|
||||||
|
|
||||||
|
# end::BUNCH_POINT_DEMO_1[]
|
||||||
|
|
||||||
|
# tag::BUNCH_POINT_DEMO_2[]
|
||||||
|
|
||||||
|
>>> Point(x=1, y=2, z=3)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Point' object has no attribute 'z'
|
||||||
|
>>> p = Point(x=21)
|
||||||
|
>>> p.y = 42
|
||||||
|
>>> p
|
||||||
|
Point(x=21, y=42)
|
||||||
|
>>> p.flavor = 'banana'
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: 'Point' object has no attribute 'flavor'
|
||||||
|
|
||||||
|
# end::BUNCH_POINT_DEMO_2[]
|
||||||
|
"""
|
||||||
|
|
||||||
|
# tag::METABUNCH[]
|
||||||
|
class MetaBunch(type): # <1>
|
||||||
|
def __new__(meta_cls, cls_name, bases, cls_dict): # <2>
|
||||||
|
|
||||||
|
defaults = {} # <3>
|
||||||
|
|
||||||
|
def __init__(self, **kwargs): # <4>
|
||||||
|
for name, default in defaults.items(): # <5>
|
||||||
|
setattr(self, name, kwargs.pop(name, default))
|
||||||
|
if kwargs: # <6>
|
||||||
|
setattr(self, *kwargs.popitem())
|
||||||
|
|
||||||
|
def __repr__(self): # <7>
|
||||||
|
rep = ', '.join(f'{name}={value!r}'
|
||||||
|
for name, default in defaults.items()
|
||||||
|
if (value := getattr(self, name)) != default)
|
||||||
|
return f'{cls_name}({rep})'
|
||||||
|
|
||||||
|
new_dict = dict(__slots__=[], __init__=__init__, __repr__=__repr__) # <8>
|
||||||
|
|
||||||
|
for name, value in cls_dict.items(): # <9>
|
||||||
|
if name.startswith('__') and name.endswith('__'): # <10>
|
||||||
|
if name in new_dict:
|
||||||
|
raise AttributeError(f"Can't set {name!r} in {cls_name!r}")
|
||||||
|
new_dict[name] = value
|
||||||
|
else: # <11>
|
||||||
|
new_dict['__slots__'].append(name)
|
||||||
|
defaults[name] = value
|
||||||
|
return super().__new__(meta_cls, cls_name, bases, new_dict) # <12>
|
||||||
|
|
||||||
|
|
||||||
|
class Bunch(metaclass=MetaBunch): # <13>
|
||||||
|
pass
|
||||||
|
# end::METABUNCH[]
|
||||||
59
25-class-metaprog/metabunch/from3.6/bunch_test.py
Normal file
59
25-class-metaprog/metabunch/from3.6/bunch_test.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from bunch import Bunch
|
||||||
|
|
||||||
|
class Point(Bunch):
|
||||||
|
""" A point has x and y coordinates, defaulting to 0.0,
|
||||||
|
and a color, defaulting to 'gray'—and nothing more,
|
||||||
|
except what Python and the metaclass conspire to add,
|
||||||
|
such as __init__ and __repr__
|
||||||
|
"""
|
||||||
|
x = 0.0
|
||||||
|
y = 0.0
|
||||||
|
color = 'gray'
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_defaults():
|
||||||
|
p = Point()
|
||||||
|
assert repr(p) == 'Point()'
|
||||||
|
|
||||||
|
|
||||||
|
def test_init():
|
||||||
|
p = Point(x=1.2, y=3.4, color='red')
|
||||||
|
assert repr(p) == "Point(x=1.2, y=3.4, color='red')"
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_wrong_argument():
|
||||||
|
with pytest.raises(AttributeError) as exc:
|
||||||
|
p = Point(x=1.2, y=3.4, flavor='coffee')
|
||||||
|
assert "no attribute 'flavor'" in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_slots():
|
||||||
|
p = Point()
|
||||||
|
with pytest.raises(AttributeError) as exc:
|
||||||
|
p.z = 5.6
|
||||||
|
assert "no attribute 'z'" in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dunder_permitted():
|
||||||
|
class Cat(Bunch):
|
||||||
|
name = ''
|
||||||
|
weight = 0
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.name} ({self.weight} kg)'
|
||||||
|
|
||||||
|
cheshire = Cat(name='Cheshire')
|
||||||
|
assert str(cheshire) == 'Cheshire (0 kg)'
|
||||||
|
|
||||||
|
|
||||||
|
def test_dunder_forbidden():
|
||||||
|
with pytest.raises(AttributeError) as exc:
|
||||||
|
class Cat(Bunch):
|
||||||
|
name = ''
|
||||||
|
weight = 0
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
assert "Can't set '__init__' in 'Cat'" in str(exc.value)
|
||||||
85
25-class-metaprog/metabunch/nutshell3e/bunch.py
Normal file
85
25-class-metaprog/metabunch/nutshell3e/bunch.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import collections
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
class MetaBunch(type):
|
||||||
|
"""
|
||||||
|
Metaclass for new and improved "Bunch": implicitly defines
|
||||||
|
__slots__, __init__ and __repr__ from variables bound in
|
||||||
|
class scope.
|
||||||
|
A class statement for an instance of MetaBunch (i.e., for a
|
||||||
|
class whose metaclass is MetaBunch) must define only
|
||||||
|
class-scope data attributes (and possibly special methods, but
|
||||||
|
NOT __init__ and __repr__). MetaBunch removes the data
|
||||||
|
attributes from class scope, snuggles them instead as items in
|
||||||
|
a class-scope dict named __dflts__, and puts in the class a
|
||||||
|
__slots__ with those attributes' names, an __init__ that takes
|
||||||
|
as optional named arguments each of them (using the values in
|
||||||
|
__dflts__ as defaults for missing ones), and a __repr__ that
|
||||||
|
shows the repr of each attribute that differs from its default
|
||||||
|
value (the output of __repr__ can be passed to __eval__ to make
|
||||||
|
an equal instance, as per usual convention in the matter, if
|
||||||
|
each non-default-valued attribute respects the convention too).
|
||||||
|
|
||||||
|
In v3, the order of data attributes remains the same as in the
|
||||||
|
class body; in v2, there is no such guarantee.
|
||||||
|
"""
|
||||||
|
def __prepare__(name, *bases, **kwargs):
|
||||||
|
# precious in v3—harmless although useless in v2
|
||||||
|
return collections.OrderedDict()
|
||||||
|
|
||||||
|
def __new__(mcl, classname, bases, classdict):
|
||||||
|
""" Everything needs to be done in __new__, since
|
||||||
|
type.__new__ is where __slots__ are taken into account.
|
||||||
|
"""
|
||||||
|
# define as local functions the __init__ and __repr__ that
|
||||||
|
# we'll use in the new class
|
||||||
|
def __init__(self, **kw):
|
||||||
|
""" Simplistic __init__: first set all attributes to
|
||||||
|
default values, then override those explicitly
|
||||||
|
passed in kw.
|
||||||
|
"""
|
||||||
|
for k in self.__dflts__:
|
||||||
|
setattr(self, k, self.__dflts__[k])
|
||||||
|
for k in kw:
|
||||||
|
setattr(self, k, kw[k])
|
||||||
|
def __repr__(self):
|
||||||
|
""" Clever __repr__: show only attributes that differ
|
||||||
|
from default values, for compactness.
|
||||||
|
"""
|
||||||
|
rep = ['{}={!r}'.format(k, getattr(self, k))
|
||||||
|
for k in self.__dflts__
|
||||||
|
if getattr(self, k) != self.__dflts__[k]
|
||||||
|
]
|
||||||
|
return '{}({})'.format(classname, ', '.join(rep))
|
||||||
|
# build the newdict that we'll use as class-dict for the
|
||||||
|
# new class
|
||||||
|
newdict = { '__slots__':[],
|
||||||
|
'__dflts__':collections.OrderedDict(),
|
||||||
|
'__init__':__init__, '__repr__':__repr__, }
|
||||||
|
for k in classdict:
|
||||||
|
if k.startswith('__') and k.endswith('__'):
|
||||||
|
# dunder methods: copy to newdict, or warn
|
||||||
|
# about conflicts
|
||||||
|
if k in newdict:
|
||||||
|
warnings.warn(
|
||||||
|
"Can't set attr {!r} in bunch-class {!r}".
|
||||||
|
format(k, classname))
|
||||||
|
else:
|
||||||
|
newdict[k] = classdict[k]
|
||||||
|
else:
|
||||||
|
# class variables, store name in __slots__, and
|
||||||
|
# name and value as an item in __dflts__
|
||||||
|
newdict['__slots__'].append(k)
|
||||||
|
newdict['__dflts__'][k] = classdict[k]
|
||||||
|
# finally delegate the rest of the work to type.__new__
|
||||||
|
return super(MetaBunch, mcl).__new__(
|
||||||
|
mcl, classname, bases, newdict)
|
||||||
|
|
||||||
|
class Bunch(metaclass=MetaBunch):
|
||||||
|
""" For convenience: inheriting from Bunch can be used to get
|
||||||
|
the new metaclass (same as defining metaclass= yourself).
|
||||||
|
|
||||||
|
In v2, remove the (metaclass=MetaBunch) above and add
|
||||||
|
instead __metaclass__=MetaBunch as the class body.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
38
25-class-metaprog/metabunch/nutshell3e/bunch_test.py
Normal file
38
25-class-metaprog/metabunch/nutshell3e/bunch_test.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from bunch import Bunch
|
||||||
|
|
||||||
|
class Point(Bunch):
|
||||||
|
""" A point has x and y coordinates, defaulting to 0.0,
|
||||||
|
and a color, defaulting to 'gray'—and nothing more,
|
||||||
|
except what Python and the metaclass conspire to add,
|
||||||
|
such as __init__ and __repr__
|
||||||
|
"""
|
||||||
|
x = 0.0
|
||||||
|
y = 0.0
|
||||||
|
color = 'gray'
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_defaults():
|
||||||
|
p = Point()
|
||||||
|
assert repr(p) == 'Point()'
|
||||||
|
|
||||||
|
|
||||||
|
def test_init():
|
||||||
|
p = Point(x=1.2, y=3.4, color='red')
|
||||||
|
assert repr(p) == "Point(x=1.2, y=3.4, color='red')"
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_wrong_argument():
|
||||||
|
with pytest.raises(AttributeError) as exc:
|
||||||
|
p = Point(x=1.2, y=3.4, flavor='coffee')
|
||||||
|
assert "no attribute 'flavor'" in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_slots():
|
||||||
|
p = Point()
|
||||||
|
with pytest.raises(AttributeError) as exc:
|
||||||
|
p.z = 5.6
|
||||||
|
assert "no attribute 'z'" in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
70
25-class-metaprog/metabunch/original/bunch.py
Normal file
70
25-class-metaprog/metabunch/original/bunch.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import warnings
|
||||||
|
|
||||||
|
class metaMetaBunch(type):
|
||||||
|
"""
|
||||||
|
metaclass for new and improved "Bunch": implicitly defines
|
||||||
|
__slots__, __init__ and __repr__ from variables bound in class scope.
|
||||||
|
|
||||||
|
An instance of metaMetaBunch (a class whose metaclass is metaMetaBunch)
|
||||||
|
defines only class-scope variables (and possibly special methods, but
|
||||||
|
NOT __init__ and __repr__!). metaMetaBunch removes those variables from
|
||||||
|
class scope, snuggles them instead as items in a class-scope dict named
|
||||||
|
__dflts__, and puts in the class a __slots__ listing those variables'
|
||||||
|
names, an __init__ that takes as optional keyword arguments each of
|
||||||
|
them (using the values in __dflts__ as defaults for missing ones), and
|
||||||
|
a __repr__ that shows the repr of each attribute that differs from its
|
||||||
|
default value (the output of __repr__ can be passed to __eval__ to make
|
||||||
|
an equal instance, as per the usual convention in the matter).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __new__(cls, classname, bases, classdict):
|
||||||
|
""" Everything needs to be done in __new__, since type.__new__ is
|
||||||
|
where __slots__ are taken into account.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# define as local functions the __init__ and __repr__ that we'll
|
||||||
|
# use in the new class
|
||||||
|
|
||||||
|
def __init__(self, **kw):
|
||||||
|
""" Simplistic __init__: first set all attributes to default
|
||||||
|
values, then override those explicitly passed in kw.
|
||||||
|
"""
|
||||||
|
for k in self.__dflts__: setattr(self, k, self.__dflts__[k])
|
||||||
|
for k in kw: setattr(self, k, kw[k])
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
""" Clever __repr__: show only attributes that differ from the
|
||||||
|
respective default values, for compactness.
|
||||||
|
"""
|
||||||
|
rep = [ '%s=%r' % (k, getattr(self, k)) for k in self.__dflts__
|
||||||
|
if getattr(self, k) != self.__dflts__[k]
|
||||||
|
]
|
||||||
|
return '%s(%s)' % (classname, ', '.join(rep))
|
||||||
|
|
||||||
|
# build the newdict that we'll use as class-dict for the new class
|
||||||
|
newdict = { '__slots__':[], '__dflts__':{},
|
||||||
|
'__init__':__init__, '__repr__':__repr__, }
|
||||||
|
|
||||||
|
for k in classdict:
|
||||||
|
if k.startswith('__'):
|
||||||
|
# special methods &c: copy to newdict, warn about conflicts
|
||||||
|
if k in newdict:
|
||||||
|
warnings.warn("Can't set attr %r in bunch-class %r" % (
|
||||||
|
k, classname))
|
||||||
|
else:
|
||||||
|
newdict[k] = classdict[k]
|
||||||
|
else:
|
||||||
|
# class variables, store name in __slots__ and name and
|
||||||
|
# value as an item in __dflts__
|
||||||
|
newdict['__slots__'].append(k)
|
||||||
|
newdict['__dflts__'][k] = classdict[k]
|
||||||
|
|
||||||
|
# finally delegate the rest of the work to type.__new__
|
||||||
|
return type.__new__(cls, classname, bases, newdict)
|
||||||
|
|
||||||
|
|
||||||
|
class MetaBunch(metaclass=metaMetaBunch):
|
||||||
|
""" For convenience: inheriting from MetaBunch can be used to get
|
||||||
|
the new metaclass (same as defining __metaclass__ yourself).
|
||||||
|
"""
|
||||||
|
__metaclass__ = metaMetaBunch
|
||||||
38
25-class-metaprog/metabunch/original/bunch_test.py
Normal file
38
25-class-metaprog/metabunch/original/bunch_test.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from bunch import MetaBunch
|
||||||
|
|
||||||
|
class Point(MetaBunch):
|
||||||
|
""" A point has x and y coordinates, defaulting to 0.0,
|
||||||
|
and a color, defaulting to 'gray'—and nothing more,
|
||||||
|
except what Python and the metaclass conspire to add,
|
||||||
|
such as __init__ and __repr__
|
||||||
|
"""
|
||||||
|
x = 0.0
|
||||||
|
y = 0.0
|
||||||
|
color = 'gray'
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_defaults():
|
||||||
|
p = Point()
|
||||||
|
assert repr(p) == 'Point()'
|
||||||
|
|
||||||
|
|
||||||
|
def test_init():
|
||||||
|
p = Point(x=1.2, y=3.4, color='red')
|
||||||
|
assert repr(p) == "Point(x=1.2, y=3.4, color='red')"
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_wrong_argument():
|
||||||
|
with pytest.raises(AttributeError) as exc:
|
||||||
|
p = Point(x=1.2, y=3.4, flavor='coffee')
|
||||||
|
assert "no attribute 'flavor'" in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_slots():
|
||||||
|
p = Point()
|
||||||
|
with pytest.raises(AttributeError) as exc:
|
||||||
|
p.z = 5.6
|
||||||
|
assert "no attribute 'z'" in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
41
25-class-metaprog/metabunch/pre3.6/bunch.py
Normal file
41
25-class-metaprog/metabunch/pre3.6/bunch.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import collections
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
class MetaBunch(type):
|
||||||
|
def __prepare__(name, *bases, **kwargs):
|
||||||
|
return collections.OrderedDict()
|
||||||
|
|
||||||
|
def __new__(meta_cls, cls_name, bases, cls_dict):
|
||||||
|
def __init__(self, **kw):
|
||||||
|
for k in self.__defaults__:
|
||||||
|
setattr(self, k, self.__defaults__[k])
|
||||||
|
for k in kw:
|
||||||
|
setattr(self, k, kw[k])
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
rep = ['{}={!r}'.format(k, getattr(self, k))
|
||||||
|
for k in self.__defaults__
|
||||||
|
if getattr(self, k) != self.__defaults__[k]
|
||||||
|
]
|
||||||
|
return '{}({})'.format(cls_name, ', '.join(rep))
|
||||||
|
|
||||||
|
new_dict = { '__slots__':[],
|
||||||
|
'__defaults__':collections.OrderedDict(),
|
||||||
|
'__init__':__init__, '__repr__':__repr__, }
|
||||||
|
|
||||||
|
for k in cls_dict:
|
||||||
|
if k.startswith('__') and k.endswith('__'):
|
||||||
|
if k in new_dict:
|
||||||
|
warnings.warn(
|
||||||
|
"Can't set attr {!r} in bunch-class {!r}".
|
||||||
|
format(k, cls_name))
|
||||||
|
else:
|
||||||
|
new_dict[k] = cls_dict[k]
|
||||||
|
else:
|
||||||
|
new_dict['__slots__'].append(k)
|
||||||
|
new_dict['__defaults__'][k] = cls_dict[k]
|
||||||
|
|
||||||
|
return super().__new__(meta_cls, cls_name, bases, new_dict)
|
||||||
|
|
||||||
|
class Bunch(metaclass=MetaBunch):
|
||||||
|
pass
|
||||||
38
25-class-metaprog/metabunch/pre3.6/bunch_test.py
Normal file
38
25-class-metaprog/metabunch/pre3.6/bunch_test.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from bunch import Bunch
|
||||||
|
|
||||||
|
class Point(Bunch):
|
||||||
|
""" A point has x and y coordinates, defaulting to 0.0,
|
||||||
|
and a color, defaulting to 'gray'—and nothing more,
|
||||||
|
except what Python and the metaclass conspire to add,
|
||||||
|
such as __init__ and __repr__
|
||||||
|
"""
|
||||||
|
x = 0.0
|
||||||
|
y = 0.0
|
||||||
|
color = 'gray'
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_defaults():
|
||||||
|
p = Point()
|
||||||
|
assert repr(p) == 'Point()'
|
||||||
|
|
||||||
|
|
||||||
|
def test_init():
|
||||||
|
p = Point(x=1.2, y=3.4, color='red')
|
||||||
|
assert repr(p) == "Point(x=1.2, y=3.4, color='red')"
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_wrong_argument():
|
||||||
|
with pytest.raises(AttributeError) as exc:
|
||||||
|
p = Point(x=1.2, y=3.4, flavor='coffee')
|
||||||
|
assert "no attribute 'flavor'" in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_slots():
|
||||||
|
p = Point()
|
||||||
|
with pytest.raises(AttributeError) as exc:
|
||||||
|
p.z = 5.6
|
||||||
|
assert "no attribute 'z'" in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
@@ -4,16 +4,16 @@ A ``Persistent`` class definition::
|
|||||||
>>> class Movie(Persistent):
|
>>> class Movie(Persistent):
|
||||||
... title: str
|
... title: str
|
||||||
... year: int
|
... year: int
|
||||||
... megabucks: float
|
... box_office: float
|
||||||
|
|
||||||
Implemented behavior::
|
Implemented behavior::
|
||||||
|
|
||||||
>>> Movie._connect() # doctest: +ELLIPSIS
|
>>> Movie._connect() # doctest: +ELLIPSIS
|
||||||
<sqlite3.Connection object at 0x...>
|
<sqlite3.Connection object at 0x...>
|
||||||
>>> movie = Movie(title='The Godfather', year=1972, megabucks=137)
|
>>> movie = Movie(title='The Godfather', year=1972, box_office=137)
|
||||||
>>> movie.title
|
>>> movie.title
|
||||||
'The Godfather'
|
'The Godfather'
|
||||||
>>> movie.megabucks
|
>>> movie.box_office
|
||||||
137.0
|
137.0
|
||||||
|
|
||||||
Instances always have a ``._pk`` attribute, but it is ``None`` until the
|
Instances always have a ``._pk`` attribute, but it is ``None`` until the
|
||||||
@@ -32,7 +32,7 @@ using ``Movie[pk]``—item access on the class itself::
|
|||||||
>>> del movie
|
>>> del movie
|
||||||
>>> film = Movie[1]
|
>>> film = Movie[1]
|
||||||
>>> film
|
>>> film
|
||||||
Movie(title='The Godfather', year=1972, megabucks=137.0, _pk=1)
|
Movie(title='The Godfather', year=1972, box_office=137.0, _pk=1)
|
||||||
|
|
||||||
By default, the table name is the class name lowercased, with an appended
|
By default, the table name is the class name lowercased, with an appended
|
||||||
"s" for plural::
|
"s" for plural::
|
||||||
@@ -84,35 +84,12 @@ class Persistent:
|
|||||||
if not name.startswith('_')
|
if not name.startswith('_')
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init_subclass__(cls, *, table: str = '', **kwargs: dict):
|
def __init_subclass__(cls, *, table: str = '', **kwargs: Any):
|
||||||
super().__init_subclass__(**kwargs) # type:ignore
|
super().__init_subclass__(**kwargs) # type:ignore
|
||||||
cls._TABLE_NAME = table if table else cls.__name__.lower() + 's'
|
cls._TABLE_NAME = table if table else cls.__name__.lower() + 's'
|
||||||
for name, py_type in cls._fields().items():
|
for name, py_type in cls._fields().items():
|
||||||
setattr(cls, name, Field(name, py_type))
|
setattr(cls, name, Field(name, py_type))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _connect(db_path: str = db.DEFAULT_DB_PATH):
|
|
||||||
return db.connect(db_path)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _ensure_table(cls) -> str:
|
|
||||||
if not cls._TABLE_READY:
|
|
||||||
db.ensure_table(cls._TABLE_NAME, cls._fields())
|
|
||||||
cls._TABLE_READY = True
|
|
||||||
return cls._TABLE_NAME
|
|
||||||
|
|
||||||
def __class_getitem__(cls, pk: int) -> 'Persistent':
|
|
||||||
field_names = ['_pk'] + list(cls._fields())
|
|
||||||
values = db.fetch_record(cls._TABLE_NAME, pk)
|
|
||||||
return cls(**dict(zip(field_names, values)))
|
|
||||||
|
|
||||||
def _asdict(self) -> dict[str, Any]:
|
|
||||||
return {
|
|
||||||
name: getattr(self, name)
|
|
||||||
for name, attr in self.__class__.__dict__.items()
|
|
||||||
if isinstance(attr, Field)
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *, _pk=None, **kwargs):
|
def __init__(self, *, _pk=None, **kwargs):
|
||||||
field_names = self._asdict().keys()
|
field_names = self._asdict().keys()
|
||||||
for name, arg in kwargs.items():
|
for name, arg in kwargs.items():
|
||||||
@@ -131,6 +108,32 @@ class Persistent:
|
|||||||
return f'{cls_name}({kwargs})'
|
return f'{cls_name}({kwargs})'
|
||||||
return f'{cls_name}({kwargs}, _pk={self._pk})'
|
return f'{cls_name}({kwargs}, _pk={self._pk})'
|
||||||
|
|
||||||
|
def _asdict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
name: getattr(self, name)
|
||||||
|
for name, attr in self.__class__.__dict__.items()
|
||||||
|
if isinstance(attr, Field)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# database methods
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _connect(db_path: str = db.DEFAULT_DB_PATH):
|
||||||
|
return db.connect(db_path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _ensure_table(cls) -> str:
|
||||||
|
if not cls._TABLE_READY:
|
||||||
|
db.ensure_table(cls._TABLE_NAME, cls._fields())
|
||||||
|
cls._TABLE_READY = True
|
||||||
|
return cls._TABLE_NAME
|
||||||
|
|
||||||
|
def __class_getitem__(cls, pk: int) -> 'Persistent':
|
||||||
|
field_names = ['_pk'] + list(cls._fields())
|
||||||
|
values = db.fetch_record(cls._TABLE_NAME, pk)
|
||||||
|
return cls(**dict(zip(field_names, values)))
|
||||||
|
|
||||||
def _save(self) -> int:
|
def _save(self) -> int:
|
||||||
table = self.__class__._ensure_table()
|
table = self.__class__._ensure_table()
|
||||||
if self._pk is None:
|
if self._pk is None:
|
||||||
|
|||||||
5
25-class-metaprog/qualname/fakedjango.py
Normal file
5
25-class-metaprog/qualname/fakedjango.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class models:
|
||||||
|
class Model:
|
||||||
|
"nothing to see here"
|
||||||
|
class IntegerField:
|
||||||
|
"nothing to see here"
|
||||||
13
25-class-metaprog/qualname/models.py
Executable file
13
25-class-metaprog/qualname/models.py
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from fakedjango import models
|
||||||
|
|
||||||
|
class Ox(models.Model):
|
||||||
|
horn_length = models.IntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['horn_length']
|
||||||
|
verbose_name_plural = 'oxen'
|
||||||
|
|
||||||
|
print(Ox.Meta.__name__)
|
||||||
|
print(Ox.Meta.__qualname__)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user