diff --git a/docs/topics/api.rst b/docs/topics/api.rst index d90eb0bad..19082d9d7 100644 --- a/docs/topics/api.rst +++ b/docs/topics/api.rst @@ -280,13 +280,13 @@ class (which they all inherit from). The following methods are not part of the stats collection api but instead used when implementing custom stats collectors: - .. method:: open_spider(spider) + .. method:: open_spider() - Open the given spider for stats collection. + Open the spider for stats collection. - .. method:: close_spider(spider) + .. method:: close_spider() - Close the given spider. After this is called, no more specific stats + Close the spider. After this is called, no more specific stats can be accessed or collected. Engine API diff --git a/scrapy/core/engine.py b/scrapy/core/engine.py index 5b0ff2471..9679d780d 100644 --- a/scrapy/core/engine.py +++ b/scrapy/core/engine.py @@ -519,7 +519,7 @@ class ExecutionEngine: await maybe_deferred_to_future(d) await self.scraper.open_spider_async() assert self.crawler.stats - self.crawler.stats.open_spider(self.crawler.spider) + self.crawler.stats.open_spider() await self.signals.send_catch_log_async( signals.spider_opened, spider=self.crawler.spider ) @@ -614,7 +614,7 @@ class ExecutionEngine: assert self.crawler.stats try: - self.crawler.stats.close_spider(spider, reason=reason) + self.crawler.stats.close_spider(reason=reason) except Exception: log_failure("Stats close failure") diff --git a/scrapy/core/scheduler.py b/scrapy/core/scheduler.py index 9ae555300..bfca9cad4 100644 --- a/scrapy/core/scheduler.py +++ b/scrapy/core/scheduler.py @@ -348,11 +348,11 @@ class Scheduler(BaseScheduler): dqok = self._dqpush(request) assert self.stats is not None if dqok: - self.stats.inc_value("scheduler/enqueued/disk", spider=self.spider) + self.stats.inc_value("scheduler/enqueued/disk") else: self._mqpush(request) - self.stats.inc_value("scheduler/enqueued/memory", spider=self.spider) - self.stats.inc_value("scheduler/enqueued", spider=self.spider) + self.stats.inc_value("scheduler/enqueued/memory") + self.stats.inc_value("scheduler/enqueued") return True def next_request(self) -> Request | None: @@ -367,13 +367,13 @@ class Scheduler(BaseScheduler): request: Request | None = self.mqs.pop() assert self.stats is not None if request is not None: - self.stats.inc_value("scheduler/dequeued/memory", spider=self.spider) + self.stats.inc_value("scheduler/dequeued/memory") else: request = self._dqpop() if request is not None: - self.stats.inc_value("scheduler/dequeued/disk", spider=self.spider) + self.stats.inc_value("scheduler/dequeued/disk") if request is not None: - self.stats.inc_value("scheduler/dequeued", spider=self.spider) + self.stats.inc_value("scheduler/dequeued") return request def __len__(self) -> int: @@ -402,7 +402,7 @@ class Scheduler(BaseScheduler): ) self.logunser = False assert self.stats is not None - self.stats.inc_value("scheduler/unserializable", spider=self.spider) + self.stats.inc_value("scheduler/unserializable") return False return True diff --git a/scrapy/core/scraper.py b/scrapy/core/scraper.py index c5095f71a..769b32fc9 100644 --- a/scrapy/core/scraper.py +++ b/scrapy/core/scraper.py @@ -381,12 +381,9 @@ class Scraper: spider=self.crawler.spider, ) assert self.crawler.stats + self.crawler.stats.inc_value("spider_exceptions/count") self.crawler.stats.inc_value( - "spider_exceptions/count", spider=self.crawler.spider - ) - self.crawler.stats.inc_value( - f"spider_exceptions/{_failure.value.__class__.__name__}", - spider=self.crawler.spider, + f"spider_exceptions/{_failure.value.__class__.__name__}" ) def handle_spider_output( diff --git a/scrapy/downloadermiddlewares/httpcache.py b/scrapy/downloadermiddlewares/httpcache.py index c0d101698..d3bfe6eba 100644 --- a/scrapy/downloadermiddlewares/httpcache.py +++ b/scrapy/downloadermiddlewares/httpcache.py @@ -83,16 +83,16 @@ class HttpCacheMiddleware: spider, request ) if cachedresponse is None: - self.stats.inc_value("httpcache/miss", spider=spider) + self.stats.inc_value("httpcache/miss") if self.ignore_missing: - self.stats.inc_value("httpcache/ignore", spider=spider) + self.stats.inc_value("httpcache/ignore") raise IgnoreRequest(f"Ignored request not in cache: {request}") return None # first time request # Return cached response only if not expired cachedresponse.flags.append("cached") if self.policy.is_cached_response_fresh(cachedresponse, request): - self.stats.inc_value("httpcache/hit", spider=spider) + self.stats.inc_value("httpcache/hit") return cachedresponse # Keep a reference to cached response to avoid a second cache lookup on @@ -120,15 +120,15 @@ class HttpCacheMiddleware: # Do not validate first-hand responses cachedresponse: Response | None = request.meta.pop("cached_response", None) if cachedresponse is None: - self.stats.inc_value("httpcache/firsthand", spider=spider) + self.stats.inc_value("httpcache/firsthand") self._cache_response(spider, response, request, cachedresponse) return response if self.policy.is_cached_response_valid(cachedresponse, response, request): - self.stats.inc_value("httpcache/revalidate", spider=spider) + self.stats.inc_value("httpcache/revalidate") return cachedresponse - self.stats.inc_value("httpcache/invalidate", spider=spider) + self.stats.inc_value("httpcache/invalidate") self._cache_response(spider, response, request, cachedresponse) return response @@ -139,7 +139,7 @@ class HttpCacheMiddleware: if cachedresponse is not None and isinstance( exception, self.DOWNLOAD_EXCEPTIONS ): - self.stats.inc_value("httpcache/errorrecovery", spider=spider) + self.stats.inc_value("httpcache/errorrecovery") return cachedresponse return None @@ -151,7 +151,7 @@ class HttpCacheMiddleware: cachedresponse: Response | None, ) -> None: if self.policy.should_cache_response(response, request): - self.stats.inc_value("httpcache/store", spider=spider) + self.stats.inc_value("httpcache/store") self.storage.store_response(spider, request, response) else: - self.stats.inc_value("httpcache/uncacheable", spider=spider) + self.stats.inc_value("httpcache/uncacheable") diff --git a/scrapy/downloadermiddlewares/httpcompression.py b/scrapy/downloadermiddlewares/httpcompression.py index 314baf1dc..e3326146e 100644 --- a/scrapy/downloadermiddlewares/httpcompression.py +++ b/scrapy/downloadermiddlewares/httpcompression.py @@ -133,11 +133,8 @@ class HttpCompressionMiddleware: self.stats.inc_value( "httpcompression/response_bytes", len(decoded_body), - spider=spider, - ) - self.stats.inc_value( - "httpcompression/response_count", spider=spider ) + self.stats.inc_value("httpcompression/response_count") respcls = responsetypes.from_args( headers=response.headers, url=response.url, body=decoded_body ) diff --git a/scrapy/downloadermiddlewares/offsite.py b/scrapy/downloadermiddlewares/offsite.py index 787c46a60..682f2cb06 100644 --- a/scrapy/downloadermiddlewares/offsite.py +++ b/scrapy/downloadermiddlewares/offsite.py @@ -54,8 +54,8 @@ class OffsiteMiddleware: {"domain": domain, "request": request}, extra={"spider": spider}, ) - self.stats.inc_value("offsite/domains", spider=spider) - self.stats.inc_value("offsite/filtered", spider=spider) + self.stats.inc_value("offsite/domains") + self.stats.inc_value("offsite/filtered") raise IgnoreRequest def should_follow(self, request: Request, spider: Spider) -> bool: diff --git a/scrapy/downloadermiddlewares/stats.py b/scrapy/downloadermiddlewares/stats.py index cb5887a6f..38e9ad891 100644 --- a/scrapy/downloadermiddlewares/stats.py +++ b/scrapy/downloadermiddlewares/stats.py @@ -48,21 +48,17 @@ class DownloaderStats: def process_request( self, request: Request, spider: Spider ) -> Request | Response | None: - self.stats.inc_value("downloader/request_count", spider=spider) - self.stats.inc_value( - f"downloader/request_method_count/{request.method}", spider=spider - ) + self.stats.inc_value("downloader/request_count") + self.stats.inc_value(f"downloader/request_method_count/{request.method}") reqlen = len(request_httprepr(request)) - self.stats.inc_value("downloader/request_bytes", reqlen, spider=spider) + self.stats.inc_value("downloader/request_bytes", reqlen) return None def process_response( self, request: Request, response: Response, spider: Spider ) -> Request | Response: - self.stats.inc_value("downloader/response_count", spider=spider) - self.stats.inc_value( - f"downloader/response_status_count/{response.status}", spider=spider - ) + self.stats.inc_value("downloader/response_count") + self.stats.inc_value(f"downloader/response_status_count/{response.status}") reslen = ( len(response.body) + get_header_size(response.headers) @@ -70,15 +66,13 @@ class DownloaderStats: + 4 ) # response.body + b"\r\n"+ response.header + b"\r\n" + response.status - self.stats.inc_value("downloader/response_bytes", reslen, spider=spider) + self.stats.inc_value("downloader/response_bytes", reslen) return response def process_exception( self, request: Request, exception: Exception, spider: Spider ) -> Request | Response | None: ex_class = global_object_name(exception.__class__) - self.stats.inc_value("downloader/exception_count", spider=spider) - self.stats.inc_value( - f"downloader/exception_type_count/{ex_class}", spider=spider - ) + self.stats.inc_value("downloader/exception_count") + self.stats.inc_value(f"downloader/exception_type_count/{ex_class}") return None diff --git a/scrapy/dupefilters.py b/scrapy/dupefilters.py index a3e2c5eb4..520ec3380 100644 --- a/scrapy/dupefilters.py +++ b/scrapy/dupefilters.py @@ -152,4 +152,4 @@ class RFPDupeFilter(BaseDupeFilter): self.logdupes = False assert spider.crawler.stats - spider.crawler.stats.inc_value("dupefilter/filtered", spider=spider) + spider.crawler.stats.inc_value("dupefilter/filtered") diff --git a/scrapy/extensions/corestats.py b/scrapy/extensions/corestats.py index 779cd5d1c..1400f4c9b 100644 --- a/scrapy/extensions/corestats.py +++ b/scrapy/extensions/corestats.py @@ -35,26 +35,24 @@ class CoreStats: def spider_opened(self, spider: Spider) -> None: self.start_time = datetime.now(tz=timezone.utc) - self.stats.set_value("start_time", self.start_time, spider=spider) + self.stats.set_value("start_time", self.start_time) def spider_closed(self, spider: Spider, reason: str) -> None: assert self.start_time is not None finish_time = datetime.now(tz=timezone.utc) elapsed_time = finish_time - self.start_time elapsed_time_seconds = elapsed_time.total_seconds() - self.stats.set_value( - "elapsed_time_seconds", elapsed_time_seconds, spider=spider - ) - self.stats.set_value("finish_time", finish_time, spider=spider) - self.stats.set_value("finish_reason", reason, spider=spider) + self.stats.set_value("elapsed_time_seconds", elapsed_time_seconds) + self.stats.set_value("finish_time", finish_time) + self.stats.set_value("finish_reason", reason) def item_scraped(self, item: Any, spider: Spider) -> None: - self.stats.inc_value("item_scraped_count", spider=spider) + self.stats.inc_value("item_scraped_count") def response_received(self, spider: Spider) -> None: - self.stats.inc_value("response_received_count", spider=spider) + self.stats.inc_value("response_received_count") def item_dropped(self, item: Any, spider: Spider, exception: BaseException) -> None: reason = exception.__class__.__name__ - self.stats.inc_value("item_dropped_count", spider=spider) - self.stats.inc_value(f"item_dropped_reasons_count/{reason}", spider=spider) + self.stats.inc_value("item_dropped_count") + self.stats.inc_value(f"item_dropped_reasons_count/{reason}") diff --git a/scrapy/extensions/memdebug.py b/scrapy/extensions/memdebug.py index 3cbbb64e5..1fde6b296 100644 --- a/scrapy/extensions/memdebug.py +++ b/scrapy/extensions/memdebug.py @@ -36,12 +36,8 @@ class MemoryDebugger: def spider_closed(self, spider: Spider, reason: str) -> None: gc.collect() - self.stats.set_value( - "memdebug/gc_garbage_count", len(gc.garbage), spider=spider - ) + self.stats.set_value("memdebug/gc_garbage_count", len(gc.garbage)) for cls, wdict in live_refs.items(): if not wdict: continue - self.stats.set_value( - f"memdebug/live_refs/{cls.__name__}", len(wdict), spider=spider - ) + self.stats.set_value(f"memdebug/live_refs/{cls.__name__}", len(wdict)) diff --git a/scrapy/pipelines/files.py b/scrapy/pipelines/files.py index 8e457a4ca..f4f34aaf7 100644 --- a/scrapy/pipelines/files.py +++ b/scrapy/pipelines/files.py @@ -585,7 +585,7 @@ class FilesPipeline(MediaPipeline): {"medianame": self.MEDIA_NAME, "request": request, "referer": referer}, extra={"spider": info.spider}, ) - self.inc_stats(info.spider, "uptodate") + self.inc_stats("uptodate") checksum = result.get("checksum", None) return { @@ -663,7 +663,7 @@ class FilesPipeline(MediaPipeline): {"status": status, "request": request, "referer": referer}, extra={"spider": info.spider}, ) - self.inc_stats(info.spider, status) + self.inc_stats(status) try: path = self.file_path(request, response=response, info=info, item=item) @@ -694,10 +694,10 @@ class FilesPipeline(MediaPipeline): "status": status, } - def inc_stats(self, spider: Spider, status: str) -> None: - assert spider.crawler.stats - spider.crawler.stats.inc_value("file_count", spider=spider) - spider.crawler.stats.inc_value(f"file_status_count/{status}", spider=spider) + def inc_stats(self, status: str) -> None: + assert self.crawler.stats + self.crawler.stats.inc_value("file_count") + self.crawler.stats.inc_value(f"file_status_count/{status}") # Overridable Interface def get_media_requests( diff --git a/scrapy/spidermiddlewares/depth.py b/scrapy/spidermiddlewares/depth.py index 6b115ebe6..fecdb0142 100644 --- a/scrapy/spidermiddlewares/depth.py +++ b/scrapy/spidermiddlewares/depth.py @@ -55,22 +55,22 @@ class DepthMiddleware(BaseSpiderMiddleware): def process_spider_output( self, response: Response, result: Iterable[Any], spider: Spider ) -> Iterable[Any]: - self._init_depth(response, spider) + self._init_depth(response) yield from super().process_spider_output(response, result, spider) async def process_spider_output_async( self, response: Response, result: AsyncIterator[Any], spider: Spider ) -> AsyncIterator[Any]: - self._init_depth(response, spider) + self._init_depth(response) async for o in super().process_spider_output_async(response, result, spider): yield o - def _init_depth(self, response: Response, spider: Spider) -> None: + def _init_depth(self, response: Response) -> None: # base case (depth=0) if "depth" not in response.meta: response.meta["depth"] = 0 if self.verbose_stats: - self.stats.inc_value("request_depth_count/0", spider=spider) + self.stats.inc_value("request_depth_count/0") def get_processed_request( self, request: Request, response: Response | None @@ -90,8 +90,6 @@ class DepthMiddleware(BaseSpiderMiddleware): ) return None if self.verbose_stats: - self.stats.inc_value( - f"request_depth_count/{depth}", spider=self.crawler.spider - ) - self.stats.max_value("request_depth_max", depth, spider=self.crawler.spider) + self.stats.inc_value(f"request_depth_count/{depth}") + self.stats.max_value("request_depth_max", depth) return request diff --git a/scrapy/spidermiddlewares/urllength.py b/scrapy/spidermiddlewares/urllength.py index 5590165a5..f325ce7a0 100644 --- a/scrapy/spidermiddlewares/urllength.py +++ b/scrapy/spidermiddlewares/urllength.py @@ -49,7 +49,5 @@ class UrlLengthMiddleware(BaseSpiderMiddleware): extra={"spider": self.crawler.spider}, ) assert self.crawler.stats - self.crawler.stats.inc_value( - "urllength/request_ignored_count", spider=self.crawler.spider - ) + self.crawler.stats.inc_value("urllength/request_ignored_count") return None diff --git a/scrapy/statscollectors.py b/scrapy/statscollectors.py index f3dd0f8e7..c2fa0ae6a 100644 --- a/scrapy/statscollectors.py +++ b/scrapy/statscollectors.py @@ -4,10 +4,14 @@ Scrapy extension for collecting scraping stats from __future__ import annotations +import inspect import logging import pprint +import warnings from typing import TYPE_CHECKING, Any +from scrapy.exceptions import ScrapyDeprecationWarning + if TYPE_CHECKING: from scrapy import Spider from scrapy.crawler import Crawler @@ -23,6 +27,41 @@ class StatsCollector: def __init__(self, crawler: Crawler): self._dump: bool = crawler.settings.getbool("STATS_DUMP") self._stats: StatsT = {} + self._crawler: Crawler = crawler + + def __getattribute__(self, name): + original_attr = super().__getattribute__(name) + + if name in ( + "get_value", + "get_stats", + "set_value", + "set_stats", + "inc_value", + "max_value", + "min_value", + "clear_stats", + "open_spider", + "close_spider", + ) and callable(original_attr): + + def _deprecated_wrapper(*args, **kwargs): + sig = inspect.signature(original_attr).bind(*args, **kwargs) + sig.apply_defaults() + + if sig.arguments.get("spider"): + warnings.warn( + f"Passing a 'spider' argument to StatsCollector.{name}() is deprecated and" + f" the argument will be removed in a future Scrapy version.", + category=ScrapyDeprecationWarning, + stacklevel=2, + ) + + return original_attr(*args, **kwargs) + + return _deprecated_wrapper + + return original_attr def get_value( self, key: str, default: Any = None, spider: Spider | None = None @@ -53,18 +92,20 @@ class StatsCollector: def clear_stats(self, spider: Spider | None = None) -> None: self._stats.clear() - def open_spider(self, spider: Spider) -> None: + def open_spider(self, spider: Spider | None = None) -> None: pass - def close_spider(self, spider: Spider, reason: str) -> None: + def close_spider( + self, spider: Spider | None = None, reason: str | None = None + ) -> None: if self._dump: logger.info( "Dumping Scrapy stats:\n" + pprint.pformat(self._stats), - extra={"spider": spider}, + extra={"spider": self._crawler.spider}, ) - self._persist_stats(self._stats, spider) + self._persist_stats(self._stats) - def _persist_stats(self, stats: StatsT, spider: Spider) -> None: + def _persist_stats(self, stats: StatsT) -> None: pass @@ -73,8 +114,9 @@ class MemoryStatsCollector(StatsCollector): super().__init__(crawler) self.spider_stats: dict[str, StatsT] = {} - def _persist_stats(self, stats: StatsT, spider: Spider) -> None: - self.spider_stats[spider.name] = stats + def _persist_stats(self, stats: StatsT) -> None: + if self._crawler.spider: + self.spider_stats[self._crawler.spider.name] = stats class DummyStatsCollector(StatsCollector): diff --git a/tests/test_downloadermiddleware_httpcache.py b/tests/test_downloadermiddleware_httpcache.py index 02f4f488e..e60d18375 100644 --- a/tests/test_downloadermiddleware_httpcache.py +++ b/tests/test_downloadermiddleware_httpcache.py @@ -31,10 +31,10 @@ class TestBase: body=b"test body", status=202, ) - self.crawler.stats.open_spider(self.spider) + self.crawler.stats.open_spider() def teardown_method(self): - self.crawler.stats.close_spider(self.spider, "") + self.crawler.stats.close_spider() shutil.rmtree(self.tmpdir) def _get_settings(self, **new_settings): diff --git a/tests/test_downloadermiddleware_httpcompression.py b/tests/test_downloadermiddleware_httpcompression.py index 4389f044c..0e58de1f3 100644 --- a/tests/test_downloadermiddleware_httpcompression.py +++ b/tests/test_downloadermiddleware_httpcompression.py @@ -72,7 +72,7 @@ class TestHttpCompression: self.crawler = get_crawler(Spider) self.spider = self.crawler._create_spider("scrapytest.org") self.mw = HttpCompressionMiddleware.from_crawler(self.crawler) - self.crawler.stats.open_spider(self.spider) + self.crawler.stats.open_spider() def _getresponse(self, coding): if coding not in FORMAT: @@ -97,8 +97,8 @@ class TestHttpCompression: return response def assertStatsEqual(self, key, value): - assert self.crawler.stats.get_value(key, spider=self.spider) == value, str( - self.crawler.stats.get_stats(self.spider) + assert self.crawler.stats.get_value(key) == value, str( + self.crawler.stats.get_stats() ) def test_setting_false_compression_enabled(self): diff --git a/tests/test_downloadermiddleware_stats.py b/tests/test_downloadermiddleware_stats.py index 748ef7d76..2f1c97a34 100644 --- a/tests/test_downloadermiddleware_stats.py +++ b/tests/test_downloadermiddleware_stats.py @@ -14,14 +14,14 @@ class TestDownloaderStats: self.spider = self.crawler._create_spider("scrapytest.org") self.mw = DownloaderStats(self.crawler.stats) - self.crawler.stats.open_spider(self.spider) + self.crawler.stats.open_spider() self.req = Request("http://scrapytest.org") self.res = Response("scrapytest.org", status=400) def assertStatsEqual(self, key, value): - assert self.crawler.stats.get_value(key, spider=self.spider) == value, str( - self.crawler.stats.get_stats(self.spider) + assert self.crawler.stats.get_value(key) == value, str( + self.crawler.stats.get_stats() ) def test_process_request(self): @@ -41,4 +41,4 @@ class TestDownloaderStats: ) def teardown_method(self): - self.crawler.stats.close_spider(self.spider, "") + self.crawler.stats.close_spider() diff --git a/tests/test_spidermiddleware_depth.py b/tests/test_spidermiddleware_depth.py index 3917c5c9d..03446db7e 100644 --- a/tests/test_spidermiddleware_depth.py +++ b/tests/test_spidermiddleware_depth.py @@ -28,13 +28,13 @@ def spider(crawler: Crawler) -> Spider: @pytest.fixture -def stats(crawler: Crawler, spider: Spider) -> Generator[StatsCollector]: +def stats(crawler: Crawler) -> Generator[StatsCollector]: assert crawler.stats is not None - crawler.stats.open_spider(spider) + crawler.stats.open_spider() yield crawler.stats - crawler.stats.close_spider(spider, "") + crawler.stats.close_spider() @pytest.fixture @@ -53,7 +53,7 @@ def test_process_spider_output( out = list(mw.process_spider_output(resp, result, spider)) assert out == result - rdc = stats.get_value("request_depth_count/1", spider=spider) + rdc = stats.get_value("request_depth_count/1") assert rdc == 1 req.meta["depth"] = 1 @@ -61,5 +61,5 @@ def test_process_spider_output( out2 = list(mw.process_spider_output(resp, result, spider)) assert not out2 - rdm = stats.get_value("request_depth_max", spider=spider) + rdm = stats.get_value("request_depth_max") assert rdm == 1 diff --git a/tests/test_spidermiddleware_urllength.py b/tests/test_spidermiddleware_urllength.py index ee52223c6..ca65529a3 100644 --- a/tests/test_spidermiddleware_urllength.py +++ b/tests/test_spidermiddleware_urllength.py @@ -59,6 +59,6 @@ def test_logging( ) -> None: with caplog.at_level(INFO): process_spider_output(mw, spider) - ric = stats.get_value("urllength/request_ignored_count", spider=spider) + ric = stats.get_value("urllength/request_ignored_count") assert ric == 1 assert f"Ignoring link (url length > {maxlength})" in caplog.text diff --git a/tests/test_stats.py b/tests/test_stats.py index 0bee05616..6814a652e 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -6,6 +6,7 @@ from unittest import mock import pytest +from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.extensions.corestats import CoreStats from scrapy.spiders import Spider from scrapy.statscollectors import DummyStatsCollector, StatsCollector @@ -91,7 +92,7 @@ class TestStatsCollector: stats.min_value("test4", 7) assert stats.get_value("test4") == 7 - def test_dummy_collector(self, crawler: Crawler, spider: Spider) -> None: + def test_dummy_collector(self, crawler: Crawler) -> None: stats = DummyStatsCollector(crawler) assert stats.get_stats() == {} assert stats.get_value("anything") is None @@ -100,7 +101,20 @@ class TestStatsCollector: stats.inc_value("v1") stats.max_value("v2", 100) stats.min_value("v3", 100) - stats.open_spider(spider) - stats.set_value("test", "value", spider=spider) + stats.open_spider() + stats.set_value("test", "value") assert stats.get_stats() == {} - assert stats.get_stats(spider) == {} + + def test_deprecated_spider_arg(self, crawler: Crawler, spider: Spider) -> None: + stats = StatsCollector(crawler) + with pytest.warns( + ScrapyDeprecationWarning, + match=r"Passing a 'spider' argument to StatsCollector.set_value\(\) is deprecated", + ): + stats.set_value("test", "value", spider=spider) + assert stats.get_stats() == {"test": "value"} + with pytest.warns( + ScrapyDeprecationWarning, + match=r"Passing a 'spider' argument to StatsCollector.get_stats\(\) is deprecated", + ): + assert stats.get_stats(spider) == {"test": "value"}