Skip to content

Commit 8e980fc

Browse files
committed
Callback decorator
1 parent 6ff0a17 commit 8e980fc

File tree

2 files changed

+28
-13
lines changed

2 files changed

+28
-13
lines changed

scrapy_playwright/_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import inspect
23
import logging
34
import platform
45
import threading
@@ -8,6 +9,7 @@
89
from playwright.async_api import Error, Page, Request, Response
910
from scrapy.http.headers import Headers
1011
from scrapy.settings import Settings
12+
from scrapy.utils.asyncgen import collect_asyncgen
1113
from scrapy.utils.python import to_unicode
1214
from twisted.internet.defer import Deferred
1315
from w3lib.encoding import html_body_declared_encoding, http_content_type_encoding
@@ -117,6 +119,7 @@ class _ThreadedLoopAdapter:
117119
@classmethod
118120
async def _handle_coro(cls, coro, future) -> None:
119121
try:
122+
coro = collect_asyncgen(coro) if inspect.isasyncgen(coro) else coro
120123
future.set_result(await coro)
121124
except Exception as exc:
122125
future.set_exception(exc)

scrapy_playwright/utils.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
import asyncio
21
import functools
32
import inspect
4-
from typing import Awaitable
3+
from typing import Awaitable, Callable
54

65
from ._utils import _ThreadedLoopAdapter
76

87

9-
def ensure_future(coro: Awaitable) -> asyncio.Future:
10-
"""Wrap a coroutine in a Future assigned to the threaded event loop.
8+
async def _run_async_gen(asyncgen):
9+
async for item in asyncgen:
10+
yield item
11+
12+
13+
def use_threaded_loop(callback: Awaitable) -> Callable:
14+
"""Wrap a coroutine callback so that Playwright coroutines are executed in
15+
the threaded event loop.
1116
1217
On windows, Playwright runs in an event loop of its own in a separate thread.
1318
If Playwright coroutines are awaited directly, they are assigned to the main
@@ -17,26 +22,33 @@ def ensure_future(coro: Awaitable) -> asyncio.Future:
1722
Usage:
1823
```
1924
from playwright.async_api import Page
20-
from scrapy_playwright import ensure_future
25+
from scrapy_playwright.utils import use_threaded_loop
2126
27+
@use_threaded_loop
2228
async def parse(self, response):
2329
page: Page = response.meta["playwright_page"]
24-
await ensure_future(page.screenshot(path="example.png", full_page=True))
30+
await page.screenshot(path="example.png", full_page=True)
2531
```
2632
"""
27-
return _ThreadedLoopAdapter._ensure_future(coro)
2833

29-
30-
def use_threaded_loop(callback):
31-
if not (inspect.iscoroutinefunction(callback) or inspect.isasyncgenfunction(callback)):
34+
if not inspect.iscoroutinefunction(callback) and not inspect.isasyncgenfunction(callback):
3235
raise RuntimeError(
3336
f"Cannot decorate callback '{callback.__name__}' with 'use_threaded_loop':"
3437
" callback must be a coroutine function or an async generator"
3538
)
3639

3740
@functools.wraps(callback)
38-
async def wrapper(*args, **kwargs):
39-
future: asyncio.Future = _ThreadedLoopAdapter._ensure_future(callback(*args, **kwargs))
41+
async def async_func_wrapper(*args, **kwargs):
42+
future = _ThreadedLoopAdapter._ensure_future(callback(*args, **kwargs))
4043
return await future
4144

42-
return wrapper
45+
@functools.wraps(callback)
46+
async def async_gen_wrapper(*args, **kwargs):
47+
asyncgen = _run_async_gen(callback(*args, **kwargs))
48+
future = _ThreadedLoopAdapter._ensure_future(asyncgen)
49+
for item in await future:
50+
yield item
51+
52+
if inspect.isasyncgenfunction(callback):
53+
return async_gen_wrapper
54+
return async_func_wrapper

0 commit comments

Comments
 (0)