From df5edd14ca5540d8e24b2b06ab3f38846f32f37b Mon Sep 17 00:00:00 2001 From: Adrian Chaves Date: Mon, 29 Jun 2026 15:08:39 +0200 Subject: [PATCH] Improve the error message when FORCE_CRAWLER_PROCESS is not True and probably should --- scrapy/crawler.py | 26 +++++++++++++++++- tests/test_command_crawl.py | 46 ++++++++++++++++++++++++++++++++ tests/test_crawler_subprocess.py | 6 +++-- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/scrapy/crawler.py b/scrapy/crawler.py index 9828fe6f5..b1b85e4e5 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -30,6 +30,7 @@ from scrapy.utils.log import ( ) from scrapy.utils.misc import build_from_crawler, load_object from scrapy.utils.ossignal import install_shutdown_handlers, signal_names +from scrapy.utils.python import global_object_name from scrapy.utils.reactor import ( _asyncio_reactor_path, install_reactor, @@ -128,7 +129,7 @@ class Crawler: ) if reactor_class: # We need to check that the correct reactor is installed. - verify_installed_reactor(reactor_class) + self._verify_installed_reactor(reactor_class) if is_asyncio_reactor_installed() and event_loop: verify_installed_asyncio_event_loop(event_loop) @@ -151,6 +152,29 @@ class Crawler: "Overridden settings:\n%(settings)s", {"settings": pprint.pformat(d)} ) + def _verify_installed_reactor(self, reactor_class: str) -> None: + try: + verify_installed_reactor(reactor_class) + except RuntimeError: + if ( + self._init_reactor + or self.settings.getbool("FORCE_CRAWLER_PROCESS") + or not is_asyncio_reactor_installed() + ): + raise + from twisted.internet import reactor + + raise RuntimeError( + f"The installed reactor ({global_object_name(type(reactor))}) " + f"does not match the one requested by spider " + f"{self.spidercls.name!r} ({reactor_class}). When running " + f"'scrapy crawl' (and similar commands), the TWISTED_REACTOR " + f"value from per-spider settings is only applied if " + f"FORCE_CRAWLER_PROCESS is True. Set FORCE_CRAWLER_PROCESS=True " + f"(in the project settings or with '-s FORCE_CRAWLER_PROCESS=True' " + f"on the command line) to use the spider's reactor." + ) from None + def _apply_reactorless_default_settings(self) -> None: """Change some setting defaults when not using a Twisted reactor. diff --git a/tests/test_command_crawl.py b/tests/test_command_crawl.py index dd69d36ba..257aecbcd 100644 --- a/tests/test_command_crawl.py +++ b/tests/test_command_crawl.py @@ -143,3 +143,49 @@ class MySpider(scrapy.Spider): assert "[myspider] DEBUG: It works!" in log assert "Not using a Twisted reactor" in log assert "Spider closed (finished)" in log + + def test_spider_reactor_without_force_crawler_process( + self, proj_path: Path + ) -> None: + spider_code = """ +import scrapy + +class MySpider(scrapy.Spider): + name = 'myspider' + custom_settings = { + 'TWISTED_REACTOR': 'twisted.internet.selectreactor.SelectReactor', + } + + async def start(self): + self.logger.debug('It works!') + return + yield +""" + log = self.get_log(spider_code, proj_path) + assert "[myspider] DEBUG: It works!" not in log + assert "does not match the one requested by spider 'myspider'" in log + assert "FORCE_CRAWLER_PROCESS=True" in log + assert "Spider closed (finished)" not in log + + def test_spider_reactor_with_force_crawler_process(self, proj_path: Path) -> None: + spider_code = """ +import scrapy + +class MySpider(scrapy.Spider): + name = 'myspider' + custom_settings = { + 'TWISTED_REACTOR': 'twisted.internet.selectreactor.SelectReactor', + } + + async def start(self): + self.logger.debug('It works!') + return + yield +""" + log = self.get_log( + spider_code, proj_path, args=("-s", "FORCE_CRAWLER_PROCESS=True") + ) + assert "[myspider] DEBUG: It works!" in log + assert "Using reactor: twisted.internet.selectreactor.SelectReactor" in log + assert "does not match the one requested by spider 'myspider'" not in log + assert "Spider closed (finished)" in log diff --git a/tests/test_crawler_subprocess.py b/tests/test_crawler_subprocess.py index e3f9ac161..6c5a79f19 100644 --- a/tests/test_crawler_subprocess.py +++ b/tests/test_crawler_subprocess.py @@ -331,10 +331,12 @@ class TestAsyncCrawlerProcessSubprocess(TestCrawlerProcessSubprocessBase): log = self.run_script("twisted_reactor_custom_settings_select.py") assert "Spider closed (finished)" not in log assert ( - "(twisted.internet.asyncioreactor.AsyncioSelectorReactor) " - "does not match the requested one " + "The installed reactor " + "(twisted.internet.asyncioreactor.AsyncioSelectorReactor) does not " + "match the one requested by spider 'asyncio_reactor' " "(twisted.internet.selectreactor.SelectReactor)" ) in log + assert "FORCE_CRAWLER_PROCESS=True" in log @pytest.mark.requires_uvloop def test_asyncio_enabled_reactor_same_loop(self) -> None: