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