Assorted test fixes (#7616)

This commit is contained in:
Andrey Rakhmatullin 2026-06-15 11:39:58 +05:00 committed by GitHub
parent af30cfea12
commit 3a36955261
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 154 additions and 185 deletions

View File

@ -1,7 +1,15 @@
import sys
from OpenSSL import __version__ as PYOPENSSL_VERSION_STRING from OpenSSL import __version__ as PYOPENSSL_VERSION_STRING
from packaging.version import Version from packaging.version import Version
from twisted import version as TWISTED_VERSION from twisted import version as TWISTED_VERSION
from twisted.python.versions import Version as TxVersion from twisted.python.versions import Version as TxVersion
from w3lib import __version__ as W3LIB_VERSION_STRING
# improved urllib.robotparser, https://github.com/python/cpython/pull/149374
STDLIB_IMPROVED_ROBOTFILEPARSER = sys.version_info >= (3, 14, 5) or (
(3, 13, 14) <= sys.version_info < (3, 14)
)
TWISTED_FAILURE_HAS_STACK = TWISTED_VERSION < TxVersion("twisted", 24, 10, 0) TWISTED_FAILURE_HAS_STACK = TWISTED_VERSION < TxVersion("twisted", 24, 10, 0)
# changes to private _sslverify code, https://github.com/twisted/twisted/pull/12506 # changes to private _sslverify code, https://github.com/twisted/twisted/pull/12506
@ -14,3 +22,7 @@ PYOPENSSL_VERSION = Version(PYOPENSSL_VERSION_STRING)
PYOPENSSL_WANTS_X509_PKEY = PYOPENSSL_VERSION < Version("24.3.0") PYOPENSSL_WANTS_X509_PKEY = PYOPENSSL_VERSION < Version("24.3.0")
# SSL.Context.set_cipher_list() creates a temporary connection, making the context immutable # SSL.Context.set_cipher_list() creates a temporary connection, making the context immutable
PYOPENSSL_SET_CIPHER_LIST_TMP_CONN = PYOPENSSL_VERSION < Version("25.2.0") PYOPENSSL_SET_CIPHER_LIST_TMP_CONN = PYOPENSSL_VERSION < Version("25.2.0")
W3LIB_VERSION = Version(W3LIB_VERSION_STRING)
# safe_url_string() strips the input, https://github.com/scrapy/w3lib/pull/207
W3LIB_STRIPS_URLS = W3LIB_VERSION >= Version("2.1.1")

View File

@ -1,3 +1 @@
scrapy/downloadermiddlewares/cookies.py
scrapy/extensions/statsmailer.py scrapy/extensions/statsmailer.py
scrapy/extensions/memusage.py

View File

@ -33,7 +33,7 @@ class SimpleMockServer(BaseMockServer):
super().__init__() super().__init__()
self.keyfile = keyfile self.keyfile = keyfile
self.certfile = certfile self.certfile = certfile
self.cipher_string = cipher_string or "" self.cipher_string = cipher_string
self.tls_min_version = tls_min_version self.tls_min_version = tls_min_version
self.tls_max_version = tls_max_version self.tls_max_version = tls_max_version

View File

@ -222,7 +222,7 @@ class AsyncDefDeferredWrappedSpider(SimpleSpider):
class AsyncDefDeferredMaybeWrappedSpider(SimpleSpider): class AsyncDefDeferredMaybeWrappedSpider(SimpleSpider):
name = "asyncdef_deferred_wrapped" name = "asyncdef_deferred_maybe_wrapped"
async def parse(self, response): async def parse(self, response):
await maybe_deferred_to_future(defer.succeed(None)) await maybe_deferred_to_future(defer.succeed(None))

View File

@ -161,6 +161,7 @@ class TestAddonManager:
settings.set("KEY", 0, priority="default") settings.set("KEY", 0, priority="default")
runner = runner_cls(settings) runner = runner_cls(settings)
crawler = runner.create_crawler(Spider) crawler = runner.create_crawler(Spider)
crawler._apply_settings()
assert crawler.settings.getint("KEY") == 20 assert crawler.settings.getint("KEY") == 20
def test_fallback_workflow(self): def test_fallback_workflow(self):

View File

@ -1,14 +1,2 @@
"""A test extension used to check the settings loading order"""
class TestExtension:
def __init__(self, settings):
settings.set("TEST1", f"{settings['TEST1']} + started")
@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings)
class DummyExtension: class DummyExtension:
pass pass

View File

@ -1,7 +1,7 @@
from pathlib import Path from pathlib import Path
EXTENSIONS = { EXTENSIONS = {
"tests.test_cmdline.extensions.TestExtension": 0, "tests.test_cmdline.extensions.DummyExtension": 0,
} }
TEST1 = "default" TEST1 = "default"

View File

@ -257,7 +257,7 @@ class TestStartprojectTemplates:
assert actual_permissions == expected_permissions assert actual_permissions == expected_permissions
def test_startproject_permissions_umask_022(self, tmp_path: Path) -> None: def test_startproject_permissions_umask_002(self, tmp_path: Path) -> None:
"""Check that generated files have the right permissions when the """Check that generated files have the right permissions when the
system uses a umask value that causes new files to have different system uses a umask value that causes new files to have different
permissions than those from the template folder.""" permissions than those from the template folder."""

View File

@ -214,9 +214,7 @@ class MySpider(scrapy.Spider):
self._append_settings(proj_path / self.project_name, "TWISTED_REACTOR = None\n") self._append_settings(proj_path / self.project_name, "TWISTED_REACTOR = None\n")
self._assert_spider_works(self.NORMAL_MSG, proj_path, "sp") self._assert_spider_works(self.NORMAL_MSG, proj_path, "sp")
self._assert_spider_asyncio_fail( self._assert_spider_asyncio_fail(self.NORMAL_MSG, proj_path, "aiosp")
self.NORMAL_MSG, proj_path, "aiosp", "-s", "TWISTED_REACTOR="
)
def test_spider_settings_asyncio(self, proj_path: Path) -> None: def test_spider_settings_asyncio(self, proj_path: Path) -> None:
"""The reactor is set via the spider settings to the asyncio value. """The reactor is set via the spider settings to the asyncio value.

View File

@ -388,7 +388,7 @@ class TestContractsManager:
request = self.conman.from_method(spider.returns_item_meta, self.results) request = self.conman.from_method(spider.returns_item_meta, self.results)
assert request.meta["key"] == "example" assert request.meta["key"] == "example"
response.meta = request.meta response.meta = request.meta
request.callback(ResponseMetaMock) request.callback(response)
assert response.meta["key"] == "example" assert response.meta["key"] == "example"
self.should_succeed() self.should_succeed()
@ -476,14 +476,14 @@ class TestContractsManager:
# invalid regex # invalid regex
request = self.conman.from_method(spider.invalid_regex, self.results) request = self.conman.from_method(spider.invalid_regex, self.results)
self.should_succeed() assert request is None
# invalid regex with valid contract # invalid regex with valid contract
request = self.conman.from_method( request = self.conman.from_method(
spider.invalid_regex_with_valid_contract, self.results spider.invalid_regex_with_valid_contract, self.results
) )
self.should_succeed()
request.callback(response) request.callback(response)
self.should_succeed()
def test_custom_contracts(self): def test_custom_contracts(self):
self.conman.from_spider(CustomContractSuccessSpider(), self.results) self.conman.from_spider(CustomContractSuccessSpider(), self.results)
@ -578,7 +578,7 @@ class TestCustomContractPrePostProcess:
spider = DemoSpider() spider = DemoSpider()
response = ResponseMock() response = ResponseMock()
contract = CustomFailContractPreProcess(spider.returns_request) contract = CustomFailContractPreProcess(spider.returns_request)
conman = ContractsManager([contract]) conman = ContractsManager([UrlContract, ReturnsContract, contract])
request = conman.from_method(spider.returns_request, self.results) request = conman.from_method(spider.returns_request, self.results)
contract.add_pre_hook(request, self.results) contract.add_pre_hook(request, self.results)
@ -592,7 +592,7 @@ class TestCustomContractPrePostProcess:
spider = DemoSpider() spider = DemoSpider()
response = ResponseMock() response = ResponseMock()
contract = CustomFailContractPostProcess(spider.returns_request) contract = CustomFailContractPostProcess(spider.returns_request)
conman = ContractsManager([contract]) conman = ContractsManager([UrlContract, ReturnsContract, contract])
request = conman.from_method(spider.returns_request, self.results) request = conman.from_method(spider.returns_request, self.results)
contract.add_post_hook(request, self.results) contract.add_post_hook(request, self.results)

View File

@ -36,7 +36,6 @@ from tests.utils.decorators import coroutine_test
if TYPE_CHECKING: if TYPE_CHECKING:
from twisted.internet.defer import Deferred from twisted.internet.defer import Deferred
from twisted.internet.ssl import ContextFactory
from twisted.web.iweb import IBodyProducer from twisted.web.iweb import IBodyProducer
@ -48,8 +47,6 @@ class TestSlot:
@pytest.mark.requires_reactor # this test is related to the Twisted HTTP code @pytest.mark.requires_reactor # this test is related to the Twisted HTTP code
class TestContextFactoryBase: class TestContextFactoryBase:
context_factory: ContextFactory | None = None
@async_yield_fixture @async_yield_fixture
async def server_url(self, tmp_path): async def server_url(self, tmp_path):
(tmp_path / "file").write_bytes(b"0123456789") (tmp_path / "file").write_bytes(b"0123456789")
@ -69,7 +66,7 @@ class TestContextFactoryBase:
return reactor.listenSSL( return reactor.listenSSL(
0, 0,
site, site,
contextFactory=self.context_factory or ssl_context_factory(), contextFactory=ssl_context_factory(),
interface="127.0.0.1", interface="127.0.0.1",
) )

View File

@ -342,7 +342,7 @@ with multiples lines
assert "responses" in crawler.spider.meta assert "responses" in crawler.spider.meta
assert "failures" not in crawler.spider.meta assert "failures" not in crawler.spider.meta
# start() doesn't set Referer header # start() doesn't set Referer header
echo0 = json.loads(to_unicode(crawler.spider.meta["responses"][2].body)) echo0 = json.loads(to_unicode(crawler.spider.meta["responses"][0].body))
assert "Referer" not in echo0["headers"] assert "Referer" not in echo0["headers"]
# following request sets Referer to the source request url # following request sets Referer to the source request url
echo1 = json.loads(to_unicode(crawler.spider.meta["responses"][1].body)) echo1 = json.loads(to_unicode(crawler.spider.meta["responses"][1].body))
@ -390,7 +390,7 @@ with multiples lines
est = [x for sublist in est for x in sublist] # flatten est = [x for sublist in est for x in sublist] # flatten
est = [x.lstrip().rstrip() for x in est] est = [x.lstrip().rstrip() for x in est]
it = iter(est) it = iter(est)
s = dict(zip(it, it, strict=False)) s = dict(zip(it, it, strict=True))
assert s["engine.spider.name"] == crawler.spider.name assert s["engine.spider.name"] == crawler.spider.name
assert s["len(engine.scraper.slot.active)"] == "1" assert s["len(engine.scraper.slot.active)"] == "1"

View File

@ -240,11 +240,7 @@ class TestCrawler(TestBaseCrawler):
@classmethod @classmethod
def from_crawler(cls, crawler): def from_crawler(cls, crawler):
try: crawler.get_downloader_middleware(DefaultSpider)
crawler.get_downloader_middleware(DefaultSpider)
except Exception as e:
MySpider.result = e
raise
crawler = get_raw_crawler(MySpider, BASE_SETTINGS) crawler = get_raw_crawler(MySpider, BASE_SETTINGS)
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
@ -322,11 +318,7 @@ class TestCrawler(TestBaseCrawler):
@classmethod @classmethod
def from_crawler(cls, crawler): def from_crawler(cls, crawler):
try: crawler.get_extension(DefaultSpider)
crawler.get_extension(DefaultSpider)
except Exception as e:
MySpider.result = e
raise
crawler = get_raw_crawler(MySpider, BASE_SETTINGS) crawler = get_raw_crawler(MySpider, BASE_SETTINGS)
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
@ -404,11 +396,7 @@ class TestCrawler(TestBaseCrawler):
@classmethod @classmethod
def from_crawler(cls, crawler): def from_crawler(cls, crawler):
try: crawler.get_item_pipeline(DefaultSpider)
crawler.get_item_pipeline(DefaultSpider)
except Exception as e:
MySpider.result = e
raise
crawler = get_raw_crawler(MySpider, BASE_SETTINGS) crawler = get_raw_crawler(MySpider, BASE_SETTINGS)
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
@ -486,11 +474,7 @@ class TestCrawler(TestBaseCrawler):
@classmethod @classmethod
def from_crawler(cls, crawler): def from_crawler(cls, crawler):
try: crawler.get_spider_middleware(DefaultSpider)
crawler.get_spider_middleware(DefaultSpider)
except Exception as e:
MySpider.result = e
raise
crawler = get_raw_crawler(MySpider, BASE_SETTINGS) crawler = get_raw_crawler(MySpider, BASE_SETTINGS)
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
@ -755,11 +739,12 @@ class TestCrawlerRunnerHasSpider:
): ):
await self._crawl(runner, NoRequestsSpider) await self._crawl(runner, NoRequestsSpider)
else: else:
CrawlerRunner( runner = CrawlerRunner(
settings={ settings={
"TWISTED_REACTOR": "twisted.internet.asyncioreactor.AsyncioSelectorReactor", "TWISTED_REACTOR": "twisted.internet.asyncioreactor.AsyncioSelectorReactor",
} }
) )
await self._crawl(runner, NoRequestsSpider)
@pytest.mark.only_asyncio @pytest.mark.only_asyncio

View File

@ -59,7 +59,7 @@ class TestFTPBase(ABC):
port = reactor.listenTCP(0, factory, interface="127.0.0.1") port = reactor.listenTCP(0, factory, interface="127.0.0.1")
portno = port.getHost().port portno = port.getHost().port
yield f"https://127.0.0.1:{portno}/" yield f"ftp://127.0.0.1:{portno}/"
await port.stopListening() await port.stopListening()
@ -142,15 +142,11 @@ class TestFTPBase(ABC):
server_url: str, server_url: str,
dh: FTPDownloadHandler, dh: FTPDownloadHandler,
) -> None: ) -> None:
f, local_fname = mkstemp()
local_fname_path = Path(local_fname)
os.close(f)
meta = {} meta = {}
meta.update(self.req_meta) meta.update(self.req_meta)
request = Request(url=server_url + filename, meta=meta) request = Request(url=server_url + filename, meta=meta)
r = await dh.download_request(request) r = await dh.download_request(request)
assert type(r) is response_class # pylint: disable=unidiomatic-typecheck assert type(r) is response_class # pylint: disable=unidiomatic-typecheck
local_fname_path.unlink()
class TestFTP(TestFTPBase): class TestFTP(TestFTPBase):

View File

@ -24,6 +24,7 @@ from tests.test_downloader_handlers_http_base import (
TestHttpWithCrawlerBase, TestHttpWithCrawlerBase,
TestMitmProxyBase, TestMitmProxyBase,
TestRealWebsiteBase, TestRealWebsiteBase,
TestSimpleHttpsBase,
) )
from tests.utils.decorators import coroutine_test from tests.utils.decorators import coroutine_test
@ -156,6 +157,10 @@ class TestHttp2(H2DownloadHandlerMixin, TestHttpsBase):
await download_handler.download_request(request) await download_handler.download_request(request)
class TestSimpleHttp2(H2DownloadHandlerMixin, TestSimpleHttpsBase):
pass
class TestHttp2WrongHostname(H2DownloadHandlerMixin, TestHttpsWrongHostnameBase): class TestHttp2WrongHostname(H2DownloadHandlerMixin, TestHttpsWrongHostnameBase):
pass pass

View File

@ -191,17 +191,14 @@ class TestS3:
@contextlib.contextmanager @contextlib.contextmanager
def _mocked_date(self, date): def _mocked_date(self, date):
try: import botocore.auth # noqa: F401,PLC0415
import botocore.auth # noqa: F401,PLC0415
except ImportError: # We need to mock botocore.auth.formatdate, because otherwise
# botocore overrides Date header with current date and time
# and Authorization header is different each time
with mock.patch("botocore.auth.formatdate") as mock_formatdate:
mock_formatdate.return_value = date
yield yield
else:
# We need to mock botocore.auth.formatdate, because otherwise
# botocore overrides Date header with current date and time
# and Authorization header is different each time
with mock.patch("botocore.auth.formatdate") as mock_formatdate:
mock_formatdate.return_value = date
yield
@coroutine_test @coroutine_test
async def test_request_signing1(self): async def test_request_signing1(self):

View File

@ -563,7 +563,7 @@ class TestHttpBase(ABC):
) -> None: ) -> None:
request = Request(mockserver.url("/text", is_secure=self.is_secure)) request = Request(mockserver.url("/text", is_secure=self.is_secure))
# 10 is minimal size for this request and the limit is only counted on # 5 is minimal size for this request and the limit is only counted on
# response body. (regardless of headers) # response body. (regardless of headers)
async with self.get_dh({"DOWNLOAD_MAXSIZE": 5}) as download_handler: async with self.get_dh({"DOWNLOAD_MAXSIZE": 5}) as download_handler:
response = await download_handler.download_request(request) response = await download_handler.download_request(request)

View File

@ -203,7 +203,7 @@ class DummyPolicyTestMixin(PolicyTestMixin):
assert mw.process_request(req) is None assert mw.process_request(req) is None
# s3 scheme response is cached by default # s3 scheme response is cached by default
req, res = Request("s3://bucket/key"), Response("http://bucket/key") req, res = Request("s3://bucket/key"), Response("s3://bucket/key")
with self._middleware() as mw: with self._middleware() as mw:
assert mw.process_request(req) is None assert mw.process_request(req) is None
mw.process_response(req, res) mw.process_response(req, res)
@ -214,7 +214,7 @@ class DummyPolicyTestMixin(PolicyTestMixin):
assert "cached" in cached.flags assert "cached" in cached.flags
# ignore s3 scheme # ignore s3 scheme
req, res = Request("s3://bucket/key2"), Response("http://bucket/key2") req, res = Request("s3://bucket/key2"), Response("s3://bucket/key2")
with self._middleware(HTTPCACHE_IGNORE_SCHEMES=["s3"]) as mw: with self._middleware(HTTPCACHE_IGNORE_SCHEMES=["s3"]) as mw:
assert mw.process_request(req) is None assert mw.process_request(req) is None
mw.process_response(req, res) mw.process_response(req, res)

View File

@ -361,7 +361,7 @@ class TestHttpCompression:
zf.write(plainbody) zf.write(plainbody)
zf.close() zf.close()
response = Response( response = Response(
"http;//www.example.com/", headers=headers, body=f.getvalue() "http://www.example.com/", headers=headers, body=f.getvalue()
) )
request = Request("http://www.example.com/") request = Request("http://www.example.com/")
@ -386,7 +386,7 @@ class TestHttpCompression:
zf.write(plainbody) zf.write(plainbody)
zf.close() zf.close()
response = HtmlResponse( response = HtmlResponse(
"http;//www.example.com/page.html", headers=headers, body=f.getvalue() "http://www.example.com/page.html", headers=headers, body=f.getvalue()
) )
request = Request("http://www.example.com/") request = Request("http://www.example.com/")
@ -493,7 +493,7 @@ class TestHttpCompression:
gz_resp.close() gz_resp.close()
response = Response( response = Response(
"http;//www.example.com/", headers=headers, body=r.getvalue() "http://www.example.com/", headers=headers, body=r.getvalue()
) )
request = Request("http://www.example.com/") request = Request("http://www.example.com/")

View File

@ -122,7 +122,7 @@ class Base:
req1 = Request("http://a.example/first") req1 = Request("http://a.example/first")
rsp1 = self.get_response(req1, "/redirected") rsp1 = self.get_response(req1, "/redirected")
req2 = self.mw.process_response(req1, rsp1) req2 = self.mw.process_response(req1, rsp1)
rsp2 = self.get_response(req1, "/redirected2") rsp2 = self.get_response(req2, "/redirected2")
req3 = self.mw.process_response(req2, rsp2) req3 = self.mw.process_response(req2, rsp2)
assert req2.url == "http://a.example/redirected" assert req2.url == "http://a.example/redirected"

View File

@ -16,7 +16,7 @@ class TestDownloaderStats:
self.crawler.stats.open_spider() self.crawler.stats.open_spider()
self.req = Request("http://scrapytest.org") self.req = Request("http://scrapytest.org")
self.res = Response("scrapytest.org", status=400) self.res = Response("http://scrapytest.org", status=400)
def assertStatsEqual(self, key, value): def assertStatsEqual(self, key, value):
assert self.crawler.stats.get_value(key) == value, str( assert self.crawler.stats.get_value(key) == value, str(

View File

@ -5,7 +5,6 @@ import pytest
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.exceptions import ScrapyDeprecationWarning from scrapy.exceptions import ScrapyDeprecationWarning
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
@ -65,9 +64,6 @@ class TestCrawl:
def teardown_class(cls): def teardown_class(cls):
cls.mockserver.__exit__(None, None, None) cls.mockserver.__exit__(None, None, None)
def setup_method(self):
self.runner = CrawlerRunner()
@inline_callbacks_test @inline_callbacks_test
def test_delay(self): def test_delay(self):
crawler = get_crawler(DownloaderSlotsSettingsTestSpider) crawler = get_crawler(DownloaderSlotsSettingsTestSpider)

View File

@ -86,7 +86,7 @@ class TestRFPDupeFilter:
df.close("finished") df.close("finished")
df2 = _get_dupefilter(settings={"JOBDIR": path}, open_=False) df2 = _get_dupefilter(settings={"JOBDIR": path}, open_=False)
assert df != df2 assert df is not df2
try: try:
df2.open() df2.open()
assert df2.request_seen(r1) assert df2.request_seen(r1)

View File

@ -6,7 +6,6 @@ import subprocess
import sys import sys
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from logging import DEBUG
from typing import TYPE_CHECKING, Any, cast from typing import TYPE_CHECKING, Any, cast
from unittest.mock import Mock, call from unittest.mock import Mock, call
from urllib.parse import urlparse from urllib.parse import urlparse
@ -395,6 +394,7 @@ class TestEngine(TestEngineBase):
self._assert_downloaded_responses(run, count=9) self._assert_downloaded_responses(run, count=9)
self._assert_scraped_items(run) self._assert_scraped_items(run)
self._assert_signals_caught(run) self._assert_signals_caught(run)
self._assert_headers_received(run)
self._assert_bytes_received(run) self._assert_bytes_received(run)
@coroutine_test @coroutine_test
@ -606,7 +606,7 @@ class TestEngineDownload(TestEngineDownloadAsync):
@coroutine_test @coroutine_test
async def test_request_scheduled_signal(caplog): async def test_request_scheduled_signal():
class TestScheduler(BaseScheduler): class TestScheduler(BaseScheduler):
def __init__(self): def __init__(self):
self.enqueued = [] self.enqueued = []
@ -633,7 +633,6 @@ async def test_request_scheduled_signal(caplog):
keep_request = Request("https://keep.example") keep_request = Request("https://keep.example")
engine._schedule_request(keep_request) engine._schedule_request(keep_request)
drop_request = Request("https://drop.example") drop_request = Request("https://drop.example")
caplog.set_level(DEBUG)
engine._schedule_request(drop_request) engine._schedule_request(drop_request)
assert scheduler.enqueued == [keep_request], ( assert scheduler.enqueued == [keep_request], (
f"{scheduler.enqueued!r} != [{keep_request!r}]" f"{scheduler.enqueued!r} != [{keep_request!r}]"

View File

@ -32,7 +32,6 @@ from scrapy.extensions.feedexport import (
FeedSlot, FeedSlot,
FileFeedStorage, FileFeedStorage,
IFeedStorage, IFeedStorage,
S3FeedStorage,
) )
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
@ -499,8 +498,7 @@ class TestFeedExport(TestFeedExportBase):
}, },
} }
crawler = get_crawler(ItemSpider, settings) crawler = get_crawler(ItemSpider, settings)
with mock.patch.object(S3FeedStorage, "store"): yield crawler.crawl(mockserver=self.mockserver)
yield crawler.crawl(mockserver=self.mockserver)
assert "feedexport/success_count/FileFeedStorage" in crawler.stats.get_stats() assert "feedexport/success_count/FileFeedStorage" in crawler.stats.get_stats()
assert "feedexport/success_count/StdoutFeedStorage" in crawler.stats.get_stats() assert "feedexport/success_count/StdoutFeedStorage" in crawler.stats.get_stats()
assert crawler.stats.get_value("feedexport/success_count/FileFeedStorage") == 1 assert crawler.stats.get_value("feedexport/success_count/FileFeedStorage") == 1

View File

@ -315,7 +315,7 @@ class TestBatchDeliveries(TestFeedExportBase):
} }
data = await self.exported_data(items, settings) data = await self.exported_data(items, settings)
for fmt, expected in formats.items(): for fmt, expected in formats.items():
for expected_batch, got_batch in zip(expected, data[fmt], strict=False): for expected_batch, got_batch in zip(expected, data[fmt], strict=True):
assert got_batch == expected_batch assert got_batch == expected_batch
@coroutine_test @coroutine_test
@ -339,7 +339,7 @@ class TestBatchDeliveries(TestFeedExportBase):
} }
data = await self.exported_data(items, settings) data = await self.exported_data(items, settings)
for fmt, expected in formats.items(): for fmt, expected in formats.items():
for expected_batch, got_batch in zip(expected, data[fmt], strict=False): for expected_batch, got_batch in zip(expected, data[fmt], strict=True):
assert got_batch == expected_batch assert got_batch == expected_batch
@coroutine_test @coroutine_test
@ -447,7 +447,7 @@ class TestBatchDeliveries(TestFeedExportBase):
yield crawler.crawl() yield crawler.crawl()
assert len(CustomS3FeedStorage.stubs) == len(items) assert len(CustomS3FeedStorage.stubs) == len(items)
for stub in CustomS3FeedStorage.stubs[:-1]: for stub in CustomS3FeedStorage.stubs:
stub.assert_no_pending_responses() stub.assert_no_pending_responses()
assert ( assert (
"feedexport/success_count/CustomS3FeedStorage" in crawler.stats.get_stats() "feedexport/success_count/CustomS3FeedStorage" in crawler.stats.get_stats()

View File

@ -270,7 +270,7 @@ class TestFeedPostProcessedExports(TestFeedExportBase):
self._named_tempfile("check_CHECK_NONE"): lzma.compress( self._named_tempfile("check_CHECK_NONE"): lzma.compress(
self.expected, check=lzma.CHECK_NONE self.expected, check=lzma.CHECK_NONE
), ),
self._named_tempfile("check_CHECK_CRC256"): lzma.compress( self._named_tempfile("CHECK_SHA256"): lzma.compress(
self.expected, check=lzma.CHECK_SHA256 self.expected, check=lzma.CHECK_SHA256
), ),
} }
@ -282,7 +282,7 @@ class TestFeedPostProcessedExports(TestFeedExportBase):
"postprocessing": ["scrapy.extensions.postprocessing.LZMAPlugin"], "postprocessing": ["scrapy.extensions.postprocessing.LZMAPlugin"],
"lzma_check": lzma.CHECK_NONE, "lzma_check": lzma.CHECK_NONE,
}, },
self._named_tempfile("check_CHECK_CRC256"): { self._named_tempfile("CHECK_SHA256"): {
"format": "csv", "format": "csv",
"postprocessing": ["scrapy.extensions.postprocessing.LZMAPlugin"], "postprocessing": ["scrapy.extensions.postprocessing.LZMAPlugin"],
"lzma_check": lzma.CHECK_SHA256, "lzma_check": lzma.CHECK_SHA256,

View File

@ -141,7 +141,7 @@ class Dataloss(LeafResource):
class NoContentLengthHeader(LeafResource): class NoContentLengthHeader(LeafResource):
def render_GET(self, request: TxRequest): def render_GET(self, request: TxRequest):
request.requestHeaders.removeHeader("Content-Length") request.responseHeaders.removeHeader("Content-Length")
self.deferRequest(request, 0, self._delayed_render, request) self.deferRequest(request, 0, self._delayed_render, request)
return NOT_DONE_YET return NOT_DONE_YET
@ -460,9 +460,7 @@ class TestHttps2ClientProtocol:
def test_invalid_negotiated_protocol( def test_invalid_negotiated_protocol(
self, server_port: int, client: H2ClientProtocol self, server_port: int, client: H2ClientProtocol
) -> Generator[Deferred[Any], Any, None]: ) -> Generator[Deferred[Any], Any, None]:
with mock.patch( with mock.patch("scrapy.core.http2.protocol.PROTOCOL_NAME", new=b"not-h2"):
"scrapy.core.http2.protocol.PROTOCOL_NAME", return_value=b"not-h2"
):
request = Request(url=self.get_url(server_port, "/status?n=200")) request = Request(url=self.get_url(server_port, "/status?n=200"))
with pytest.raises(ResponseFailed): with pytest.raises(ResponseFailed):
yield make_request_dfd(client, request) yield make_request_dfd(client, request)

View File

@ -23,7 +23,6 @@ class TestRequest:
# url argument must be basestring # url argument must be basestring
with pytest.raises(TypeError): with pytest.raises(TypeError):
self.request_class(123) self.request_class(123)
r = self.request_class("http://www.example.com")
r = self.request_class("http://www.example.com") r = self.request_class("http://www.example.com")
assert isinstance(r.url, str) assert isinstance(r.url, str)
@ -211,11 +210,11 @@ class TestRequest:
r1.cb_kwargs["key"] = "value" r1.cb_kwargs["key"] = "value"
r2 = r1.copy() r2 = r1.copy()
# make sure copy does not propagate callbacks # make sure callbaclks are copied
assert r1.callback is somecallback assert r1.callback is somecallback
assert r1.errback is somecallback assert r1.errback is somecallback
assert r2.callback is r1.callback assert r2.callback is r1.callback
assert r2.errback is r2.errback assert r2.errback is r1.errback
# make sure flags list is shallow copied # make sure flags list is shallow copied
assert r1.flags is not r2.flags, "flags must be a shallow copy, not identical" assert r1.flags is not r2.flags, "flags must be a shallow copy, not identical"

View File

@ -1,13 +1,19 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import pytest import pytest
from packaging.version import Version as parse_version
from w3lib import __version__ as w3lib_version
from w3lib.encoding import resolve_encoding from w3lib.encoding import resolve_encoding
from scrapy.exceptions import NotSupported from scrapy.exceptions import NotSupported
from scrapy.http import Headers, Request, Response from scrapy.http import Headers, Request, Response
from scrapy.link import Link from scrapy.link import Link
from scrapy.utils._deps_compat import W3LIB_STRIPS_URLS
from tests import get_testdata from tests import get_testdata
if TYPE_CHECKING:
from collections.abc import Iterable
class TestResponse: class TestResponse:
response_class = Response response_class = Response
@ -249,7 +255,7 @@ class TestResponse:
r.follow(None) r.follow(None)
@pytest.mark.xfail( @pytest.mark.xfail(
parse_version(w3lib_version) < parse_version("2.1.1"), not W3LIB_STRIPS_URLS,
reason="https://github.com/scrapy/w3lib/pull/207", reason="https://github.com/scrapy/w3lib/pull/207",
strict=True, strict=True,
) )
@ -257,7 +263,7 @@ class TestResponse:
self._assert_followed_url("foo ", "http://example.com/foo") self._assert_followed_url("foo ", "http://example.com/foo")
@pytest.mark.xfail( @pytest.mark.xfail(
parse_version(w3lib_version) < parse_version("2.1.1"), not W3LIB_STRIPS_URLS,
reason="https://github.com/scrapy/w3lib/pull/207", reason="https://github.com/scrapy/w3lib/pull/207",
strict=True, strict=True,
) )
@ -325,16 +331,26 @@ class TestResponse:
with pytest.raises(ValueError, match="url can't be None"): with pytest.raises(ValueError, match="url can't be None"):
list(r.follow_all(urls=[None])) list(r.follow_all(urls=[None]))
@pytest.mark.xfail(
not W3LIB_STRIPS_URLS,
reason="https://github.com/scrapy/w3lib/pull/207",
strict=True,
)
def test_follow_all_whitespace(self): def test_follow_all_whitespace(self):
relative = ["foo ", "bar ", "foo/bar ", "bar/foo "] relative = ["foo ", "bar ", "foo/bar ", "bar/foo "]
absolute = [ absolute = [
"http://example.com/foo%20", "http://example.com/foo",
"http://example.com/bar%20", "http://example.com/bar",
"http://example.com/foo/bar%20", "http://example.com/foo/bar",
"http://example.com/bar/foo%20", "http://example.com/bar/foo",
] ]
self._assert_followed_all_urls(relative, absolute) self._assert_followed_all_urls(relative, absolute)
@pytest.mark.xfail(
not W3LIB_STRIPS_URLS,
reason="https://github.com/scrapy/w3lib/pull/207",
strict=True,
)
def test_follow_all_whitespace_links(self): def test_follow_all_whitespace_links(self):
absolute = [ absolute = [
"http://example.com/foo ", "http://example.com/foo ",
@ -342,8 +358,8 @@ class TestResponse:
"http://example.com/foo/bar ", "http://example.com/foo/bar ",
"http://example.com/bar/foo ", "http://example.com/bar/foo ",
] ]
links = map(Link, absolute) links = [Link(u) for u in absolute]
expected = [u.replace(" ", "%20") for u in absolute] expected = [u.strip() for u in absolute]
self._assert_followed_all_urls(links, expected) self._assert_followed_all_urls(links, expected)
def test_follow_all_flags(self): def test_follow_all_flags(self):
@ -357,25 +373,36 @@ class TestResponse:
for req in fol: for req in fol:
assert req.flags == ["cached", "allowed"] assert req.flags == ["cached", "allowed"]
def _assert_followed_url(self, follow_obj, target_url, response=None): def _assert_followed_url(
self,
follow_obj: str | Link,
target_url: str,
response: Response | None = None,
encoding: str | None = None,
) -> None:
if response is None: if response is None:
response = self._links_response() response = self._links_response()
req = response.follow(follow_obj) req = response.follow(follow_obj)
assert req.url == target_url assert req.url == target_url
return req if encoding is not None:
assert req.encoding == encoding
def _assert_followed_all_urls(self, follow_obj, target_urls, response=None): def _assert_followed_all_urls(
self,
follow_obj: Iterable[str | Link],
target_urls: Iterable[str],
response: Response | None = None,
) -> None:
if response is None: if response is None:
response = self._links_response() response = self._links_response()
followed = response.follow_all(follow_obj) followed = response.follow_all(follow_obj)
for req, target in zip(followed, target_urls, strict=False): for req, target in zip(followed, target_urls, strict=True):
assert req.url == target assert req.url == target
yield req
def _links_response(self): def _links_response(self) -> Response:
body = get_testdata("link_extractor", "linkextractor.html") body = get_testdata("link_extractor", "linkextractor.html")
return self.response_class("http://example.com/index", body=body) return self.response_class("http://example.com/index", body=body)
def _links_response_no_href(self): def _links_response_no_href(self) -> Response:
body = get_testdata("link_extractor", "linkextractor_no_href.html") body = get_testdata("link_extractor", "linkextractor_no_href.html")
return self.response_class("http://example.com/index", body=body) return self.response_class("http://example.com/index", body=body)

View File

@ -179,7 +179,6 @@ class TestTextResponse(TestResponse):
# Inferring encoding from body also cache decoded body as sideeffect, # Inferring encoding from body also cache decoded body as sideeffect,
# this test tries to ensure that calling response.encoding and # this test tries to ensure that calling response.encoding and
# response.text in indistinct order doesn't affect final # response.text in indistinct order doesn't affect final
# response.text in indistinct order doesn't affect final
# values for encoding and decoded body. # values for encoding and decoded body.
url = "http://example.com" url = "http://example.com"
body = b"\xef\xbb\xbfWORD" body = b"\xef\xbb\xbfWORD"
@ -308,11 +307,12 @@ class TestTextResponse(TestResponse):
"http://example.com/sample3.html#foo", "http://example.com/sample3.html#foo",
"http://www.google.com/something", "http://www.google.com/something",
"http://example.com/innertag.html", "http://example.com/innertag.html",
"http://example.com/page%204.html",
] ]
# select <a> elements # select <a> elements
for sellist in [resp.css("a"), resp.xpath("//a")]: for sellist in [resp.css("a"), resp.xpath("//a")]:
for sel, url in zip(sellist, urls, strict=False): for sel, url in zip(sellist, urls, strict=True):
self._assert_followed_url(sel, url, response=resp) self._assert_followed_url(sel, url, response=resp)
# select <link> elements # select <link> elements
@ -324,7 +324,7 @@ class TestTextResponse(TestResponse):
# href attributes should work # href attributes should work
for sellist in [resp.css("a::attr(href)"), resp.xpath("//a/@href")]: for sellist in [resp.css("a::attr(href)"), resp.xpath("//a/@href")]:
for sel, url in zip(sellist, urls, strict=False): for sel, url in zip(sellist, urls, strict=True):
self._assert_followed_url(sel, url, response=resp) self._assert_followed_url(sel, url, response=resp)
# non-a elements are not supported # non-a elements are not supported
@ -376,12 +376,12 @@ class TestTextResponse(TestResponse):
encoding="utf8", encoding="utf8",
body='<html><body><a href="foo?привет">click me</a></body></html>'.encode(), body='<html><body><a href="foo?привет">click me</a></body></html>'.encode(),
) )
req = self._assert_followed_url( self._assert_followed_url(
resp1.css("a")[0], resp1.css("a")[0],
"http://example.com/foo?%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82", "http://example.com/foo?%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82",
response=resp1, response=resp1,
encoding="utf8",
) )
assert req.encoding == "utf8"
resp2 = self.response_class( resp2 = self.response_class(
"http://example.com", "http://example.com",
@ -390,12 +390,12 @@ class TestTextResponse(TestResponse):
"cp1251" "cp1251"
), ),
) )
req = self._assert_followed_url( self._assert_followed_url(
resp2.css("a")[0], resp2.css("a")[0],
"http://example.com/foo?%EF%F0%E8%E2%E5%F2", "http://example.com/foo?%EF%F0%E8%E2%E5%F2",
response=resp2, response=resp2,
encoding="cp1251",
) )
assert req.encoding == "cp1251"
def test_follow_flags(self): def test_follow_flags(self):
res = self.response_class("http://example.com/") res = self.response_class("http://example.com/")

View File

@ -28,14 +28,14 @@ class TestLogFormatter:
self.spider = Spider("default") self.spider = Spider("default")
self.spider.crawler = get_crawler() self.spider.crawler = get_crawler()
def test_crawled_with_referer(self): def test_crawled_without_referer(self):
req = Request("http://www.example.com") req = Request("http://www.example.com")
res = Response("http://www.example.com") res = Response("http://www.example.com")
logkws = self.formatter.crawled(req, res, self.spider) logkws = self.formatter.crawled(req, res, self.spider)
logline = logkws["msg"] % logkws["args"] logline = logkws["msg"] % logkws["args"]
assert logline == "Crawled (200) <GET http://www.example.com> (referer: None)" assert logline == "Crawled (200) <GET http://www.example.com> (referer: None)"
def test_crawled_without_referer(self): def test_crawled_with_referer(self):
req = Request( req = Request(
"http://www.example.com", headers={"referer": "http://example.com"} "http://www.example.com", headers={"referer": "http://example.com"}
) )
@ -198,7 +198,7 @@ class TestLogformatterSubclass(TestLogFormatter):
self.spider = Spider("default") self.spider = Spider("default")
self.spider.crawler = get_crawler(Spider) self.spider.crawler = get_crawler(Spider)
def test_crawled_with_referer(self): def test_crawled_without_referer(self):
req = Request("http://www.example.com") req = Request("http://www.example.com")
res = Response("http://www.example.com") res = Response("http://www.example.com")
logkws = self.formatter.crawled(req, res, self.spider) logkws = self.formatter.crawled(req, res, self.spider)
@ -207,7 +207,7 @@ class TestLogformatterSubclass(TestLogFormatter):
logline == "Crawled (200) <GET http://www.example.com> (referer: None) []" logline == "Crawled (200) <GET http://www.example.com> (referer: None) []"
) )
def test_crawled_without_referer(self): def test_crawled_with_referer(self):
req = Request( req = Request(
"http://www.example.com", "http://www.example.com",
headers={"referer": "http://example.com"}, headers={"referer": "http://example.com"},

View File

@ -175,10 +175,6 @@ class MockedMediaPipeline(UserDefinedPipeline):
super().__init__(*args, crawler=crawler, **kwargs) super().__init__(*args, crawler=crawler, **kwargs)
self._mockcalled = [] self._mockcalled = []
def download(self, request, info):
self._mockcalled.append("download")
return super().download(request, info)
def media_to_download(self, request, info, *, item=None): def media_to_download(self, request, info, *, item=None):
self._mockcalled.append("media_to_download") self._mockcalled.append("media_to_download")
if "result" in request.meta: if "result" in request.meta:

View File

@ -1,5 +1,3 @@
import sys
import pytest import pytest
from scrapy.robotstxt import ( from scrapy.robotstxt import (
@ -8,6 +6,7 @@ from scrapy.robotstxt import (
RerpRobotParser, RerpRobotParser,
decode_robotstxt, decode_robotstxt,
) )
from scrapy.utils._deps_compat import STDLIB_IMPROVED_ROBOTFILEPARSER
def rerp_available() -> bool: def rerp_available() -> bool:
@ -139,28 +138,25 @@ class TestDecodeRobotsTxt:
class TestPythonRobotParser(BaseRobotParserTest): class TestPythonRobotParser(BaseRobotParserTest):
# https://github.com/python/cpython/pull/149374 improves it
IMPROVED_ROBOTFILEPARSER = sys.version_info >= (3, 14, 5)
def setup_method(self): def setup_method(self):
super()._setUp(PythonRobotParser) super()._setUp(PythonRobotParser)
@pytest.mark.skipif( @pytest.mark.skipif(
not IMPROVED_ROBOTFILEPARSER, not STDLIB_IMPROVED_ROBOTFILEPARSER,
reason="RobotFileParser from this Python version does not support length based directives precedence.", reason="RobotFileParser from this Python version does not support length based directives precedence.",
) )
def test_length_based_precedence(self): def test_length_based_precedence(self):
super().test_length_based_precedence() super().test_length_based_precedence()
@pytest.mark.skipif( @pytest.mark.skipif(
IMPROVED_ROBOTFILEPARSER, STDLIB_IMPROVED_ROBOTFILEPARSER,
reason="RobotFileParser from this Python version does not support order based directives precedence.", reason="RobotFileParser from this Python version does not support order based directives precedence.",
) )
def test_order_based_precedence(self): def test_order_based_precedence(self):
super().test_order_based_precedence() super().test_order_based_precedence()
@pytest.mark.skipif( @pytest.mark.skipif(
not IMPROVED_ROBOTFILEPARSER, not STDLIB_IMPROVED_ROBOTFILEPARSER,
reason="RobotFileParser from this Python version does not support wildcards.", reason="RobotFileParser from this Python version does not support wildcards.",
) )
def test_allowed_wildcards(self): def test_allowed_wildcards(self):

View File

@ -104,7 +104,7 @@ class TestMinimalScheduler(InterfaceCheckMixin):
for url in URLS: for url in URLS:
assert self.scheduler.enqueue_request(Request(url)) assert self.scheduler.enqueue_request(Request(url))
assert not self.scheduler.enqueue_request(Request(url)) assert not self.scheduler.enqueue_request(Request(url))
assert self.scheduler.has_pending_requests assert self.scheduler.has_pending_requests()
dequeued = [] dequeued = []
while self.scheduler.has_pending_requests(): while self.scheduler.has_pending_requests():

View File

@ -238,16 +238,6 @@ class TestProcessSpiderOutputAsyncGen(TestProcessSpiderOutputSimple):
yield item yield item
class ProcessSpiderOutputNonIterableMiddleware:
def process_spider_output(self, response, result):
return
class ProcessSpiderOutputCoroutineMiddleware:
async def process_spider_output(self, response, result):
return result
class ProcessStartSimpleMiddleware: class ProcessStartSimpleMiddleware:
async def process_start(self, start): async def process_start(self, start):
async for item_or_request in start: async for item_or_request in start:
@ -423,20 +413,12 @@ class TestBuiltinMiddlewareAsyncGen(TestBuiltinMiddlewareSimple):
class TestProcessSpiderException(TestBaseAsyncSpiderMiddleware): class TestProcessSpiderException(TestBaseAsyncSpiderMiddleware):
ITEM_TYPE = dict ITEM_TYPE = dict
MW_ASYNCGEN = ProcessSpiderOutputAsyncGenMiddleware MW_ASYNCGEN = ProcessSpiderOutputAsyncGenMiddleware
MW_UNIVERSAL = ProcessSpiderOutputUniversalMiddleware
MW_EXC_SIMPLE = ProcessSpiderExceptionSimpleIterableMiddleware MW_EXC_SIMPLE = ProcessSpiderExceptionSimpleIterableMiddleware
MW_EXC_ASYNCGEN = ProcessSpiderExceptionAsyncIteratorMiddleware MW_EXC_ASYNCGEN = ProcessSpiderExceptionAsyncIteratorMiddleware
def _callback(self) -> Any: def _callback(self) -> Any:
1 / 0 1 / 0
async def _test_asyncgen_nodowngrade(self, *mw_classes: type[Any]) -> None:
with pytest.raises(
_InvalidOutput,
match=r"Async iterable returned from .+ cannot be downgraded",
):
await self._get_middleware_result(*mw_classes)
@coroutine_test @coroutine_test
async def test_exc_simple(self): async def test_exc_simple(self):
"""Simple exc mw""" """Simple exc mw"""

View File

@ -14,7 +14,6 @@ from .utils.decorators import coroutine_test
ITEM_A = {"id": "a"} ITEM_A = {"id": "a"}
ITEM_B = {"id": "b"} ITEM_B = {"id": "b"}
ITEM_C = {"id": "c"} ITEM_C = {"id": "c"}
ITEM_D = {"id": "d"}
class AsyncioSleepSpiderMiddleware: class AsyncioSleepSpiderMiddleware:
@ -47,10 +46,6 @@ class ModernWrapSpider(Spider):
yield ITEM_B yield ITEM_B
class ModernWrapSpiderSubclass(ModernWrapSpider):
name = "test"
class ModernWrapSpiderMiddleware: class ModernWrapSpiderMiddleware:
async def process_start(self, start): async def process_start(self, start):
yield ITEM_A yield ITEM_A
@ -79,10 +74,6 @@ class TestMain:
expected_items = expected_items or [ITEM_A, ITEM_B, ITEM_C] expected_items = expected_items or [ITEM_A, ITEM_B, ITEM_C]
await self._test([spider_middleware], spider_cls, expected_items) await self._test([spider_middleware], spider_cls, expected_items)
async def _test_douple_wrap(self, smw1, smw2, spider_cls, expected_items=None):
expected_items = expected_items or [ITEM_A, ITEM_A, ITEM_B, ITEM_C, ITEM_C]
await self._test([smw1, smw2], spider_cls, expected_items)
@coroutine_test @coroutine_test
async def test_modern_mw_modern_spider(self): async def test_modern_mw_modern_spider(self):
with warnings.catch_warnings(): with warnings.catch_warnings():

View File

@ -836,7 +836,7 @@ class TestRequestMetaSettingFallback:
request_meta, request_meta,
policy_class, policy_class,
check_warning, check_warning,
) in self.params[3:]: ) in self.params:
mw = RefererMiddleware(Settings(settings)) mw = RefererMiddleware(Settings(settings))
response = Response(origin, headers=response_headers) response = Response(origin, headers=response_headers)

View File

@ -1,4 +1,7 @@
from __future__ import annotations
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import TYPE_CHECKING
import pytest import pytest
@ -7,8 +10,11 @@ from scrapy.extensions.spiderstate import SpiderState
from scrapy.spiders import Spider from scrapy.spiders import Spider
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
if TYPE_CHECKING:
from pathlib import Path
def test_store_load(tmp_path):
def test_store_load(tmp_path: Path) -> None:
jobdir = str(tmp_path) jobdir = str(tmp_path)
spider = Spider(name="default") spider = Spider(name="default")
@ -16,6 +22,7 @@ def test_store_load(tmp_path):
ss = SpiderState(jobdir) ss = SpiderState(jobdir)
ss.spider_opened(spider) ss.spider_opened(spider)
assert hasattr(spider, "state")
spider.state["one"] = 1 spider.state["one"] = 1
spider.state["dt"] = dt spider.state["dt"] = dt
ss.spider_closed(spider) ss.spider_closed(spider)
@ -23,21 +30,23 @@ def test_store_load(tmp_path):
spider2 = Spider(name="default") spider2 = Spider(name="default")
ss2 = SpiderState(jobdir) ss2 = SpiderState(jobdir)
ss2.spider_opened(spider2) ss2.spider_opened(spider2)
assert spider.state == {"one": 1, "dt": dt} assert hasattr(spider2, "state")
assert spider2.state == {"one": 1, "dt": dt}
ss2.spider_closed(spider2) ss2.spider_closed(spider2)
def test_state_attribute(): def test_state_attribute() -> None:
# state attribute must be present if jobdir is not set, to provide a # state attribute must be present if jobdir is not set, to provide a
# consistent interface # consistent interface
spider = Spider(name="default") spider = Spider(name="default")
ss = SpiderState() ss = SpiderState()
ss.spider_opened(spider) ss.spider_opened(spider)
assert hasattr(spider, "state")
assert spider.state == {} assert spider.state == {}
ss.spider_closed(spider) ss.spider_closed(spider)
def test_not_configured(): def test_not_configured() -> None:
crawler = get_crawler(Spider) crawler = get_crawler(Spider)
with pytest.raises(NotConfigured): with pytest.raises(NotConfigured):
SpiderState.from_crawler(crawler) SpiderState.from_crawler(crawler)

View File

@ -83,6 +83,7 @@ class TestParallelAsyncio:
max_parallel_count, max_parallel_count,
) )
assert list(range(length)) == sorted(results) assert list(range(length)) == sorted(results)
assert parallel_count[0] == 0
assert max_parallel_count[0] <= self.CONCURRENT_ITEMS assert max_parallel_count[0] <= self.CONCURRENT_ITEMS
@coroutine_test @coroutine_test
@ -101,6 +102,7 @@ class TestParallelAsyncio:
max_parallel_count, max_parallel_count,
) )
assert list(range(length)) == sorted(results) assert list(range(length)) == sorted(results)
assert parallel_count[0] == 0
assert max_parallel_count[0] <= self.CONCURRENT_ITEMS assert max_parallel_count[0] <= self.CONCURRENT_ITEMS

View File

@ -22,9 +22,7 @@ class NewName(SomeBaseClass):
class TestWarnWhenSubclassed: class TestWarnWhenSubclassed:
def _mywarnings( def _mywarnings(self, w: list[WarningMessage]) -> list[WarningMessage]:
self, w: list[WarningMessage], category: type[Warning] = MyWarning
) -> list[WarningMessage]:
return [x for x in w if x.category is MyWarning] return [x for x in w if x.category is MyWarning]
def test_no_warning_on_definition(self): def test_no_warning_on_definition(self):

View File

@ -17,4 +17,4 @@ def test_urlparse_cached():
assert req1a == urlp assert req1a == urlp
assert req1a is req1b assert req1a is req1b
assert req1a is not req2 assert req1a is not req2
assert req1a is not req2 assert req1b is not req2

View File

@ -76,15 +76,16 @@ class TestLogCounterHandler:
@pytest.fixture @pytest.fixture
def logger(self, crawler: Crawler) -> Generator[logging.Logger]: def logger(self, crawler: Crawler) -> Generator[logging.Logger]:
logger = logging.getLogger("test") logger = logging.getLogger("test")
logger.setLevel(logging.NOTSET) logger.setLevel(logging.DEBUG)
logger.propagate = False logger.propagate = False
handler = LogCounterHandler(crawler) handler = LogCounterHandler(crawler, level=crawler.settings.get("LOG_LEVEL"))
logger.addHandler(handler) logger.addHandler(handler)
try:
yield logger yield logger
finally:
logger.propagate = True logger.propagate = True
logger.removeHandler(handler) logger.setLevel(logging.NOTSET)
logger.removeHandler(handler)
def test_init(self, crawler: Crawler, logger: logging.Logger) -> None: def test_init(self, crawler: Crawler, logger: logging.Logger) -> None:
assert crawler.stats assert crawler.stats
@ -102,7 +103,7 @@ class TestLogCounterHandler:
def test_filtered_out_level(self, crawler: Crawler, logger: logging.Logger) -> None: def test_filtered_out_level(self, crawler: Crawler, logger: logging.Logger) -> None:
logger.debug("test log msg") logger.debug("test log msg")
assert crawler.stats assert crawler.stats
assert crawler.stats.get_value("log_count/INFO") is None assert crawler.stats.get_value("log_count/DEBUG") is None
class TestStreamLogger: class TestStreamLogger:

View File

@ -23,7 +23,7 @@ def _read_browser_output(burl: str):
def test_open_in_browser(): def test_open_in_browser():
url = "http:///www.example.com/some/page.html" url = "http://www.example.com/some/page.html"
body = ( body = (
b"<html> <head> <title>test page</title> </head> <body>test body</body> </html>" b"<html> <head> <title>test page</title> </head> <body>test body</body> </html>"
) )