Skip to content

Commit 80f7f84

Browse files
committed
2e reviewed manuscript
1 parent f5e3cb8 commit 80f7f84

32 files changed

+323
-156
lines changed

02-array-seq/lispy/py3.10/lis.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def standard_env() -> Environment:
8484
'-': op.sub,
8585
'*': op.mul,
8686
'/': op.truediv,
87-
'//': op.floordiv,
87+
'quotient': op.floordiv,
8888
'>': op.gt,
8989
'<': op.lt,
9090
'>=': op.ge,
@@ -149,7 +149,7 @@ def evaluate(exp: Expression, env: Environment) -> Any:
149149
# end::EVAL_MATCH_TOP[]
150150
case int(x) | float(x):
151151
return x
152-
case Symbol(name):
152+
case Symbol() as name:
153153
return env[name]
154154
# tag::EVAL_MATCH_MIDDLE[]
155155
case ['quote', x]: # <1>
@@ -161,12 +161,12 @@ def evaluate(exp: Expression, env: Environment) -> Any:
161161
return evaluate(alternative, env)
162162
case ['lambda', [*parms], *body] if body: # <3>
163163
return Procedure(parms, body, env)
164-
case ['define', Symbol(name), value_exp]: # <4>
164+
case ['define', Symbol() as name, value_exp]: # <4>
165165
env[name] = evaluate(value_exp, env)
166166
# end::EVAL_MATCH_MIDDLE[]
167-
case ['define', [Symbol(name), *parms], *body] if body:
167+
case ['define', [Symbol() as name, *parms], *body] if body:
168168
env[name] = Procedure(parms, body, env)
169-
case ['set!', Symbol(name), value_exp]:
169+
case ['set!', Symbol() as name, value_exp]:
170170
env.change(name, evaluate(value_exp, env))
171171
case [func_exp, *args] if func_exp not in KEYWORDS:
172172
proc = evaluate(func_exp, env)

11-pythonic-obj/vector2d_v3.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,6 @@
7979
8080
>>> v1 = Vector2d(3, 4)
8181
>>> v2 = Vector2d(3.1, 4.2)
82-
>>> hash(v1), hash(v2)
83-
(7, 384307168202284039)
8482
>>> len({v1, v2})
8583
2
8684
@@ -124,7 +122,7 @@ def __eq__(self, other):
124122
return tuple(self) == tuple(other)
125123

126124
def __hash__(self):
127-
return hash(self.x) ^ hash(self.y)
125+
return hash((self.x, self.y))
128126

129127
def __abs__(self):
130128
return math.hypot(self.x, self.y)

11-pythonic-obj/vector2d_v3_prophash.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,6 @@
8181
8282
>>> v1 = Vector2d(3, 4)
8383
>>> v2 = Vector2d(3.1, 4.2)
84-
>>> hash(v1), hash(v2)
85-
(7, 384307168202284039)
8684
>>> len({v1, v2})
8785
2
8886
@@ -131,7 +129,7 @@ def __eq__(self, other):
131129

132130
# tag::VECTOR_V3_HASH[]
133131
def __hash__(self):
134-
return hash(self.x) ^ hash(self.y)
132+
return hash((self.x, self.y))
135133
# end::VECTOR_V3_HASH[]
136134

137135
def __abs__(self):

11-pythonic-obj/vector2d_v3_slots.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@
7878
7979
>>> v1 = Vector2d(3, 4)
8080
>>> v2 = Vector2d(3.1, 4.2)
81-
>>> hash(v1), hash(v2)
82-
(7, 384307168202284039)
8381
>>> len({v1, v2})
8482
2
8583
@@ -126,7 +124,7 @@ def __eq__(self, other):
126124
return tuple(self) == tuple(other)
127125

128126
def __hash__(self):
129-
return hash(self.x) ^ hash(self.y)
127+
return hash((self.x, self.y))
130128

131129
def __abs__(self):
132130
return math.hypot(self.x, self.y)

18-with-match/lispy/py3.10/examples_test.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727
>>> inner_env = {'a': 2}
2828
>>> outer_env = {'a': 0, 'b': 1}
2929
>>> env = Environment(inner_env, outer_env)
30-
>>> env['a'] = 111 # <1>
30+
>>> env['a'] # <1>
31+
2
32+
>>> env['a'] = 111 # <2>
3133
>>> env['c'] = 222
3234
>>> env
3335
Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 1})
34-
>>> env.change('b', 333) # <2>
36+
>>> env.change('b', 333) # <3>
3537
>>> env
3638
Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 333})
3739

18-with-match/lispy/py3.10/lis.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def parse_atom(token: str) -> Atom:
6565
class Environment(ChainMap[Symbol, Any]):
6666
"A ChainMap that allows changing an item in-place."
6767

68-
def change(self, key: Symbol, value: object) -> None:
68+
def change(self, key: Symbol, value: Any) -> None:
6969
"Find where key is defined and change the value there."
7070
for map in self.maps:
7171
if key in map:

18-with-match/lispy/py3.9/lis.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def parse_atom(token: str) -> Atom:
6262
class Environment(ChainMap[Symbol, Any]):
6363
"A ChainMap that allows changing an item in-place."
6464

65-
def change(self, key: Symbol, value: object) -> None:
65+
def change(self, key: Symbol, value: Any) -> None:
6666
"Find where key is defined and change the value there."
6767
for map in self.maps:
6868
if key in map:

20-executors/getflags/flags2_asyncio.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ async def download_one(client: httpx.AsyncClient,
3737
try:
3838
async with semaphore: # <3>
3939
image = await get_flag(client, base_url, cc)
40-
except httpx.HTTPStatusError as exc: # <5>
40+
except httpx.HTTPStatusError as exc: # <4>
4141
res = exc.response
4242
if res.status_code == HTTPStatus.NOT_FOUND:
4343
status = DownloadStatus.NOT_FOUND
4444
msg = f'not found: {res.url}'
4545
else:
4646
raise
4747
else:
48-
await asyncio.to_thread(save_flag, image, f'{cc}.gif') # <6>
48+
await asyncio.to_thread(save_flag, image, f'{cc}.gif') # <5>
4949
status = DownloadStatus.OK
5050
msg = 'OK'
5151
if verbose and msg:

20-executors/getflags/flags2_asyncio_executor.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,23 @@
2222
MAX_CONCUR_REQ = 1000
2323

2424

25-
async def get_flag(session: httpx.AsyncClient, # <2>
25+
async def get_flag(client: httpx.AsyncClient, # <2>
2626
base_url: str,
2727
cc: str) -> bytes:
2828
url = f'{base_url}/{cc}/{cc}.gif'.lower()
29-
resp = await session.get(url, timeout=3.1, follow_redirects=True) # <3>
29+
resp = await client.get(url, timeout=3.1, follow_redirects=True) # <3>
3030
resp.raise_for_status()
3131
return resp.content
3232

3333

34-
async def download_one(session: httpx.AsyncClient,
34+
async def download_one(client: httpx.AsyncClient,
3535
cc: str,
3636
base_url: str,
3737
semaphore: asyncio.Semaphore,
3838
verbose: bool) -> DownloadStatus:
3939
try:
4040
async with semaphore:
41-
image = await get_flag(session, base_url, cc)
41+
image = await get_flag(client, base_url, cc)
4242
except httpx.HTTPStatusError as exc:
4343
res = exc.response
4444
if res.status_code == HTTPStatus.NOT_FOUND:
@@ -64,8 +64,8 @@ async def supervisor(cc_list: list[str],
6464
concur_req: int) -> Counter[DownloadStatus]: # <1>
6565
counter: Counter[DownloadStatus] = Counter()
6666
semaphore = asyncio.Semaphore(concur_req) # <2>
67-
async with httpx.AsyncClient() as session:
68-
to_do = [download_one(session, cc, base_url, semaphore, verbose)
67+
async with httpx.AsyncClient() as client:
68+
to_do = [download_one(client, cc, base_url, semaphore, verbose)
6969
for cc in sorted(cc_list)] # <3>
7070
to_do_iter = asyncio.as_completed(to_do) # <4>
7171
if not verbose:
+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env python3
2+
3+
"""Download flags of countries (with error handling).
4+
5+
asyncio async/await version
6+
7+
"""
8+
# tag::FLAGS2_ASYNCIO_TOP[]
9+
import asyncio
10+
from collections import Counter
11+
from http import HTTPStatus
12+
from pathlib import Path
13+
14+
import httpx
15+
import tqdm # type: ignore
16+
17+
from flags2_common import main, DownloadStatus, save_flag
18+
19+
# low concurrency default to avoid errors from remote site,
20+
# such as 503 - Service Temporarily Unavailable
21+
DEFAULT_CONCUR_REQ = 5
22+
MAX_CONCUR_REQ = 1000
23+
24+
async def get_flag(client: httpx.AsyncClient, # <1>
25+
base_url: str,
26+
cc: str) -> bytes:
27+
url = f'{base_url}/{cc}/{cc}.gif'.lower()
28+
resp = await client.get(url, timeout=3.1, follow_redirects=True) # <2>
29+
resp.raise_for_status()
30+
return resp.content
31+
32+
# tag::FLAGS3_ASYNCIO_GET_COUNTRY[]
33+
async def get_country(client: httpx.AsyncClient,
34+
base_url: str,
35+
cc: str) -> str: # <1>
36+
url = f'{base_url}/{cc}/metadata.json'.lower()
37+
resp = await client.get(url, timeout=3.1, follow_redirects=True)
38+
resp.raise_for_status()
39+
metadata = resp.json() # <2>
40+
return metadata['country'] # <3>
41+
# end::FLAGS3_ASYNCIO_GET_COUNTRY[]
42+
43+
# tag::FLAGS3_ASYNCIO_DOWNLOAD_ONE[]
44+
async def download_one(client: httpx.AsyncClient,
45+
cc: str,
46+
base_url: str,
47+
semaphore: asyncio.Semaphore,
48+
verbose: bool) -> DownloadStatus:
49+
try:
50+
async with semaphore: # <1>
51+
image = await get_flag(client, base_url, cc)
52+
async with semaphore: # <2>
53+
country = await get_country(client, base_url, cc)
54+
except httpx.HTTPStatusError as exc:
55+
res = exc.response
56+
if res.status_code == HTTPStatus.NOT_FOUND:
57+
status = DownloadStatus.NOT_FOUND
58+
msg = f'not found: {res.url}'
59+
else:
60+
raise
61+
else:
62+
filename = country.replace(' ', '_') # <3>
63+
await asyncio.to_thread(save_flag, image, f'{filename}.gif')
64+
status = DownloadStatus.OK
65+
msg = 'OK'
66+
if verbose and msg:
67+
print(cc, msg)
68+
return status
69+
# end::FLAGS3_ASYNCIO_DOWNLOAD_ONE[]
70+
71+
# tag::FLAGS2_ASYNCIO_START[]
72+
async def supervisor(cc_list: list[str],
73+
base_url: str,
74+
verbose: bool,
75+
concur_req: int) -> Counter[DownloadStatus]: # <1>
76+
counter: Counter[DownloadStatus] = Counter()
77+
semaphore = asyncio.Semaphore(concur_req) # <2>
78+
async with httpx.AsyncClient() as client:
79+
to_do = [download_one(client, cc, base_url, semaphore, verbose)
80+
for cc in sorted(cc_list)] # <3>
81+
to_do_iter = asyncio.as_completed(to_do) # <4>
82+
if not verbose:
83+
to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list)) # <5>
84+
error: httpx.HTTPError | None = None # <6>
85+
for coro in to_do_iter: # <7>
86+
try:
87+
status = await coro # <8>
88+
except httpx.HTTPStatusError as exc:
89+
error_msg = 'HTTP error {resp.status_code} - {resp.reason_phrase}'
90+
error_msg = error_msg.format(resp=exc.response)
91+
error = exc # <9>
92+
except httpx.RequestError as exc:
93+
error_msg = f'{exc} {type(exc)}'.strip()
94+
error = exc # <10>
95+
except KeyboardInterrupt:
96+
break
97+
98+
if error:
99+
status = DownloadStatus.ERROR # <11>
100+
if verbose:
101+
url = str(error.request.url) # <12>
102+
cc = Path(url).stem.upper() # <13>
103+
print(f'{cc} error: {error_msg}')
104+
counter[status] += 1
105+
106+
return counter
107+
108+
def download_many(cc_list: list[str],
109+
base_url: str,
110+
verbose: bool,
111+
concur_req: int) -> Counter[DownloadStatus]:
112+
coro = supervisor(cc_list, base_url, verbose, concur_req)
113+
counts = asyncio.run(coro) # <14>
114+
115+
return counts
116+
117+
if __name__ == '__main__':
118+
main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ)
119+
# end::FLAGS2_ASYNCIO_START[]

20-executors/getflags/flags_asyncio.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@
1717

1818
from flags import BASE_URL, save_flag, main # <2>
1919

20-
async def download_one(session: AsyncClient, cc: str): # <3>
21-
image = await get_flag(session, cc)
20+
async def download_one(client: AsyncClient, cc: str): # <3>
21+
image = await get_flag(client, cc)
2222
save_flag(image, f'{cc}.gif')
2323
print(cc, end=' ', flush=True)
2424
return cc
2525

26-
async def get_flag(session: AsyncClient, cc: str) -> bytes: # <4>
26+
async def get_flag(client: AsyncClient, cc: str) -> bytes: # <4>
2727
url = f'{BASE_URL}/{cc}/{cc}.gif'.lower()
28-
resp = await session.get(url, timeout=6.1,
28+
resp = await client.get(url, timeout=6.1,
2929
follow_redirects=True) # <5>
3030
return resp.read() # <6>
3131
# end::FLAGS_ASYNCIO_TOP[]
@@ -35,8 +35,8 @@ def download_many(cc_list: list[str]) -> int: # <1>
3535
return asyncio.run(supervisor(cc_list)) # <2>
3636

3737
async def supervisor(cc_list: list[str]) -> int:
38-
async with AsyncClient() as session: # <3>
39-
to_do = [download_one(session, cc)
38+
async with AsyncClient() as client: # <3>
39+
to_do = [download_one(client, cc)
4040
for cc in sorted(cc_list)] # <4>
4141
res = await asyncio.gather(*to_do) # <5>
4242

20-executors/getflags/httpx-error-tree/tree.py

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
def tree(cls, level=0, last_sibling=True):
77
yield cls, level, last_sibling
8+
89
# get RuntimeError and exceptions defined in httpx
910
subclasses = [sub for sub in cls.__subclasses__()
1011
if sub is RuntimeError or sub.__module__ == 'httpx']

20-executors/getflags/tree.py

-38
This file was deleted.

0 commit comments

Comments
 (0)