Reactorless tests 1: the test env (#6952)

* Reactorless tests.

* Updates after merging master.

* Update a new import.

* Updates after merging master.

* Update testenv:no-reactor-pinned.

* Foundations for the reactorless mode.

* Fixes after merging master.

* Updates after merging reactorless-base.

* Add simple subprocess tests for reactorless AsyncCrawler*.

* More reactorless tests.

* Refactor AsyncCrawlerProcess.start().

* More checks.

* Fix test_reactorless_import_hook.

* More tests.

* Call install_reactor() before asyncio.run().

* Cleanup.

* Rephrase.

* Fix a test.

* Fixes.

* Rephrasing.

* Set TELNETCONSOLE_ENABLED=False in the reactorless mode.

* Fix a new import.

* Clarify --reactor marks.

* Cleanup.

* More docs/comments.

* Update test_fallback_workflow().

* Update TestDeferredFFromCoroF.

* Typing improvements.
This commit is contained in:
Andrey Rakhmatullin 2026-02-02 18:25:30 +04:00 committed by GitHub
parent 54d8562fb0
commit 3186ccf5d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 256 additions and 114 deletions

View File

@ -32,6 +32,9 @@ jobs:
- python-version: "3.13" - python-version: "3.13"
env: env:
TOXENV: default-reactor TOXENV: default-reactor
- python-version: "3.13"
env:
TOXENV: no-reactor
- python-version: pypy3.11 - python-version: pypy3.11
env: env:
TOXENV: pypy3 TOXENV: pypy3
@ -43,6 +46,9 @@ jobs:
- python-version: "3.10.19" - python-version: "3.10.19"
env: env:
TOXENV: default-reactor-pinned TOXENV: default-reactor-pinned
- python-version: "3.10.19"
env:
TOXENV: no-reactor-pinned
- python-version: pypy3.11 - python-version: pypy3.11
env: env:
TOXENV: pypy3-pinned TOXENV: pypy3-pinned

View File

@ -32,6 +32,9 @@ jobs:
- python-version: "3.13" - python-version: "3.13"
env: env:
TOXENV: default-reactor TOXENV: default-reactor
- python-version: "3.13"
env:
TOXENV: no-reactor
# pinned deps # pinned deps
- python-version: "3.10.11" - python-version: "3.10.11"

View File

@ -8,6 +8,7 @@ import pytest
from twisted.web.http import H2_ENABLED from twisted.web.http import H2_ENABLED
from scrapy.utils.reactor import set_asyncio_event_loop_policy from scrapy.utils.reactor import set_asyncio_event_loop_policy
from scrapy.utils.reactorless import install_reactor_import_hook
from tests.keys import generate_keys from tests.keys import generate_keys
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
@ -25,10 +26,6 @@ collect_ignore = [
# not a test, but looks like a test # not a test, but looks like a test
"scrapy/utils/testproc.py", "scrapy/utils/testproc.py",
"scrapy/utils/testsite.py", "scrapy/utils/testsite.py",
"tests/ftpserver.py",
"tests/mockserver.py",
"tests/pipelines.py",
"tests/spiders.py",
# contains scripts to be run by tests/test_crawler.py::AsyncCrawlerProcessSubprocess # contains scripts to be run by tests/test_crawler.py::AsyncCrawlerProcessSubprocess
*_py_files("tests/AsyncCrawlerProcess"), *_py_files("tests/AsyncCrawlerProcess"),
# contains scripts to be run by tests/test_crawler.py::AsyncCrawlerRunnerSubprocess # contains scripts to be run by tests/test_crawler.py::AsyncCrawlerRunnerSubprocess
@ -56,6 +53,17 @@ if not H2_ENABLED:
) )
def pytest_addoption(parser, pluginmanager):
if pluginmanager.hasplugin("twisted"):
return
# add the full choice set so that pytest doesn't complain about invalid choices in some cases
parser.addoption(
"--reactor",
default="none",
choices=["asyncio", "default", "none"],
)
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def mockserver() -> Generator[MockServer]: def mockserver() -> Generator[MockServer]:
with MockServer() as mockserver: with MockServer() as mockserver:
@ -72,17 +80,26 @@ def pytest_configure(config):
# Needed on Windows to switch from proactor to selector for Twisted reactor compatibility. # Needed on Windows to switch from proactor to selector for Twisted reactor compatibility.
# If we decide to run tests with both, we will need to add a new option and check it here. # If we decide to run tests with both, we will need to add a new option and check it here.
set_asyncio_event_loop_policy() set_asyncio_event_loop_policy()
elif config.getoption("--reactor") == "none":
install_reactor_import_hook()
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
# Skip tests based on reactor markers # Skip tests based on reactor markers
reactor = item.config.getoption("--reactor") reactor = item.config.getoption("--reactor")
if item.get_closest_marker("only_asyncio") and reactor != "asyncio": if item.get_closest_marker("requires_reactor") and reactor == "none":
pytest.skip("This test is only run with --reactor=asyncio") pytest.skip('This test is only run when the --reactor value is not "none"')
if item.get_closest_marker("only_not_asyncio") and reactor == "asyncio": if item.get_closest_marker("only_asyncio") and reactor not in {"asyncio", "none"}:
pytest.skip("This test is only run without --reactor=asyncio") pytest.skip(
'This test is only run when the --reactor value is "asyncio" (default) or "none"'
)
if item.get_closest_marker("only_not_asyncio") and reactor in {"asyncio", "none"}:
pytest.skip(
'This test is only run when the --reactor value is not "asyncio" (default) or "none"'
)
# Skip tests requiring optional dependencies # Skip tests requiring optional dependencies
optional_deps = [ optional_deps = [

View File

@ -227,8 +227,9 @@ addopts = [
xfail_strict = true xfail_strict = true
python_files = ["test_*.py", "test_*/__init__.py"] python_files = ["test_*.py", "test_*/__init__.py"]
markers = [ markers = [
"only_asyncio: marks tests as only enabled when --reactor=asyncio is passed", "only_asyncio: marks tests that require the asyncio loop to be used",
"only_not_asyncio: marks tests as only enabled when --reactor=asyncio is not passed", "only_not_asyncio: marks tests that require the asyncio loop to not be used",
"requires_reactor: marks tests that require a reactor",
"requires_uvloop: marks tests as only enabled when uvloop is known to be working", "requires_uvloop: marks tests as only enabled when uvloop is known to be working",
"requires_botocore: marks tests that need botocore (but not boto3)", "requires_botocore: marks tests that need botocore (but not boto3)",
"requires_boto3: marks tests that need botocore and boto3", "requires_boto3: marks tests that need botocore and boto3",

View File

@ -17,7 +17,7 @@ from unittest import mock
from twisted.trial.unittest import SkipTest from twisted.trial.unittest import SkipTest
from twisted.web.client import Agent from twisted.web.client import Agent
from scrapy.crawler import CrawlerRunner from scrapy.crawler import AsyncCrawlerRunner, CrawlerRunner, CrawlerRunnerBase
from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.utils.boto import is_botocore_available from scrapy.utils.boto import is_botocore_available
from scrapy.utils.deprecate import create_deprecated_class from scrapy.utils.deprecate import create_deprecated_class
@ -116,16 +116,24 @@ def get_reactor_settings() -> dict[str, Any]:
``Crawler._apply_settings()`` checks that the installed reactor matches the ``Crawler._apply_settings()`` checks that the installed reactor matches the
settings, so tests that run the crawler in the current process may need to settings, so tests that run the crawler in the current process may need to
pass a correct ``"TWISTED_REACTOR"`` setting value when creating it. pass a correct :setting:`TWISTED_REACTOR` setting value when creating it.
""" """
if not is_reactor_installed():
raise RuntimeError(
"get_reactor_settings() called without an installed reactor,"
" you may need to install a reactor explicitly when running your tests."
)
settings: dict[str, Any] = {} settings: dict[str, Any] = {}
if not is_asyncio_reactor_installed(): if is_reactor_installed():
settings["TWISTED_REACTOR"] = None if not is_asyncio_reactor_installed():
settings["TWISTED_REACTOR"] = None
else:
# We are either running Scrapy tests for the reactorless mode, or
# running some 3rd-party library tests for the reactorless mode, or
# running some 3rd-party library tests without initializing a reactor
# properly. The first two cases are fine, but we cannot distinguish the
# last one from them.
settings["TWISTED_ENABLED"] = False
settings["DOWNLOAD_HANDLERS"] = {
"ftp": None,
"http": None,
"https": None,
}
return settings return settings
@ -144,7 +152,11 @@ def get_crawler(
**get_reactor_settings(), **get_reactor_settings(),
**(settings_dict or {}), **(settings_dict or {}),
} }
runner = CrawlerRunner(settings) runner: CrawlerRunnerBase
if is_reactor_installed():
runner = CrawlerRunner(settings)
else:
runner = AsyncCrawlerRunner(settings)
crawler = runner.create_crawler(spidercls or DefaultSpider) crawler = runner.create_crawler(spidercls or DefaultSpider)
crawler._apply_settings() crawler._apply_settings()
return crawler return crawler

View File

@ -2,13 +2,12 @@ import itertools
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
from twisted.internet.defer import inlineCallbacks
from scrapy import Spider from scrapy import Spider
from scrapy.crawler import Crawler, CrawlerRunner from scrapy.crawler import AsyncCrawlerRunner, Crawler, CrawlerRunner
from scrapy.exceptions import NotConfigured from scrapy.exceptions import NotConfigured
from scrapy.settings import BaseSettings, Settings from scrapy.settings import BaseSettings, Settings
from scrapy.utils.test import get_crawler, get_reactor_settings from scrapy.utils.test import get_crawler, get_reactor_settings
from tests.utils.decorators import inlineCallbacks
class SimpleAddon: class SimpleAddon:
@ -109,9 +108,15 @@ class TestAddonManager:
crawler = get_crawler(settings_dict=settings_dict) crawler = get_crawler(settings_dict=settings_dict)
assert crawler.settings.getint("KEY") == 15 assert crawler.settings.getint("KEY") == 15
runner_cls = (
CrawlerRunner
if settings_dict.get("TWISTED_ENABLED", True)
else AsyncCrawlerRunner
)
settings = Settings(settings_dict) settings = Settings(settings_dict)
settings.set("KEY", 0, priority="default") settings.set("KEY", 0, priority="default")
runner = CrawlerRunner(settings) runner = runner_cls(settings)
crawler = runner.create_crawler(Spider) crawler = runner.create_crawler(Spider)
crawler._apply_settings() crawler._apply_settings()
assert crawler.settings.getint("KEY") == 15 assert crawler.settings.getint("KEY") == 15
@ -123,44 +128,39 @@ class TestAddonManager:
} }
settings = Settings(settings_dict) settings = Settings(settings_dict)
settings.set("KEY", 0, priority="default") settings.set("KEY", 0, priority="default")
runner = CrawlerRunner(settings) runner = runner_cls(settings)
crawler = runner.create_crawler(Spider) crawler = runner.create_crawler(Spider)
assert crawler.settings.getint("KEY") == 20 assert crawler.settings.getint("KEY") == 20
def test_fallback_workflow(self): def test_fallback_workflow(self):
FALLBACK_SETTING = "MY_FALLBACK_DOWNLOAD_HANDLER" FALLBACK_SETTING = "MY_FALLBACK_SCHEDULER"
class AddonWithFallback: class AddonWithFallback:
def update_settings(self, settings): def update_settings(self, settings):
if not settings.get(FALLBACK_SETTING): if not settings.get(FALLBACK_SETTING):
settings.set( settings.set(
FALLBACK_SETTING, FALLBACK_SETTING,
settings.getwithbase("DOWNLOAD_HANDLERS")["https"], settings.get("SCHEDULER"),
"addon", "addon",
) )
settings["DOWNLOAD_HANDLERS"]["https"] = "AddonHandler" settings["SCHEDULER"] = "AddonScheduler"
settings_dict = { settings_dict = {
"ADDONS": {AddonWithFallback: 1}, "ADDONS": {AddonWithFallback: 1},
} }
crawler = get_crawler(settings_dict=settings_dict) crawler = get_crawler(settings_dict=settings_dict)
assert crawler.settings.get("SCHEDULER") == "AddonScheduler"
assert ( assert (
crawler.settings.getwithbase("DOWNLOAD_HANDLERS")["https"] == "AddonHandler" crawler.settings.get(FALLBACK_SETTING) == "scrapy.core.scheduler.Scheduler"
)
assert (
crawler.settings.get(FALLBACK_SETTING)
== "scrapy.core.downloader.handlers.http11.HTTP11DownloadHandler"
) )
settings_dict = { settings_dict = {
"ADDONS": {AddonWithFallback: 1}, "ADDONS": {AddonWithFallback: 1},
"DOWNLOAD_HANDLERS": {"https": "UserHandler"}, "SCHEDULER": "UserScheduler",
} }
crawler = get_crawler(settings_dict=settings_dict) crawler = get_crawler(settings_dict=settings_dict)
assert ( assert crawler.settings.get("SCHEDULER") == "AddonScheduler"
crawler.settings.getwithbase("DOWNLOAD_HANDLERS")["https"] == "AddonHandler" assert crawler.settings.get(FALLBACK_SETTING) == "UserScheduler"
)
assert crawler.settings.get(FALLBACK_SETTING) == "UserHandler"
def test_logging_message(self): def test_logging_message(self):
class LoggedAddon: class LoggedAddon:

View File

@ -1,5 +1,3 @@
from twisted.internet.defer import inlineCallbacks
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.spiders import ( from tests.spiders import (
@ -9,6 +7,7 @@ from tests.spiders import (
MaxItemsAndRequestsSpider, MaxItemsAndRequestsSpider,
SlowSpider, SlowSpider,
) )
from tests.utils.decorators import inlineCallbacks
class TestCloseSpider: class TestCloseSpider:

View File

@ -1,7 +1,6 @@
from unittest import TextTestResult from unittest import TextTestResult
import pytest import pytest
from twisted.internet.defer import inlineCallbacks
from twisted.python import failure from twisted.python import failure
from scrapy import FormRequest from scrapy import FormRequest
@ -19,6 +18,7 @@ from scrapy.spidermiddlewares.httperror import HttpError
from scrapy.spiders import Spider from scrapy.spiders import Spider
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.utils.decorators import inlineCallbacks
class DemoItem(Item): class DemoItem(Item):

View File

@ -18,13 +18,14 @@ from scrapy.core.downloader.contextfactory import (
from scrapy.core.downloader.handlers.http11 import _RequestBodyProducer from scrapy.core.downloader.handlers.http11 import _RequestBodyProducer
from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.settings import Settings from scrapy.settings import Settings
from scrapy.utils.defer import deferred_f_from_coro_f, maybe_deferred_to_future from scrapy.utils.defer import maybe_deferred_to_future
from scrapy.utils.misc import build_from_crawler from scrapy.utils.misc import build_from_crawler
from scrapy.utils.python import to_bytes from scrapy.utils.python import to_bytes
from scrapy.utils.spider import DefaultSpider from scrapy.utils.spider import DefaultSpider
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http_resources import PayloadResource from tests.mockserver.http_resources import PayloadResource
from tests.mockserver.utils import ssl_context_factory from tests.mockserver.utils import ssl_context_factory
from tests.utils.decorators import deferred_f_from_coro_f
if TYPE_CHECKING: if TYPE_CHECKING:
from twisted.internet.defer import Deferred from twisted.internet.defer import Deferred

View File

@ -2,9 +2,9 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from scrapy.utils.defer import deferred_f_from_coro_f
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.spiders import SimpleSpider from tests.spiders import SimpleSpider
from tests.utils.decorators import deferred_f_from_coro_f
if TYPE_CHECKING: if TYPE_CHECKING:
import pytest import pytest

View File

@ -9,7 +9,7 @@ from urllib.parse import urlencode, urlparse
import pytest import pytest
from testfixtures import LogCapture from testfixtures import LogCapture
from twisted.internet.defer import inlineCallbacks, succeed from twisted.internet.defer import succeed
from twisted.internet.ssl import Certificate from twisted.internet.ssl import Certificate
from twisted.python.failure import Failure from twisted.python.failure import Failure
@ -18,7 +18,7 @@ from scrapy.crawler import CrawlerRunner
from scrapy.exceptions import CloseSpider, ScrapyDeprecationWarning, StopDownload from scrapy.exceptions import CloseSpider, ScrapyDeprecationWarning, StopDownload
from scrapy.http import Request from scrapy.http import Request
from scrapy.http.response import Response from scrapy.http.response import Response
from scrapy.utils.defer import deferred_f_from_coro_f, maybe_deferred_to_future from scrapy.utils.defer import maybe_deferred_to_future
from scrapy.utils.engine import format_engine_status, get_engine_status from scrapy.utils.engine import format_engine_status, get_engine_status
from scrapy.utils.python import to_unicode from scrapy.utils.python import to_unicode
from scrapy.utils.test import get_crawler, get_reactor_settings from scrapy.utils.test import get_crawler, get_reactor_settings
@ -55,6 +55,7 @@ from tests.spiders import (
StartGoodAndBadOutput, StartGoodAndBadOutput,
StartItemSpider, StartItemSpider,
) )
from tests.utils.decorators import deferred_f_from_coro_f, inlineCallbacks
if TYPE_CHECKING: if TYPE_CHECKING:
from scrapy.statscollectors import StatsCollector from scrapy.statscollectors import StatsCollector

View File

@ -14,7 +14,7 @@ from typing import Any
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
from pexpect.popen_spawn import PopenSpawn from pexpect.popen_spawn import PopenSpawn
from twisted.internet.defer import Deferred, inlineCallbacks from twisted.internet.defer import Deferred
from w3lib import __version__ as w3lib_version from w3lib import __version__ as w3lib_version
from zope.interface.exceptions import MultipleInvalid from zope.interface.exceptions import MultipleInvalid
@ -31,11 +31,7 @@ from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.extensions.throttle import AutoThrottle from scrapy.extensions.throttle import AutoThrottle
from scrapy.settings import Settings, default_settings from scrapy.settings import Settings, default_settings
from scrapy.utils.asyncio import call_later from scrapy.utils.asyncio import call_later
from scrapy.utils.defer import ( from scrapy.utils.defer import deferred_from_coro, maybe_deferred_to_future
deferred_f_from_coro_f,
deferred_from_coro,
maybe_deferred_to_future,
)
from scrapy.utils.log import ( from scrapy.utils.log import (
_uninstall_scrapy_root_handler, _uninstall_scrapy_root_handler,
configure_logging, configure_logging,
@ -45,6 +41,7 @@ from scrapy.utils.spider import DefaultSpider
from scrapy.utils.test import get_crawler, get_reactor_settings from scrapy.utils.test import get_crawler, get_reactor_settings
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.utils import get_script_run_env from tests.utils import get_script_run_env
from tests.utils.decorators import deferred_f_from_coro_f, inlineCallbacks
BASE_SETTINGS: dict[str, Any] = {} BASE_SETTINGS: dict[str, Any] = {}

View File

@ -25,6 +25,9 @@ if TYPE_CHECKING:
from twisted.protocols.ftp import FTPFactory from twisted.protocols.ftp import FTPFactory
pytestmark = pytest.mark.requires_reactor
class TestFTPBase(ABC): class TestFTPBase(ABC):
username = "scrapy" username = "scrapy"
password = "passwd" password = "passwd"

View File

@ -16,6 +16,9 @@ if TYPE_CHECKING:
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
pytestmark = pytest.mark.requires_reactor
class HTTP10DownloadHandlerMixin: class HTTP10DownloadHandlerMixin:
@property @property
def download_handler_cls(self) -> type[DownloadHandlerProtocol]: def download_handler_cls(self) -> type[DownloadHandlerProtocol]:

View File

@ -4,6 +4,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import pytest
from scrapy.core.downloader.handlers.http11 import HTTP11DownloadHandler from scrapy.core.downloader.handlers.http11 import HTTP11DownloadHandler
from tests.test_downloader_handlers_http_base import ( from tests.test_downloader_handlers_http_base import (
TestHttp11Base, TestHttp11Base,
@ -21,6 +23,9 @@ if TYPE_CHECKING:
from scrapy.core.downloader.handlers import DownloadHandlerProtocol from scrapy.core.downloader.handlers import DownloadHandlerProtocol
pytestmark = pytest.mark.requires_reactor
class HTTP11DownloadHandlerMixin: class HTTP11DownloadHandlerMixin:
@property @property
def download_handler_cls(self) -> type[DownloadHandlerProtocol]: def download_handler_cls(self) -> type[DownloadHandlerProtocol]:

View File

@ -27,9 +27,13 @@ if TYPE_CHECKING:
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.mockserver.proxy_echo import ProxyEchoMockServer from tests.mockserver.proxy_echo import ProxyEchoMockServer
pytestmark = pytest.mark.skipif(
not H2_ENABLED, reason="HTTP/2 support in Twisted is not enabled" pytestmark = [
) pytest.mark.requires_reactor,
pytest.mark.skipif(
not H2_ENABLED, reason="HTTP/2 support in Twisted is not enabled"
),
]
class H2DownloadHandlerMixin: class H2DownloadHandlerMixin:

View File

@ -19,9 +19,9 @@ from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning
from scrapy.http import Request from scrapy.http import Request
from scrapy.responsetypes import responsetypes from scrapy.responsetypes import responsetypes
from scrapy.utils.boto import is_botocore_available from scrapy.utils.boto import is_botocore_available
from scrapy.utils.defer import deferred_f_from_coro_f
from scrapy.utils.misc import build_from_crawler from scrapy.utils.misc import build_from_crawler
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.utils.decorators import deferred_f_from_coro_f
class DummyDH: class DummyDH:

View File

@ -26,11 +26,7 @@ from scrapy.exceptions import (
UnsupportedURLSchemeError, UnsupportedURLSchemeError,
) )
from scrapy.http import Headers, HtmlResponse, Request, Response, TextResponse from scrapy.http import Headers, HtmlResponse, Request, Response, TextResponse
from scrapy.utils.defer import ( from scrapy.utils.defer import deferred_from_coro, maybe_deferred_to_future
deferred_f_from_coro_f,
deferred_from_coro,
maybe_deferred_to_future,
)
from scrapy.utils.misc import build_from_crawler from scrapy.utils.misc import build_from_crawler
from scrapy.utils.spider import DefaultSpider from scrapy.utils.spider import DefaultSpider
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
@ -38,6 +34,7 @@ from tests import NON_EXISTING_RESOLVABLE
from tests.mockserver.proxy_echo import ProxyEchoMockServer from tests.mockserver.proxy_echo import ProxyEchoMockServer
from tests.mockserver.simple_https import SimpleMockServer from tests.mockserver.simple_https import SimpleMockServer
from tests.spiders import SingleRequestSpider from tests.spiders import SingleRequestSpider
from tests.utils.decorators import deferred_f_from_coro_f
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import AsyncGenerator, Generator from collections.abc import AsyncGenerator, Generator

View File

@ -13,9 +13,10 @@ from scrapy.core.downloader.middleware import DownloaderMiddlewareManager
from scrapy.exceptions import ScrapyDeprecationWarning, _InvalidOutput from scrapy.exceptions import ScrapyDeprecationWarning, _InvalidOutput
from scrapy.http import Request, Response from scrapy.http import Request, Response
from scrapy.spiders import Spider from scrapy.spiders import Spider
from scrapy.utils.defer import deferred_f_from_coro_f, maybe_deferred_to_future from scrapy.utils.defer import maybe_deferred_to_future
from scrapy.utils.python import to_bytes from scrapy.utils.python import to_bytes
from scrapy.utils.test import get_crawler, get_from_asyncio_queue from scrapy.utils.test import get_crawler, get_from_asyncio_queue
from tests.utils.decorators import deferred_f_from_coro_f
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator

View File

@ -14,12 +14,9 @@ from scrapy.http import Request, Response, TextResponse
from scrapy.http.request import NO_CALLBACK from scrapy.http.request import NO_CALLBACK
from scrapy.settings import Settings from scrapy.settings import Settings
from scrapy.utils.asyncio import call_later from scrapy.utils.asyncio import call_later
from scrapy.utils.defer import ( from scrapy.utils.defer import deferred_from_coro, maybe_deferred_to_future
deferred_f_from_coro_f,
deferred_from_coro,
maybe_deferred_to_future,
)
from tests.test_robotstxt_interface import rerp_available from tests.test_robotstxt_interface import rerp_available
from tests.utils.decorators import deferred_f_from_coro_f
if TYPE_CHECKING: if TYPE_CHECKING:
from scrapy.crawler import Crawler from scrapy.crawler import Crawler

View File

@ -2,17 +2,16 @@ import time
from typing import Any from typing import Any
import pytest import pytest
from twisted.internet.defer import inlineCallbacks
from scrapy import Request from scrapy import Request
from scrapy.core.downloader import Downloader, Slot from scrapy.core.downloader import Downloader, Slot
from scrapy.crawler import CrawlerRunner from scrapy.crawler import CrawlerRunner
from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.utils.defer import deferred_f_from_coro_f
from scrapy.utils.spider import DefaultSpider from scrapy.utils.spider import DefaultSpider
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.spiders import MetaSpider from tests.spiders import MetaSpider
from tests.utils.decorators import deferred_f_from_coro_f, inlineCallbacks
class DownloaderSlotsSettingsTestSpider(MetaSpider): class DownloaderSlotsSettingsTestSpider(MetaSpider):
@ -85,6 +84,7 @@ class TestCrawl:
assert max(list(error_delta.values())) < tolerance assert max(list(error_delta.values())) < tolerance
@pytest.mark.requires_reactor # needs a reactor or an event loop for Downloader._slot_gc_loop
def test_params(): def test_params():
params = { params = {
"concurrency": 1, "concurrency": 1,
@ -109,6 +109,7 @@ def test_params():
) )
@pytest.mark.requires_reactor # needs a reactor or an event loop for Downloader._slot_gc_loop
def test_get_slot_deprecated_spider_arg(): def test_get_slot_deprecated_spider_arg():
crawler = get_crawler(DefaultSpider) crawler = get_crawler(DefaultSpider)
crawler.spider = crawler._create_spider() crawler.spider = crawler._create_spider()

View File

@ -17,7 +17,6 @@ from itemadapter import ItemAdapter
from pydispatch import dispatcher from pydispatch import dispatcher
from testfixtures import LogCapture from testfixtures import LogCapture
from twisted.internet import defer from twisted.internet import defer
from twisted.internet.defer import inlineCallbacks
from scrapy import signals from scrapy import signals
from scrapy.core.engine import ExecutionEngine, _Slot from scrapy.core.engine import ExecutionEngine, _Slot
@ -29,7 +28,6 @@ from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import Spider from scrapy.spiders import Spider
from scrapy.utils.defer import ( from scrapy.utils.defer import (
_schedule_coro, _schedule_coro,
deferred_f_from_coro_f,
deferred_from_coro, deferred_from_coro,
maybe_deferred_to_future, maybe_deferred_to_future,
) )
@ -37,6 +35,7 @@ from scrapy.utils.signal import disconnect_all
from scrapy.utils.spider import DefaultSpider from scrapy.utils.spider import DefaultSpider
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests import get_testdata from tests import get_testdata
from tests.utils.decorators import deferred_f_from_coro_f, inlineCallbacks
if TYPE_CHECKING: if TYPE_CHECKING:
from scrapy.core.scheduler import Scheduler from scrapy.core.scheduler import Scheduler
@ -599,6 +598,7 @@ class TestEngineDownload(TestEngineDownloadAsync):
return await maybe_deferred_to_future(engine.download(request)) return await maybe_deferred_to_future(engine.download(request))
@pytest.mark.requires_reactor # needs a reactor or an event loop for _Slot.heartbeat
def test_request_scheduled_signal(caplog): def test_request_scheduled_signal(caplog):
class TestScheduler(BaseScheduler): class TestScheduler(BaseScheduler):
def __init__(self): def __init__(self):

View File

@ -7,10 +7,11 @@ from typing import TYPE_CHECKING
from twisted.internet.defer import Deferred from twisted.internet.defer import Deferred
from scrapy import Request, Spider, signals from scrapy import Request, Spider, signals
from scrapy.utils.defer import deferred_f_from_coro_f, maybe_deferred_to_future from scrapy.utils.defer import maybe_deferred_to_future
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.test_scheduler import MemoryScheduler from tests.test_scheduler import MemoryScheduler
from tests.utils.decorators import deferred_f_from_coro_f
if TYPE_CHECKING: if TYPE_CHECKING:
import pytest import pytest

View File

@ -3,7 +3,6 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from scrapy.exceptions import StopDownload from scrapy.exceptions import StopDownload
from scrapy.utils.defer import deferred_f_from_coro_f
from tests.test_engine import ( from tests.test_engine import (
AttrsItemsSpider, AttrsItemsSpider,
CrawlerRun, CrawlerRun,
@ -12,6 +11,7 @@ from tests.test_engine import (
MySpider, MySpider,
TestEngineBase, TestEngineBase,
) )
from tests.utils.decorators import deferred_f_from_coro_f
if TYPE_CHECKING: if TYPE_CHECKING:
import pytest import pytest

View File

@ -3,7 +3,6 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from scrapy.exceptions import StopDownload from scrapy.exceptions import StopDownload
from scrapy.utils.defer import deferred_f_from_coro_f
from tests.test_engine import ( from tests.test_engine import (
AttrsItemsSpider, AttrsItemsSpider,
CrawlerRun, CrawlerRun,
@ -12,6 +11,7 @@ from tests.test_engine import (
MySpider, MySpider,
TestEngineBase, TestEngineBase,
) )
from tests.utils.decorators import deferred_f_from_coro_f
if TYPE_CHECKING: if TYPE_CHECKING:
import pytest import pytest

View File

@ -3,6 +3,8 @@ from __future__ import annotations
import datetime import datetime
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import pytest
from scrapy.extensions.periodic_log import PeriodicLog from scrapy.extensions.periodic_log import PeriodicLog
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
@ -86,6 +88,7 @@ class TestPeriodicLog:
assert extension({"PERIODIC_LOG_DELTA": True, "LOGSTATS_INTERVAL": 60}) assert extension({"PERIODIC_LOG_DELTA": True, "LOGSTATS_INTERVAL": 60})
assert extension({"PERIODIC_LOG_DELTA": "True", "LOGSTATS_INTERVAL": 60}) assert extension({"PERIODIC_LOG_DELTA": "True", "LOGSTATS_INTERVAL": 60})
@pytest.mark.requires_reactor # needs a reactor or an event loop for PeriodicLog.task
def test_log_delta(self): def test_log_delta(self):
def emulate(settings=None): def emulate(settings=None):
spider = MetaSpider() spider = MetaSpider()
@ -143,6 +146,7 @@ class TestPeriodicLog:
and ("downloader/" in k and "bytes" not in k), and ("downloader/" in k and "bytes" not in k),
) )
@pytest.mark.requires_reactor # needs a reactor or an event loop for PeriodicLog.task
def test_log_stats(self): def test_log_stats(self):
def emulate(settings=None): def emulate(settings=None):
spider = MetaSpider() spider = MetaSpider()

View File

@ -1,10 +1,10 @@
import pytest import pytest
from twisted.conch.telnet import ITelnetProtocol from twisted.conch.telnet import ITelnetProtocol
from twisted.cred import credentials from twisted.cred import credentials
from twisted.internet.defer import inlineCallbacks
from scrapy.extensions.telnet import TelnetConsole from scrapy.extensions.telnet import TelnetConsole
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.utils.decorators import inlineCallbacks
class TestTelnetExtension: class TestTelnetExtension:

View File

@ -29,7 +29,6 @@ import lxml.etree
import pytest import pytest
from packaging.version import Version from packaging.version import Version
from testfixtures import LogCapture from testfixtures import LogCapture
from twisted.internet.defer import inlineCallbacks
from w3lib.url import file_uri_to_path, path_to_file_uri from w3lib.url import file_uri_to_path, path_to_file_uri
from zope.interface import implementer from zope.interface import implementer
from zope.interface.verify import verifyObject from zope.interface.verify import verifyObject
@ -50,12 +49,13 @@ from scrapy.extensions.feedexport import (
StdoutFeedStorage, StdoutFeedStorage,
) )
from scrapy.settings import Settings from scrapy.settings import Settings
from scrapy.utils.defer import deferred_f_from_coro_f, maybe_deferred_to_future from scrapy.utils.defer import maybe_deferred_to_future
from scrapy.utils.python import to_unicode from scrapy.utils.python import to_unicode
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.ftp import MockFTPServer from tests.mockserver.ftp import MockFTPServer
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.spiders import ItemSpider from tests.spiders import ItemSpider
from tests.utils.decorators import deferred_f_from_coro_f, inlineCallbacks
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable

View File

@ -39,9 +39,12 @@ if TYPE_CHECKING:
from scrapy.core.http2.protocol import H2ClientProtocol from scrapy.core.http2.protocol import H2ClientProtocol
pytestmark = pytest.mark.skipif( pytestmark = [
not H2_ENABLED, reason="HTTP/2 support in Twisted is not enabled" pytest.mark.requires_reactor,
) pytest.mark.skipif(
not H2_ENABLED, reason="HTTP/2 support in Twisted is not enabled"
),
]
def generate_random_string(size: int) -> str: def generate_random_string(size: int) -> str:

View File

@ -2,7 +2,6 @@ import logging
import pytest import pytest
from testfixtures import LogCapture from testfixtures import LogCapture
from twisted.internet.defer import inlineCallbacks
from twisted.python.failure import Failure from twisted.python.failure import Failure
from scrapy.exceptions import DropItem from scrapy.exceptions import DropItem
@ -13,6 +12,7 @@ from scrapy.spiders import Spider
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.spiders import ItemSpider from tests.spiders import ItemSpider
from tests.utils.decorators import inlineCallbacks
class CustomItem(Item): class CustomItem(Item):

View File

@ -16,6 +16,7 @@ class TestLogStats:
self.stats.set_value("response_received_count", 4802) self.stats.set_value("response_received_count", 4802)
self.stats.set_value("item_scraped_count", 3201) self.stats.set_value("item_scraped_count", 3201)
@pytest.mark.requires_reactor # needs a reactor or an event loop for LogStats.task
def test_stats_calculations(self): def test_stats_calculations(self):
logstats = LogStats.from_crawler(self.crawler) logstats = LogStats.from_crawler(self.crawler)

View File

@ -1,12 +1,14 @@
from email.charset import Charset from email.charset import Charset
from io import BytesIO from io import BytesIO
import pytest
from twisted.internet import defer from twisted.internet import defer
from twisted.internet._sslverify import ClientTLSOptions from twisted.internet._sslverify import ClientTLSOptions
from scrapy.mail import MailSender from scrapy.mail import MailSender
@pytest.mark.requires_reactor
class TestMailSender: class TestMailSender:
def test_send(self): def test_send(self):
mailsender = MailSender(debug=True) mailsender = MailSender(debug=True)

View File

@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Any
import pytest import pytest
from testfixtures import LogCapture from testfixtures import LogCapture
from twisted.internet.defer import inlineCallbacks
from w3lib.url import add_or_replace_parameter from w3lib.url import add_or_replace_parameter
from scrapy import Spider, signals from scrapy import Spider, signals
@ -15,6 +14,7 @@ from scrapy.utils.misc import load_object
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.spiders import SimpleSpider from tests.spiders import SimpleSpider
from tests.utils.decorators import inlineCallbacks
if TYPE_CHECKING: if TYPE_CHECKING:
from scrapy.crawler import Crawler from scrapy.crawler import Crawler

View File

@ -19,7 +19,6 @@ from urllib.parse import urlparse
import attr import attr
import pytest import pytest
from itemadapter import ItemAdapter from itemadapter import ItemAdapter
from twisted.internet.defer import inlineCallbacks
from scrapy.exceptions import NotConfigured from scrapy.exceptions import NotConfigured
from scrapy.http import Request, Response from scrapy.http import Request, Response
@ -32,10 +31,10 @@ from scrapy.pipelines.files import (
S3FilesStore, S3FilesStore,
) )
from scrapy.settings import Settings from scrapy.settings import Settings
from scrapy.utils.defer import deferred_f_from_coro_f
from scrapy.utils.spider import DefaultSpider from scrapy.utils.spider import DefaultSpider
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.ftp import MockFTPServer from tests.mockserver.ftp import MockFTPServer
from tests.utils.decorators import deferred_f_from_coro_f, inlineCallbacks
from .test_pipeline_media import _mocked_download_func from .test_pipeline_media import _mocked_download_func

View File

@ -13,11 +13,12 @@ from scrapy.http import Request, Response
from scrapy.http.request import NO_CALLBACK from scrapy.http.request import NO_CALLBACK
from scrapy.pipelines.files import FileException from scrapy.pipelines.files import FileException
from scrapy.pipelines.media import MediaPipeline from scrapy.pipelines.media import MediaPipeline
from scrapy.utils.defer import _defer_sleep_async, deferred_f_from_coro_f from scrapy.utils.defer import _defer_sleep_async
from scrapy.utils.log import failure_to_exc_info from scrapy.utils.log import failure_to_exc_info
from scrapy.utils.signal import disconnect_all from scrapy.utils.signal import disconnect_all
from scrapy.utils.spider import DefaultSpider from scrapy.utils.spider import DefaultSpider
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.utils.decorators import deferred_f_from_coro_f
async def _mocked_download_func(request): async def _mocked_download_func(request):

View File

@ -10,14 +10,11 @@ from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.pipelines import ItemPipelineManager from scrapy.pipelines import ItemPipelineManager
from scrapy.utils.asyncio import call_later from scrapy.utils.asyncio import call_later
from scrapy.utils.conf import build_component_list from scrapy.utils.conf import build_component_list
from scrapy.utils.defer import ( from scrapy.utils.defer import deferred_to_future, maybe_deferred_to_future
deferred_f_from_coro_f,
deferred_to_future,
maybe_deferred_to_future,
)
from scrapy.utils.spider import DefaultSpider from scrapy.utils.spider import DefaultSpider
from scrapy.utils.test import get_crawler, get_from_asyncio_queue from scrapy.utils.test import get_crawler, get_from_asyncio_queue
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.utils.decorators import deferred_f_from_coro_f
class SimplePipeline: class SimplePipeline:
@ -251,6 +248,9 @@ class TestPipeline:
class TestCustomPipelineManager: class TestCustomPipelineManager:
# needs a reactor or an event loop for is_asyncio_available()
# (for ItemPipelineManager.process_item())
@pytest.mark.requires_reactor
def test_deprecated_process_item_spider_arg(self) -> None: def test_deprecated_process_item_spider_arg(self) -> None:
class CustomPipelineManager(ItemPipelineManager): class CustomPipelineManager(ItemPipelineManager):
def process_item(self, item, spider): # pylint: disable=useless-parent-delegation def process_item(self, item, spider): # pylint: disable=useless-parent-delegation

View File

@ -8,12 +8,12 @@ from urllib.parse import urlsplit, urlunsplit
import pytest import pytest
from testfixtures import LogCapture from testfixtures import LogCapture
from twisted.internet.defer import inlineCallbacks
from scrapy.http import Request from scrapy.http import Request
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.spiders import SimpleSpider, SingleRequestSpider from tests.spiders import SimpleSpider, SingleRequestSpider
from tests.utils.decorators import inlineCallbacks
class MitmProxy: class MitmProxy:

View File

@ -1,11 +1,11 @@
from testfixtures import LogCapture from testfixtures import LogCapture
from twisted.internet.defer import inlineCallbacks
from scrapy import Request, signals from scrapy import Request, signals
from scrapy.http.response import Response from scrapy.http.response import Response
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.spiders import SingleRequestSpider from tests.spiders import SingleRequestSpider
from tests.utils.decorators import inlineCallbacks
OVERRIDDEN_URL = "https://example.org" OVERRIDDEN_URL = "https://example.org"

View File

@ -1,10 +1,10 @@
from testfixtures import LogCapture from testfixtures import LogCapture
from twisted.internet.defer import inlineCallbacks
from scrapy.http import Request from scrapy.http import Request
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.spiders import MockServerSpider from tests.spiders import MockServerSpider
from tests.utils.decorators import inlineCallbacks
class InjectArgumentsDownloaderMiddleware: class InjectArgumentsDownloaderMiddleware:

View File

@ -1,9 +1,8 @@
from twisted.internet.defer import inlineCallbacks
from scrapy.signals import request_left_downloader from scrapy.signals import request_left_downloader
from scrapy.spiders import Spider from scrapy.spiders import Spider
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.utils.decorators import inlineCallbacks
class SignalCatcherSpider(Spider): class SignalCatcherSpider(Spider):

View File

@ -8,7 +8,6 @@ from collections import deque
from typing import Any, NamedTuple from typing import Any, NamedTuple
import pytest import pytest
from twisted.internet.defer import inlineCallbacks
from scrapy.core.downloader import Downloader from scrapy.core.downloader import Downloader
from scrapy.core.scheduler import BaseScheduler, Scheduler from scrapy.core.scheduler import BaseScheduler, Scheduler
@ -20,6 +19,7 @@ from scrapy.utils.httpobj import urlparse_cached
from scrapy.utils.misc import load_object from scrapy.utils.misc import load_object
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.utils.decorators import inlineCallbacks
class MemoryScheduler(BaseScheduler): class MemoryScheduler(BaseScheduler):
@ -99,6 +99,9 @@ class MockCrawler(Crawler):
self.stats = load_object(self.settings["STATS_CLASS"])(self) self.stats = load_object(self.settings["STATS_CLASS"])(self)
# needs a reactor or an event loop for is_asyncio_available()
# (for _schedule_coro())
@pytest.mark.requires_reactor
class SchedulerHandler(ABC): class SchedulerHandler(ABC):
jobdir = None jobdir = None
@ -243,6 +246,9 @@ _URLS_WITH_SLOTS = [
class TestMigration: class TestMigration:
# needs a reactor or an event loop for is_asyncio_available()
# (for _schedule_coro())
@pytest.mark.requires_reactor
def test_migration(self, tmpdir): def test_migration(self, tmpdir):
class PrevSchedulerHandler(SchedulerHandler): class PrevSchedulerHandler(SchedulerHandler):
jobdir = tmpdir jobdir = tmpdir

View File

@ -5,7 +5,6 @@ from urllib.parse import urljoin
import pytest import pytest
from testfixtures import LogCapture from testfixtures import LogCapture
from twisted.internet import defer from twisted.internet import defer
from twisted.internet.defer import inlineCallbacks
from scrapy.core.scheduler import BaseScheduler from scrapy.core.scheduler import BaseScheduler
from scrapy.http import Request from scrapy.http import Request
@ -14,6 +13,7 @@ from scrapy.utils.httpobj import urlparse_cached
from scrapy.utils.request import fingerprint from scrapy.utils.request import fingerprint
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.utils.decorators import inlineCallbacks
PATHS = ["/a", "/b", "/c"] PATHS = ["/a", "/b", "/c"]
URLS = [urljoin("https://example.org", p) for p in PATHS] URLS = [urljoin("https://example.org", p) for p in PATHS]

View File

@ -1,10 +1,9 @@
import pytest import pytest
from twisted.internet.defer import inlineCallbacks
from scrapy import Request, Spider, signals from scrapy import Request, Spider, signals
from scrapy.utils.defer import deferred_f_from_coro_f
from scrapy.utils.test import get_crawler, get_from_asyncio_queue from scrapy.utils.test import get_crawler, get_from_asyncio_queue
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.utils.decorators import deferred_f_from_coro_f, inlineCallbacks
class ItemSpider(Spider): class ItemSpider(Spider):

View File

@ -12,7 +12,6 @@ from unittest import mock
import pytest import pytest
from testfixtures import LogCapture from testfixtures import LogCapture
from twisted.internet.defer import inlineCallbacks
from w3lib.url import safe_url_string from w3lib.url import safe_url_string
from scrapy import signals from scrapy import signals
@ -29,9 +28,9 @@ from scrapy.spiders import (
XMLFeedSpider, XMLFeedSpider,
) )
from scrapy.spiders.init import InitSpider from scrapy.spiders.init import InitSpider
from scrapy.utils.defer import deferred_f_from_coro_f
from scrapy.utils.test import get_crawler, get_reactor_settings from scrapy.utils.test import get_crawler, get_reactor_settings
from tests import get_testdata, tests_datadir from tests import get_testdata, tests_datadir
from tests.utils.decorators import deferred_f_from_coro_f, inlineCallbacks
class TestSpider: class TestSpider:

View File

@ -9,10 +9,11 @@ from testfixtures import LogCapture
from scrapy import Spider, signals from scrapy import Spider, signals
from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.utils.defer import deferred_f_from_coro_f, maybe_deferred_to_future from scrapy.utils.defer import maybe_deferred_to_future
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from .utils import twisted_sleep from .utils import twisted_sleep
from .utils.decorators import deferred_f_from_coro_f
SLEEP_SECONDS = 0.1 SLEEP_SECONDS = 0.1

View File

@ -15,9 +15,10 @@ from scrapy.http import Request, Response
from scrapy.spiders import Spider from scrapy.spiders import Spider
from scrapy.utils.asyncgen import collect_asyncgen from scrapy.utils.asyncgen import collect_asyncgen
from scrapy.utils.asyncio import call_later from scrapy.utils.asyncio import call_later
from scrapy.utils.defer import deferred_f_from_coro_f, maybe_deferred_to_future from scrapy.utils.defer import maybe_deferred_to_future
from scrapy.utils.spider import DefaultSpider from scrapy.utils.spider import DefaultSpider
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.utils.decorators import deferred_f_from_coro_f
if TYPE_CHECKING: if TYPE_CHECKING:
from twisted.python.failure import Failure from twisted.python.failure import Failure

View File

@ -4,7 +4,6 @@ import logging
import pytest import pytest
from testfixtures import LogCapture from testfixtures import LogCapture
from twisted.internet.defer import inlineCallbacks
from scrapy.http import Request, Response from scrapy.http import Request, Response
from scrapy.spidermiddlewares.httperror import HttpError, HttpErrorMiddleware from scrapy.spidermiddlewares.httperror import HttpError, HttpErrorMiddleware
@ -12,6 +11,7 @@ from scrapy.utils.spider import DefaultSpider
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.spiders import MockServerSpider from tests.spiders import MockServerSpider
from tests.utils.decorators import inlineCallbacks
class _HttpErrorSpider(MockServerSpider): class _HttpErrorSpider(MockServerSpider):

View File

@ -1,9 +1,9 @@
from testfixtures import LogCapture from testfixtures import LogCapture
from scrapy import Request, Spider from scrapy import Request, Spider
from scrapy.utils.defer import deferred_f_from_coro_f
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
from tests.utils.decorators import deferred_f_from_coro_f
class _BaseSpiderMiddleware: class _BaseSpiderMiddleware:

View File

@ -5,11 +5,12 @@ import pytest
from scrapy import Spider, signals from scrapy import Spider, signals
from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.utils.defer import deferred_f_from_coro_f, maybe_deferred_to_future from scrapy.utils.defer import maybe_deferred_to_future
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.test_spider_start import SLEEP_SECONDS from tests.test_spider_start import SLEEP_SECONDS
from .utils import twisted_sleep from .utils import twisted_sleep
from .utils.decorators import deferred_f_from_coro_f
ITEM_A = {"id": "a"} ITEM_A = {"id": "a"}
ITEM_B = {"id": "b"} ITEM_B = {"id": "b"}

View File

@ -1,9 +1,9 @@
from scrapy.http import Request from scrapy.http import Request
from scrapy.spidermiddlewares.start import StartSpiderMiddleware from scrapy.spidermiddlewares.start import StartSpiderMiddleware
from scrapy.spiders import Spider from scrapy.spiders import Spider
from scrapy.utils.defer import deferred_f_from_coro_f
from scrapy.utils.misc import build_from_crawler from scrapy.utils.misc import build_from_crawler
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.utils.decorators import deferred_f_from_coro_f
class TestMiddleware: class TestMiddleware:

View File

@ -10,9 +10,9 @@ from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.extensions.corestats import CoreStats from scrapy.extensions.corestats import CoreStats
from scrapy.spiders import Spider from scrapy.spiders import Spider
from scrapy.statscollectors import DummyStatsCollector, StatsCollector from scrapy.statscollectors import DummyStatsCollector, StatsCollector
from scrapy.utils.defer import deferred_f_from_coro_f
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
from tests.spiders import SimpleSpider from tests.spiders import SimpleSpider
from tests.utils.decorators import deferred_f_from_coro_f
if TYPE_CHECKING: if TYPE_CHECKING:
from scrapy.crawler import Crawler from scrapy.crawler import Crawler

View File

@ -1,5 +1,5 @@
from scrapy.utils.asyncgen import as_async_generator, collect_asyncgen from scrapy.utils.asyncgen import as_async_generator, collect_asyncgen
from scrapy.utils.defer import deferred_f_from_coro_f from tests.utils.decorators import deferred_f_from_coro_f
class TestAsyncgenUtils: class TestAsyncgenUtils:

View File

@ -14,13 +14,14 @@ from scrapy.utils.asyncio import (
_parallel_asyncio, _parallel_asyncio,
is_asyncio_available, is_asyncio_available,
) )
from scrapy.utils.defer import deferred_f_from_coro_f from tests.utils.decorators import deferred_f_from_coro_f
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
class TestAsyncio: class TestAsyncio:
@pytest.mark.requires_reactor # needs a reactor or an event loop for is_asyncio_available()
def test_is_asyncio_available(self, reactor_pytest: str) -> None: def test_is_asyncio_available(self, reactor_pytest: str) -> None:
# the result should depend only on the pytest --reactor argument # the result should depend only on the pytest --reactor argument
assert is_asyncio_available() == (reactor_pytest == "asyncio") assert is_asyncio_available() == (reactor_pytest == "asyncio")
@ -103,6 +104,7 @@ class TestParallelAsyncio:
assert max_parallel_count[0] <= self.CONCURRENT_ITEMS assert max_parallel_count[0] <= self.CONCURRENT_ITEMS
@pytest.mark.requires_reactor # needs a running event loop for AsyncioLoopingCall.start()
@pytest.mark.only_asyncio @pytest.mark.only_asyncio
class TestAsyncioLoopingCall: class TestAsyncioLoopingCall:
def test_looping_call(self): def test_looping_call(self):

View File

@ -6,7 +6,8 @@ from asyncio import Future
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import pytest import pytest
from twisted.internet.defer import Deferred, inlineCallbacks, succeed from twisted.internet.defer import Deferred, succeed
from twisted.internet.defer import inlineCallbacks as inlineCallbacks_orig
from scrapy.utils.asyncgen import as_async_generator, collect_asyncgen from scrapy.utils.asyncgen import as_async_generator, collect_asyncgen
from scrapy.utils.defer import ( from scrapy.utils.defer import (
@ -19,6 +20,7 @@ from scrapy.utils.defer import (
mustbe_deferred, mustbe_deferred,
parallel_async, parallel_async,
) )
from tests.utils.decorators import inlineCallbacks
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import AsyncGenerator, Awaitable, Callable, Generator from collections.abc import AsyncGenerator, Awaitable, Callable, Generator
@ -107,6 +109,7 @@ class TestIterErrback:
assert isinstance(errors[0].value, ZeroDivisionError) assert isinstance(errors[0].value, ZeroDivisionError)
@pytest.mark.requires_reactor
class TestAiterErrback: class TestAiterErrback:
@deferred_f_from_coro_f @deferred_f_from_coro_f
async def test_aiter_errback_good(self): async def test_aiter_errback_good(self):
@ -134,6 +137,7 @@ class TestAiterErrback:
assert isinstance(errors[0].value, ZeroDivisionError) assert isinstance(errors[0].value, ZeroDivisionError)
@pytest.mark.requires_reactor
class TestAsyncDefTestsuite: class TestAsyncDefTestsuite:
@deferred_f_from_coro_f @deferred_f_from_coro_f
async def test_deferred_f_from_coro_f(self): async def test_deferred_f_from_coro_f(self):
@ -304,7 +308,7 @@ class TestDeferredFromCoro:
class TestDeferredFFromCoroF: class TestDeferredFFromCoroF:
@inlineCallbacks @inlineCallbacks_orig
def _assert_result( def _assert_result(
self, c_f: Callable[[], Awaitable[int]] self, c_f: Callable[[], Awaitable[int]]
) -> Generator[Deferred[Any], Any, None]: ) -> Generator[Deferred[Any], Any, None]:
@ -342,6 +346,7 @@ class TestDeferredFFromCoroF:
@pytest.mark.only_asyncio @pytest.mark.only_asyncio
@pytest.mark.requires_reactor
class TestDeferredToFuture: class TestDeferredToFuture:
@deferred_f_from_coro_f @deferred_f_from_coro_f
async def test_deferred(self): async def test_deferred(self):
@ -377,6 +382,9 @@ class TestDeferredToFuture:
@pytest.mark.only_asyncio @pytest.mark.only_asyncio
# needs a reactor or an event loop for is_asyncio_available()
# (for maybe_deferred_to_future())
@pytest.mark.requires_reactor
class TestMaybeDeferredToFutureAsyncio: class TestMaybeDeferredToFutureAsyncio:
@deferred_f_from_coro_f @deferred_f_from_coro_f
async def test_deferred(self): async def test_deferred(self):
@ -412,6 +420,9 @@ class TestMaybeDeferredToFutureAsyncio:
@pytest.mark.only_not_asyncio @pytest.mark.only_not_asyncio
# needs a reactor or an event loop for is_asyncio_available()
# (for maybe_deferred_to_future())
@pytest.mark.requires_reactor
class TestMaybeDeferredToFutureNotAsyncio: class TestMaybeDeferredToFutureNotAsyncio:
def test_deferred(self): def test_deferred(self):
d = Deferred() d = Deferred()

View File

@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, TypeVar
import pytest import pytest
from scrapy.utils.asyncgen import as_async_generator, collect_asyncgen from scrapy.utils.asyncgen import as_async_generator, collect_asyncgen
from scrapy.utils.defer import aiter_errback, deferred_f_from_coro_f from scrapy.utils.defer import aiter_errback
from scrapy.utils.python import ( from scrapy.utils.python import (
MutableAsyncChain, MutableAsyncChain,
MutableChain, MutableChain,
@ -20,6 +20,7 @@ from scrapy.utils.python import (
to_unicode, to_unicode,
without_none_values, without_none_values,
) )
from tests.utils.decorators import deferred_f_from_coro_f
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Iterable, Mapping from collections.abc import Iterable, Mapping

View File

@ -3,20 +3,22 @@ import warnings
import pytest import pytest
from scrapy.utils.defer import deferred_f_from_coro_f
from scrapy.utils.reactor import ( from scrapy.utils.reactor import (
_asyncio_reactor_path, _asyncio_reactor_path,
install_reactor, install_reactor,
is_asyncio_reactor_installed, is_asyncio_reactor_installed,
set_asyncio_event_loop, set_asyncio_event_loop,
) )
from tests.utils.decorators import deferred_f_from_coro_f
class TestAsyncio: class TestAsyncio:
@pytest.mark.requires_reactor
def test_is_asyncio_reactor_installed(self, reactor_pytest: str) -> None: def test_is_asyncio_reactor_installed(self, reactor_pytest: str) -> None:
# the result should depend only on the pytest --reactor argument # the result should depend only on the pytest --reactor argument
assert is_asyncio_reactor_installed() == (reactor_pytest == "asyncio") assert is_asyncio_reactor_installed() == (reactor_pytest == "asyncio")
@pytest.mark.requires_reactor
def test_install_asyncio_reactor(self): def test_install_asyncio_reactor(self):
from twisted.internet import reactor as original_reactor from twisted.internet import reactor as original_reactor

View File

@ -4,7 +4,6 @@ import pytest
from pydispatch import dispatcher from pydispatch import dispatcher
from testfixtures import LogCapture from testfixtures import LogCapture
from twisted.internet import defer from twisted.internet import defer
from twisted.internet.defer import inlineCallbacks
from twisted.python.failure import Failure from twisted.python.failure import Failure
from scrapy.utils.asyncio import call_later from scrapy.utils.asyncio import call_later
@ -15,6 +14,7 @@ from scrapy.utils.signal import (
send_catch_log_deferred, send_catch_log_deferred,
) )
from scrapy.utils.test import get_from_asyncio_queue from scrapy.utils.test import get_from_asyncio_queue
from tests.utils.decorators import inlineCallbacks
class TestSendCatchLog: class TestSendCatchLog:

View File

@ -30,6 +30,8 @@ from tests.mockserver.http_resources import (
from tests.mockserver.utils import ssl_context_factory from tests.mockserver.utils import ssl_context_factory
from tests.test_core_downloader import TestContextFactoryBase from tests.test_core_downloader import TestContextFactoryBase
pytestmark = pytest.mark.requires_reactor
def getPage(url, contextFactory=None, response_transform=None, *args, **kwargs): def getPage(url, contextFactory=None, response_transform=None, *args, **kwargs):
"""Adapted version of twisted.web.client.getPage""" """Adapted version of twisted.web.client.getPage"""

View File

@ -30,6 +30,7 @@ def test_stderr_log_handler() -> None:
assert c == 0 assert c == 0
@pytest.mark.requires_reactor # needs a running event loop for asyncio.all_tasks()
@pytest.mark.only_asyncio @pytest.mark.only_asyncio
def test_pending_asyncio_tasks() -> None: def test_pending_asyncio_tasks() -> None:
"""Test that there are no pending asyncio tasks.""" """Test that there are no pending asyncio tasks."""

47
tests/utils/decorators.py Normal file
View File

@ -0,0 +1,47 @@
from __future__ import annotations
from functools import wraps
from typing import TYPE_CHECKING, Any, ParamSpec
import pytest
from twisted.internet.defer import Deferred
from twisted.internet.defer import inlineCallbacks as inlineCallbacks_orig
from scrapy.utils.defer import deferred_from_coro
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable, Generator
_P = ParamSpec("_P")
def inlineCallbacks(
f: Callable[_P, Generator[Deferred[Any], Any, None]],
) -> Callable[_P, Deferred[None]]:
"""Like :func:`twisted.internet.defer.inlineCallbacks`, but marks the
decorated function with ``@pytest.mark.requires_reactor``."""
@pytest.mark.requires_reactor
@wraps(f)
@inlineCallbacks_orig
def wrapper(
*args: _P.args, **kwargs: _P.kwargs
) -> Generator[Deferred[Any], Any, None]:
return f(*args, **kwargs)
return wrapper
def deferred_f_from_coro_f(
coro_f: Callable[_P, Awaitable[None]],
) -> Callable[_P, Deferred[None]]:
"""Like :func:`scrapy.utils.defer.deferred_f_from_coro_f`, but marks the
decorated function with ``@pytest.mark.requires_reactor``."""
@pytest.mark.requires_reactor
@wraps(coro_f)
def f(*coro_args: _P.args, **coro_kwargs: _P.kwargs) -> Deferred[None]:
return deferred_from_coro(coro_f(*coro_args, **coro_kwargs))
return f

11
tox.ini
View File

@ -161,6 +161,10 @@ commands = {[pinned]commands}
commands = commands =
{[testenv]commands} --reactor=default {[testenv]commands} --reactor=default
[testenv:no-reactor]
commands =
{[testenv]commands} -p no:twisted --reactor=none
[testenv:default-reactor-pinned] [testenv:default-reactor-pinned]
basepython = {[pinned]basepython} basepython = {[pinned]basepython}
deps = {[testenv:pinned]deps} deps = {[testenv:pinned]deps}
@ -168,6 +172,13 @@ commands = {[pinned]commands} --reactor=default
setenv = setenv =
{[pinned]setenv} {[pinned]setenv}
[testenv:no-reactor-pinned]
basepython = {[pinned]basepython}
deps = {[testenv:pinned]deps}
commands = {[pinned]commands} -p no:twisted --reactor=none
setenv =
{[pinned]setenv}
[testenv:pypy3] [testenv:pypy3]
basepython = pypy3 basepython = pypy3
commands = commands =