diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b1ac18..115090c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +### Fixed + +- Match pages and `Summary()` load inside Jupyter and other code that already + runs an asyncio event loop; they raised `asyncio.run() cannot be called from + a running event loop` (#55). + ### Added - `Series.season(key)` finds a season by year or by ESPNcricinfo's two-year diff --git a/espncricinfo/match.py b/espncricinfo/match.py index d6858ac..105aa61 100644 --- a/espncricinfo/match.py +++ b/espncricinfo/match.py @@ -2,6 +2,7 @@ import asyncio import json +from concurrent.futures import ThreadPoolExecutor from datetime import date as _date from datetime import datetime as _datetime from typing import Any, TypedDict @@ -50,9 +51,19 @@ async def _async_playwright_fetch(url): return json.loads(m.group(1)) -def _playwright_fetch(url): - """Synchronous wrapper around _async_playwright_fetch.""" - return asyncio.run(_async_playwright_fetch(url)) +def _playwright_fetch(url: str) -> dict[str, Any]: + """Synchronous wrapper around _async_playwright_fetch. + + Jupyter and other async callers already run an event loop on this thread, + and asyncio.run() refuses to nest inside one, so the fetch then runs on a + worker thread with its own loop. + """ + try: + asyncio.get_running_loop() + except RuntimeError: + return asyncio.run(_async_playwright_fetch(url)) + with ThreadPoolExecutor(max_workers=1) as pool: + return pool.submit(lambda: asyncio.run(_async_playwright_fetch(url))).result() def _as_float(record: dict[str, Any] | None, key: str) -> float | None: diff --git a/tests/test_match.py b/tests/test_match.py index 3007615..ced411e 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -434,3 +434,19 @@ def test_no_toss_leaves_toss_fields_none(): def test_series_id_is_int(): assert isinstance(_make_match().series_id, int) + + +def test_fetch_works_inside_a_running_event_loop(): + """Jupyter runs an event loop; asyncio.run() refuses to nest inside it (#55).""" + import asyncio + + import espncricinfo.match as match_module + + async def fake_fetch(url): + return {"url": url} + + async def notebook_cell(): + return match_module._playwright_fetch("https://example.invalid/page") + + with patch.object(match_module, "_async_playwright_fetch", fake_fetch): + assert asyncio.run(notebook_cell()) == {"url": "https://example.invalid/page"}