Skip to content

Commit 60a4e06

Browse files
committed
Move _WindowsAdapter to _utils module
1 parent 4674ba8 commit 60a4e06

File tree

3 files changed

+48
-38
lines changed

3 files changed

+48
-38
lines changed

scrapy_playwright/_utils.py

+43-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import asyncio
2+
import concurrent
13
import logging
4+
import platform
5+
import threading
26
from typing import Awaitable, Iterator, Optional, Tuple, Union
37

8+
import scrapy
49
from playwright.async_api import Error, Page, Request, Response
5-
from scrapy import Spider
610
from scrapy.http.headers import Headers
711
from scrapy.utils.python import to_unicode
12+
from twisted.internet.defer import Deferred
813
from w3lib.encoding import html_body_declared_encoding, http_content_type_encoding
914

1015

@@ -53,7 +58,7 @@ def _is_safe_close_error(error: Error) -> bool:
5358

5459
async def _get_page_content(
5560
page: Page,
56-
spider: Spider,
61+
spider: scrapy.Spider,
5762
context_name: str,
5863
scrapy_request_url: str,
5964
scrapy_request_method: str,
@@ -89,3 +94,39 @@ async def _get_header_value(
8994
return await resource.header_value(header_name)
9095
except Exception:
9196
return None
97+
98+
99+
if platform.system() == "Windows":
100+
101+
class _WindowsAdapter:
102+
"""Utility class to redirect coroutines to an asyncio event loop running
103+
in a different thread. This allows to use a ProactorEventLoop, which is
104+
supported by Playwright on Windows.
105+
"""
106+
107+
loop = None
108+
thread = None
109+
110+
@classmethod
111+
def get_event_loop(cls) -> asyncio.AbstractEventLoop:
112+
if cls.thread is None:
113+
if cls.loop is None:
114+
policy = asyncio.WindowsProactorEventLoopPolicy() # type: ignore
115+
cls.loop = policy.new_event_loop()
116+
asyncio.set_event_loop(cls.loop)
117+
if not cls.loop.is_running():
118+
cls.thread = threading.Thread(target=cls.loop.run_forever, daemon=True)
119+
cls.thread.start()
120+
return cls.loop
121+
122+
@classmethod
123+
async def get_result(cls, o) -> concurrent.futures.Future:
124+
return asyncio.run_coroutine_threadsafe(coro=o, loop=cls.get_event_loop()).result()
125+
126+
def deferred_from_coro(o) -> Deferred:
127+
if isinstance(o, Deferred):
128+
return o
129+
return scrapy.utils.defer.deferred_from_coro(_WindowsAdapter.get_result(o))
130+
131+
else:
132+
deferred_from_coro = scrapy.utils.defer.deferred_from_coro

scrapy_playwright/handler.py

+3-35
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import asyncio
2-
import concurrent
32
import logging
4-
import platform
53
from contextlib import suppress
64
from dataclasses import dataclass
75
from ipaddress import ip_address
@@ -27,7 +25,6 @@
2725
from scrapy.http.headers import Headers
2826
from scrapy.responsetypes import responsetypes
2927
from scrapy.settings import Settings
30-
from scrapy.utils.defer import deferred_from_coro as deferred_from_coro_default
3128
from scrapy.utils.misc import load_object
3229
from scrapy.utils.reactor import verify_installed_reactor
3330
from twisted.internet.defer import Deferred, inlineCallbacks
@@ -40,48 +37,19 @@
4037
_get_page_content,
4138
_is_safe_close_error,
4239
_maybe_await,
40+
deferred_from_coro,
4341
)
4442

4543

4644
__all__ = ["ScrapyPlaywrightDownloadHandler"]
4745

4846

49-
if platform.system() == "Windows":
50-
import threading
51-
52-
class _WindowsAdapter:
53-
loop = None
54-
thread = None
55-
56-
@classmethod
57-
def get_event_loop(cls) -> asyncio.AbstractEventLoop:
58-
if cls.thread is None:
59-
if cls.loop is None:
60-
policy = asyncio.WindowsProactorEventLoopPolicy() # type: ignore
61-
cls.loop = policy.new_event_loop()
62-
asyncio.set_event_loop(cls.loop)
63-
if not cls.loop.is_running():
64-
cls.thread = threading.Thread(target=cls.loop.run_forever, daemon=True)
65-
cls.thread.start()
66-
return cls.loop
67-
68-
@classmethod
69-
async def get_result(cls, o) -> concurrent.futures.Future:
70-
return asyncio.run_coroutine_threadsafe(coro=o, loop=cls.get_event_loop()).result()
71-
72-
def deferred_from_coro(o) -> Deferred:
73-
if isinstance(o, Deferred):
74-
return o
75-
return deferred_from_coro_default(_WindowsAdapter.get_result(o))
76-
77-
else:
78-
deferred_from_coro = deferred_from_coro_default
79-
80-
8147
PlaywrightHandler = TypeVar("PlaywrightHandler", bound="ScrapyPlaywrightDownloadHandler")
8248

49+
8350
logger = logging.getLogger("scrapy-playwright")
8451

52+
8553
DEFAULT_BROWSER_TYPE = "chromium"
8654
DEFAULT_CONTEXT_NAME = "default"
8755
PERSISTENT_CONTEXT_PATH_KEY = "user_data_dir"

tests/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313

1414

1515
if platform.system() == "Windows":
16-
from scrapy_playwright.handler import _WindowsAdapter
16+
from scrapy_playwright._utils import _WindowsAdapter
1717

1818
def allow_windows(test_method):
19+
"""Wrap tests with the _WindowsAdapter class on Windows."""
1920
if not inspect.iscoroutinefunction(test_method):
2021
raise RuntimeError(f"{test_method} must be an async def method")
2122

0 commit comments

Comments
 (0)