From 068aa69b350ab7e06df5d4cb372cfd15b256ea39 Mon Sep 17 00:00:00 2001 From: Andrey Rakhmatullin Date: Mon, 27 Apr 2026 18:47:52 +0500 Subject: [PATCH] Fixes for Python 3.14. (#7460) --- scrapy/extensions/feedexport.py | 2 +- tests/test_command_check.py | 10 +++++----- tests/test_feedexport.py | 4 ++-- tests/test_pipeline_files.py | 12 ++++++------ tests/test_squeues.py | 2 +- tests/test_utils_reactor.py | 12 ------------ 6 files changed, 15 insertions(+), 27 deletions(-) diff --git a/scrapy/extensions/feedexport.py b/scrapy/extensions/feedexport.py index ed4f60785..8f70dbc2e 100644 --- a/scrapy/extensions/feedexport.py +++ b/scrapy/extensions/feedexport.py @@ -167,7 +167,7 @@ class StdoutFeedStorage: @implementer(IFeedStorage) class FileFeedStorage: def __init__(self, uri: str, *, feed_options: dict[str, Any] | None = None): - self.path: str = file_uri_to_path(uri) if uri.startswith("file://") else uri + self.path: str = file_uri_to_path(uri) if uri.startswith("file:") else uri feed_options = feed_options or {} self.write_mode: OpenBinaryMode = ( "wb" if feed_options.get("overwrite", False) else "ab" diff --git a/tests/test_command_check.py b/tests/test_command_check.py index fd1ff612c..815f87026 100644 --- a/tests/test_command_check.py +++ b/tests/test_command_check.py @@ -4,7 +4,7 @@ import sys from io import StringIO from typing import TYPE_CHECKING from unittest import TestCase -from unittest.mock import Mock, PropertyMock, call, patch +from unittest.mock import MagicMock, Mock, PropertyMock, call, patch from scrapy.commands.check import Command, TextTestResult from tests.test_commands import TestProjectBase @@ -133,7 +133,7 @@ class CheckSpider(scrapy.Spider): def test_printSummary_with_unsuccessful_test_result_without_errors_and_without_failures( self, ) -> None: - result = TextTestResult(Mock(), descriptions=False, verbosity=1) + result = TextTestResult(MagicMock(), descriptions=False, verbosity=1) start_time = 1.0 stop_time = 2.0 result.testsRun = 5 @@ -147,7 +147,7 @@ class CheckSpider(scrapy.Spider): def test_printSummary_with_unsuccessful_test_result_with_only_failures( self, ) -> None: - result = TextTestResult(Mock(), descriptions=False, verbosity=1) + result = TextTestResult(MagicMock(), descriptions=False, verbosity=1) start_time = 1.0 stop_time = 2.0 result.testsRun = 5 @@ -158,7 +158,7 @@ class CheckSpider(scrapy.Spider): mock_write.assert_called_with(" (failures=1)") def test_printSummary_with_unsuccessful_test_result_with_only_errors(self) -> None: - result = TextTestResult(Mock(), descriptions=False, verbosity=1) + result = TextTestResult(MagicMock(), descriptions=False, verbosity=1) start_time = 1.0 stop_time = 2.0 result.testsRun = 5 @@ -171,7 +171,7 @@ class CheckSpider(scrapy.Spider): def test_printSummary_with_unsuccessful_test_result_with_both_failures_and_errors( self, ) -> None: - result = TextTestResult(Mock(), descriptions=False, verbosity=1) + result = TextTestResult(MagicMock(), descriptions=False, verbosity=1) start_time = 1.0 stop_time = 2.0 result.testsRun = 5 diff --git a/tests/test_feedexport.py b/tests/test_feedexport.py index dcf412c0f..cbf568524 100644 --- a/tests/test_feedexport.py +++ b/tests/test_feedexport.py @@ -44,7 +44,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterable -def path_to_url(path: Path) -> str: +def path_to_url(path: str | Path) -> str: return urljoin("file:", pathname2url(str(path))) @@ -1293,7 +1293,7 @@ class TestFeedExporterSignals: with tempfile.NamedTemporaryFile(suffix="json") as tmp: settings = { "FEEDS": { - f"file:///{tmp.name}": { + printf_escape(path_to_url(tmp.name)): { "format": "json", }, }, diff --git a/tests/test_pipeline_files.py b/tests/test_pipeline_files.py index 76f8c2512..dbe8d85a5 100644 --- a/tests/test_pipeline_files.py +++ b/tests/test_pipeline_files.py @@ -115,8 +115,8 @@ class TestFilesPipeline: req1 = Request("http://foo.bar/baz.txt?fizz") assert file_path(req1) == "full/a2b4913a62f65445aeae2bac08cd8c3b41d7195e.txt" - req2 = Request("http://foo.bar/get_img.php?file=photo.jpg") - assert file_path(req2) == "full/118230fd648f1080c81c234d5e2463ea496f8c05.jpg" + req2 = Request("http://foo.bar/get_img.foo?file=photo.jpg") + assert file_path(req2) == "full/7fc9461c9fd836515bea6983373097203a7d748e.jpg" def test_file_path(self): file_path = self.pipeline.file_path @@ -141,10 +141,10 @@ class TestFilesPipeline: assert ( file_path( Request( - "http://www.dfsonline.co.uk/get_prod_image.php?img=status_0907_mdm.jpg" + "http://www.dfsonline.co.uk/get_prod_image?img=status_0907_mdm.jpg" ) ) - == "full/4507be485f38b0da8a0be9eb2e1dfab8a19223f2.jpg" + == "full/c67f916ff9d542e822dedf38f9fcb146d1faba78.jpg" ) assert ( file_path(Request("http://www.dorma.co.uk/images/product_details/2532/")) @@ -165,10 +165,10 @@ class TestFilesPipeline: assert ( file_path( Request( - "http://www.dfsonline.co.uk/get_prod_image.php?img=status_0907_mdm.jpg.bohaha" + "http://www.dfsonline.co.uk/get_prod_image?img=status_0907_mdm.jpg.bohaha" ) ) - == "full/76c00cef2ef669ae65052661f68d451162829507" + == "full/e75f2fa260521b56f6b6a867447b8002d00b5841" ) assert ( file_path( diff --git a/tests/test_squeues.py b/tests/test_squeues.py index 8283d3d5d..ddc12766c 100644 --- a/tests/test_squeues.py +++ b/tests/test_squeues.py @@ -120,7 +120,7 @@ class PickleFifoDiskQueueTest(t.FifoDiskQueueTest, FifoDiskQueueTestMixin): match=r"Can't (get|pickle) local object|Can't pickle .*: it's not found as", ) as exc_info: q.push(lambda x: x) - if hasattr(sys, "pypy_version_info"): + if sys.version_info >= (3, 14) or hasattr(sys, "pypy_version_info"): assert isinstance(exc_info.value.__context__, pickle.PicklingError) else: assert isinstance(exc_info.value.__context__, AttributeError) diff --git a/tests/test_utils_reactor.py b/tests/test_utils_reactor.py index d11fbc582..7d39a478e 100644 --- a/tests/test_utils_reactor.py +++ b/tests/test_utils_reactor.py @@ -1,5 +1,4 @@ import asyncio -import warnings import pytest @@ -18,17 +17,6 @@ class TestAsyncio: # the result should depend only on the pytest --reactor argument assert is_asyncio_reactor_installed() == (reactor_pytest == "asyncio") - @pytest.mark.requires_reactor # installs a reactor - def test_install_asyncio_reactor(self): - from twisted.internet import reactor as original_reactor - - with warnings.catch_warnings(record=True) as w: - install_reactor(_asyncio_reactor_path) - assert len(w) == 0, [str(warning) for warning in w] - from twisted.internet import reactor # pylint: disable=reimported - - assert original_reactor == reactor - @pytest.mark.requires_reactor # installs a reactor @pytest.mark.only_asyncio @coroutine_test