From 1a5da64b51bcf2da8605ba4603bae6164f671195 Mon Sep 17 00:00:00 2001 From: Real Python Bot <42617967+realpython-bot@users.noreply.github.com> Date: Tue, 29 Sep 2026 06:10:12 +0000 Subject: [PATCH] Sync asyncio-walkthrough with the maintenance update of Python's asyncio: A Hands-On Walkthrough Co-Authored-By: Claude Opus 5.5 --- asyncio-walkthrough/areq.py | 123 ------------------------------ asyncio-walkthrough/asyncq.py | 70 ----------------- asyncio-walkthrough/chained.py | 66 ++++++++-------- asyncio-walkthrough/countasync.py | 8 +- asyncio-walkthrough/countsync.py | 8 +- asyncio-walkthrough/phases.py | 35 --------- asyncio-walkthrough/queued.py | 52 +++++++++++++ asyncio-walkthrough/rand.py | 46 +++++------ asyncio-walkthrough/urls.txt | 8 -- 9 files changed, 108 insertions(+), 308 deletions(-) delete mode 100644 asyncio-walkthrough/areq.py delete mode 100644 asyncio-walkthrough/asyncq.py delete mode 100644 asyncio-walkthrough/phases.py create mode 100644 asyncio-walkthrough/queued.py delete mode 100644 asyncio-walkthrough/urls.txt diff --git a/asyncio-walkthrough/areq.py b/asyncio-walkthrough/areq.py deleted file mode 100644 index 12ae0cde14..0000000000 --- a/asyncio-walkthrough/areq.py +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env python3 -# areq.py - -"""Asynchronously get links embedded in multiple pages' HMTL.""" - -import asyncio -import logging -import re -import sys -import urllib.error -import urllib.parse -from typing import IO - -import aiofiles -import aiohttp -from aiohttp import ClientSession - -logging.basicConfig( - format="%(asctime)s %(levelname)s:%(name)s: %(message)s", - level=logging.DEBUG, - datefmt="%H:%M:%S", - stream=sys.stderr, -) -logger = logging.getLogger("areq") -logging.getLogger("chardet.charsetprober").disabled = True - -HREF_RE = re.compile(r'href="(.*?)"') - - -async def fetch_html(url: str, session: ClientSession, **kwargs) -> str: - """GET request wrapper to fetch page HTML. - - kwargs are passed to `session.request()`. - """ - - # Don't do any try/except here. If either the request or reading - # of bytes raises, let that be handled by caller. - resp = await session.request(method="GET", url=url, **kwargs) - resp.raise_for_status() # raise if status >= 400 - logger.info("Got response [%s] for URL: %s", resp.status, url) - html = await resp.text() # For bytes: resp.read() - - # Dont close session; let caller decide when to do that. - return html - - -async def parse(url: str, session: ClientSession, **kwargs) -> set: - """Find HREFs in the HTML of `url`.""" - found = set() - try: - html = await fetch_html(url=url, session=session, **kwargs) - except ( - aiohttp.ClientError, - aiohttp.http_exceptions.HttpProcessingError, - ) as e: - logger.error( - "aiohttp exception for %s [%s]: %s", - url, - getattr(e, "status", None), - getattr(e, "message", None), - ) - return found - except Exception as e: - # May be raised from other libraries, such as chardet or yarl. - # logger.exception will show the full traceback. - logger.exception( - "Non-aiohttp exception occured: %s", getattr(e, "__dict__", {}) - ) - return found - else: - # This portion is not really async, but it is the request/response - # IO cycle that eats the largest portion of time. - for link in HREF_RE.findall(html): - try: - # Ensure we return an absolute path. - abslink = urllib.parse.urljoin(url, link) - except (urllib.error.URLError, ValueError): - logger.exception("Error parsing URL: %s", link) - pass - else: - found.add(abslink) - logger.info("Found %d links for %s", len(found), url) - return found - - -async def write_one(file: IO, url: str, **kwargs) -> None: - """Write the found HREFs from `url` to `file`.""" - res = await parse(url=url, **kwargs) - if not res: - return None - async with aiofiles.open(file, "a") as f: - for p in res: - await f.write(f"{url}\t{p}\n") - logger.info("Wrote results for source URL: %s", url) - - -async def bulk_crawl_and_write(file: IO, urls: set, **kwargs) -> None: - """Crawl & write concurrently to `file` for multiple `urls`.""" - async with ClientSession() as session: - tasks = [] - for url in urls: - tasks.append( - write_one(file=file, url=url, session=session, **kwargs) - ) - await asyncio.gather(*tasks) # see also: return_exceptions=True - - -if __name__ == "__main__": - import pathlib - import sys - - assert sys.version_info >= (3, 7), "Script requires Python 3.7+." - here = pathlib.Path(__file__).parent - - with open(here.joinpath("urls.txt")) as infile: - urls = set(map(str.strip, infile)) - - # Header - just a single, initial row-write - outpath = here.joinpath("foundurls.txt") - with open(outpath, "w") as outfile: - outfile.write("source_url\tparsed_url\n") - - asyncio.run(bulk_crawl_and_write(file=outpath, urls=urls)) diff --git a/asyncio-walkthrough/asyncq.py b/asyncio-walkthrough/asyncq.py deleted file mode 100644 index ef2c9c15e1..0000000000 --- a/asyncio-walkthrough/asyncq.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -# asyncq.py - -import asyncio -import itertools as it -import os -import random -import time - - -async def makeitem(size: int = 5) -> str: - return os.urandom(size).hex() - - -async def seconds() -> float: - return time.perf_counter() - - -async def randint(a: int, b: int) -> int: - return random.randint(a, b) - - -async def randsleep(a: int = 1, b: int = 5, caller=None) -> None: - i = await randint(a, b) - if caller: - print(f"{caller} sleeping for {i} seconds.") - await asyncio.sleep(i) - - -async def produce(name: int, q: asyncio.Queue) -> None: - n = await randint(1, 5) - for _ in it.repeat(None, n): # Synchronous - await randsleep(caller=f"Producer {name}") - i = await makeitem() - t = await seconds() - await q.put((i, t)) - print(f"Producer {name} added <{i}> to queue.") - - -async def consume(name: int, q: asyncio.Queue) -> None: - while True: - await randsleep(caller=f"Consumer {name}") - i, t = await q.get() - now = await seconds() - print(f"Consumer {name} got element <{i}> in {now - t:0.5f} seconds.") - q.task_done() - - -async def main(nprod: int, ncon: int): - q = asyncio.Queue() - producers = [asyncio.create_task(produce(n, q)) for n in range(nprod)] - consumers = [asyncio.create_task(consume(n, q)) for n in range(ncon)] - await asyncio.gather(*producers) - await q.join() - for c in consumers: - c.cancel() - - -if __name__ == "__main__": - import argparse - - random.seed(444) - parser = argparse.ArgumentParser() - parser.add_argument("-p", "--nprod", type=int, default=5) - parser.add_argument("-c", "--ncon", type=int, default=10) - ns = parser.parse_args() - start = time.perf_counter() - asyncio.run(main(**ns.__dict__)) - elapsed = time.perf_counter() - start - print(f"Program completed in {elapsed:0.5f} seconds.") diff --git a/asyncio-walkthrough/chained.py b/asyncio-walkthrough/chained.py index 766a085d96..c9df237e98 100644 --- a/asyncio-walkthrough/chained.py +++ b/asyncio-walkthrough/chained.py @@ -1,51 +1,45 @@ -#!/usr/env/bin python3 -# chained.py - import asyncio import random import time -async def randint(a: int, b: int) -> int: - return random.randint(a, b) - +async def main(): + user_ids = [1, 2, 3] + start = time.perf_counter() + await asyncio.gather( + *(get_user_with_posts(user_id) for user_id in user_ids) + ) + end = time.perf_counter() + print(f"\n==> Total time: {end - start:.2f} seconds") -async def part1(n: int) -> str: - i = await randint(0, 10) - print(f"part1({n}) sleeping for {i} seconds.") - await asyncio.sleep(i) - result = f"result{n}-1" - print(f"Returning part1({n}) == {result}.") - return result +async def get_user_with_posts(user_id): + user = await fetch_user(user_id) + await fetch_posts(user) -async def part2(n: int, arg: str) -> str: - i = await randint(0, 10) - print(f"part2{n, arg} sleeping for {i} seconds.") - await asyncio.sleep(i) - result = f"result{n}-2 derived from {arg}" - print(f"Returning part2{n, arg} == {result}.") - return result +async def fetch_user(user_id): + delay = random.uniform(0.5, 2.0) + print(f"User coro: fetching user by {user_id=}...") + await asyncio.sleep(delay) + user = {"id": user_id, "name": f"User{user_id}"} + print(f"User coro: fetched user with {user_id=} (done in {delay:.1f}s).") + return user -async def chain(n: int) -> None: - start = time.perf_counter() - p1 = await part1(n) - p2 = await part2(n, p1) - end = time.perf_counter() - start - print(f"-->Chained result{n} => {p2} (took {end:0.2f} seconds).") - -async def main(*args): - await asyncio.gather(*(chain(n) for n in args)) +async def fetch_posts(user): + delay = random.uniform(0.5, 2.0) + print(f"Post coro: retrieving posts for {user['name']}...") + await asyncio.sleep(delay) + posts = [f"Post {i} by {user['name']}" for i in range(1, 3)] + print( + f"Post coro: got {len(posts)} posts by {user['name']}" + f" (done in {delay:.1f}s):" + ) + for post in posts: + print(f" - {post}") if __name__ == "__main__": - import sys - random.seed(444) - args = [1, 2, 3] if len(sys.argv) == 1 else map(int, sys.argv[1:]) - start = time.perf_counter() - asyncio.run(main(*args)) - end = time.perf_counter() - start - print(f"Program finished in {end:0.2f} seconds.") + asyncio.run(main()) diff --git a/asyncio-walkthrough/countasync.py b/asyncio-walkthrough/countasync.py index 3b5e6dcaba..a7b82de778 100644 --- a/asyncio-walkthrough/countasync.py +++ b/asyncio-walkthrough/countasync.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python3 -# countasync.py - import asyncio @@ -8,6 +5,7 @@ async def count(): print("One") await asyncio.sleep(1) print("Two") + await asyncio.sleep(1) async def main(): @@ -17,7 +15,7 @@ async def main(): if __name__ == "__main__": import time - s = time.perf_counter() + start = time.perf_counter() asyncio.run(main()) - elapsed = time.perf_counter() - s + elapsed = time.perf_counter() - start print(f"{__file__} executed in {elapsed:0.2f} seconds.") diff --git a/asyncio-walkthrough/countsync.py b/asyncio-walkthrough/countsync.py index e93e2e4459..a67d6478c1 100644 --- a/asyncio-walkthrough/countsync.py +++ b/asyncio-walkthrough/countsync.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python3 -# countsync.py - import time @@ -8,6 +5,7 @@ def count(): print("One") time.sleep(1) print("Two") + time.sleep(1) def main(): @@ -16,7 +14,7 @@ def main(): if __name__ == "__main__": - s = time.perf_counter() + start = time.perf_counter() main() - elapsed = time.perf_counter() - s + elapsed = time.perf_counter() - start print(f"{__file__} executed in {elapsed:0.2f} seconds.") diff --git a/asyncio-walkthrough/phases.py b/asyncio-walkthrough/phases.py deleted file mode 100644 index 4b888f924d..0000000000 --- a/asyncio-walkthrough/phases.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/env/bin python3 -# phases.py - -import asyncio - - -async def phase1(callerid: str): - print(f"phase 1 called from {callerid}") - await asyncio.sleep(2) - return "result1" - - -async def phase2(callerid: str, arg: str): - print(f"phase 2 called from {callerid}") - await asyncio.sleep(2) - # No await needed here - arg is passed from caller. - return f"result2 derived from {arg}" - - -async def outer(callerid: str): - """A wrapper for parameterizing a full coroutine.""" - print(f"outer called from {callerid}") - r1 = await phase1(callerid) - r2 = await phase2(callerid, r1) - return r1, r2 - - -async def main(): - """Wrap the coroutines into tasks and execute.""" - results = await asyncio.gather(*(outer(i) for i in "ABC")) - return results - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/asyncio-walkthrough/queued.py b/asyncio-walkthrough/queued.py new file mode 100644 index 0000000000..701d7406fb --- /dev/null +++ b/asyncio-walkthrough/queued.py @@ -0,0 +1,52 @@ +import asyncio +import random +import time + + +async def main(): + queue = asyncio.Queue() + user_ids = [1, 2, 3] + + start = time.perf_counter() + await asyncio.gather( + producer(queue, user_ids), + *(consumer(queue) for _ in user_ids), + ) + end = time.perf_counter() + print(f"\n==> Total time: {end - start:.2f} seconds") + + +async def producer(queue, user_ids): + async def fetch_user(user_id): + delay = random.uniform(0.5, 2.0) + print(f"Producer: fetching user by {user_id=}...") + await asyncio.sleep(delay) + user = {"id": user_id, "name": f"User{user_id}"} + print(f"Producer: fetched user with {user_id=} (done in {delay:.1f}s)") + await queue.put(user) + + await asyncio.gather(*(fetch_user(uid) for uid in user_ids)) + for _ in range(len(user_ids)): + await queue.put(None) # Sentinels for consumers to terminate + + +async def consumer(queue): + while True: + user = await queue.get() + if user is None: + break + delay = random.uniform(0.5, 2.0) + print(f"Consumer: retrieving posts for {user['name']}...") + await asyncio.sleep(delay) + posts = [f"Post {i} by {user['name']}" for i in range(1, 3)] + print( + f"Consumer: got {len(posts)} posts by {user['name']}" + f" (done in {delay:.1f}s):" + ) + for post in posts: + print(f" - {post}") + + +if __name__ == "__main__": + random.seed(444) + asyncio.run(main()) diff --git a/asyncio-walkthrough/rand.py b/asyncio-walkthrough/rand.py index 43b65c10e1..db4bbe9335 100644 --- a/asyncio-walkthrough/rand.py +++ b/asyncio-walkthrough/rand.py @@ -1,36 +1,30 @@ -#!/usr/bin/env python3 -# rand.py - import asyncio import random -# colors -c = ( - "\033[0m", # end of color - "\033[36m", # cyan - "\033[91m", # red - "\033[35m", # magenta +COLORS = ( + "\033[0m", # End of color + "\033[36m", # Cyan + "\033[91m", # Red + "\033[35m", # Magenta ) -async def randint(a: int, b: int) -> int: - return random.randint(a, b) - - -async def makerandom(idx: int, threshold: int = 6) -> int: - print(c[idx + 1] + f"Initiated makerandom({idx}).") - i = await randint(0, 10) - while i <= threshold: - print(c[idx + 1] + f"makerandom({idx}) == {i} too low; retrying.") - await asyncio.sleep(idx + 1) - i = await randint(0, 10) - print(c[idx + 1] + f"---> Finished: makerandom({idx}) == {i}" + c[0]) - return i - - async def main(): - res = await asyncio.gather(*(makerandom(i, 10 - i - 1) for i in range(3))) - return res + return await asyncio.gather( + makerandom(1, 9), + makerandom(2, 8), + makerandom(3, 8), + ) + + +async def makerandom(delay, threshold=6): + color = COLORS[delay] + print(f"{color}Initiated makerandom({delay}).") + while (number := random.randint(0, 10)) <= threshold: + print(f"{color}makerandom({delay}) == {number} too low; retrying.") + await asyncio.sleep(delay) + print(f"{color}---> Finished: makerandom({delay}) == {number}" + COLORS[0]) + return number if __name__ == "__main__": diff --git a/asyncio-walkthrough/urls.txt b/asyncio-walkthrough/urls.txt deleted file mode 100644 index 9de3776bb5..0000000000 --- a/asyncio-walkthrough/urls.txt +++ /dev/null @@ -1,8 +0,0 @@ -https://regex101.com/ -https://docs.python.org/3/this-url-will-404.html -https://www.nytimes.com/guides/ -https://www.mediamatters.org/ -https://1.1.1.1/ -https://www.politico.com/tipsheets/morning-money -https://www.bloomberg.com/markets/economics -https://www.ietf.org/rfc/rfc2616.txt