diff --git a/pyproject.toml b/pyproject.toml index 0bdcf6b51..a3c34a035 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,13 +116,9 @@ allow_incomplete_defs = true # 59 errors # TODO [[tool.mypy.overrides]] module = [ - "tests.spiders", - "tests.test_closespider", "tests.test_cmdline", - "tests.test_contracts", "tests.test_downloaderslotssettings", "tests.test_dupefilters", - "tests.test_engine_loop", "tests.test_extension_statsmailer", "tests.test_extension_throttle", "tests.test_linkextractors", @@ -131,15 +127,9 @@ module = [ "tests.test_pqueues", "tests.test_scheduler_base", "tests.test_settings", - "tests.test_spider", - "tests.test_spider_crawl", - "tests.test_spidermiddleware_output_chain", - "tests.test_spidermiddleware_process_start", - "tests.test_spider_sitemap", "tests.test_squeues", "tests.test_squeues_request", "tests.test_stats", - "tests.utils.bases.spider", ] check_untyped_defs = false diff --git a/tests/benchmarks/__init__.py b/tests/benchmarks/__init__.py index a066ed00a..7504ddf18 100644 --- a/tests/benchmarks/__init__.py +++ b/tests/benchmarks/__init__.py @@ -36,7 +36,6 @@ class NullDownloadHandler: async def download_request(self, request: Request) -> Response: self._active += 1 - assert self._crawler.stats self._crawler.stats.max_value("benchmark/peak_concurrency", self._active) try: await asyncio.sleep(0) diff --git a/tests/benchmarks/test_crawl.py b/tests/benchmarks/test_crawl.py index f79b66a5b..4f68e4f6f 100644 --- a/tests/benchmarks/test_crawl.py +++ b/tests/benchmarks/test_crawl.py @@ -128,7 +128,6 @@ class _DelayedPipeline: async def process_item(self, item: Any) -> Any: url = item["url"] self._active[url] += 1 - assert self._crawler.stats self._crawler.stats.max_value("benchmark/peak_items", self._active[url]) try: await asyncio.sleep(DELAY) @@ -147,7 +146,6 @@ def _crawl_tree( pages=pages, items=items, ) - assert crawler.stats assert crawler.stats.get_value("downloader/response_count") == domains * pages assert crawler.stats.get_value("item_scraped_count", 0) == domains * pages * items return crawler @@ -166,7 +164,6 @@ def test_overhead_http(benchmark: BenchmarkFixture, mockserver: MockServer) -> N def run() -> None: crawler = crawl(_FollowSpider, settings, url=url) - assert crawler.stats assert crawler.stats.get_value("item_scraped_count") == PAGES + 1 benchmark(run) @@ -177,7 +174,6 @@ def test_overhead_engine(benchmark: BenchmarkFixture) -> None: def run() -> None: crawler = _crawl_tree({}, domains=1, pages=REQUESTS) - assert crawler.stats assert crawler.stats.get_value("benchmark/peak_concurrency") > 1 benchmark(run) @@ -262,7 +258,6 @@ def test_overhead_item_concurrency(benchmark: BenchmarkFixture) -> None: crawler = _crawl_tree( settings, domains=1, pages=ITEM_REQUESTS, items=ITEMS_PER_RESPONSE ) - assert crawler.stats assert ( crawler.stats.get_value("benchmark/peak_items") == DELAYED_CONCURRENT_ITEMS ) diff --git a/tests/spiders.py b/tests/spiders.py index 7c7d3007c..3ff30ce03 100644 --- a/tests/spiders.py +++ b/tests/spiders.py @@ -13,22 +13,31 @@ from twisted.internet import defer from scrapy import signals from scrapy.exceptions import StopDownload -from scrapy.http import Request +from scrapy.http import Request, TextResponse from scrapy.item import Item from scrapy.linkextractors import LinkExtractor +from scrapy.spidermiddlewares.httperror import HttpError from scrapy.spiders import Spider from scrapy.spiders.crawl import CrawlSpider, Rule from scrapy.utils.defer import deferred_to_future, maybe_deferred_to_future from scrapy.utils.test import get_from_asyncio_queue if TYPE_CHECKING: + from collections.abc import AsyncIterator, Callable, Iterator + + from twisted.python.failure import Failure + from typing_extensions import Self + + from scrapy.crawler import Crawler + from scrapy.http import Headers, Response + from scrapy.http.request import CallbackT from tests.mockserver.http import MockServer class MockServerSpider(Spider): def __init__( self, - *args, + *args: Any, mockserver: MockServer | None = None, is_secure: bool = False, **kwargs: Any, @@ -74,7 +83,7 @@ class MetaSpider(MockServerSpider): super().__init__(*args, **kwargs) self.meta: dict[str, Any] = {} - def closed(self, reason): + def closed(self, reason: str) -> None: self.meta["close_reason"] = reason @@ -83,16 +92,24 @@ class FollowAllSpider(MetaSpider): link_extractor = LinkExtractor() def __init__( - self, total=10, show=20, order="rand", maxlatency=0.0, *args, **kwargs + self, + total: int = 10, + show: int = 20, + order: str = "rand", + maxlatency: float = 0.0, + *args: Any, + **kwargs: Any, ): super().__init__(*args, **kwargs) - self.urls_visited = [] - self.times = [] + self.urls_visited: list[str] = [] + self.times: list[float] = [] qargs = {"total": total, "show": show, "order": order, "maxlatency": maxlatency} + assert self.mockserver url = self.mockserver.url(f"/follow?{urlencode(qargs, doseq=True)}") self.start_urls = [url] - def parse(self, response): + def parse(self, response: Response) -> Iterator[Any]: + assert isinstance(response, TextResponse) self.urls_visited.append(response.url) self.times.append(time.time()) for link in self.link_extractor.extract_links(response): @@ -102,50 +119,52 @@ class FollowAllSpider(MetaSpider): class DelaySpider(MetaSpider): name = "delay" - def __init__(self, n=1, b=0, *args, **kwargs): + def __init__(self, n: float = 1, b: float = 0, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.n = n self.b = b - self.t1 = self.t2 = self.t2_err = 0 + self.t1 = self.t2 = self.t2_err = 0.0 - async def start(self): + async def start(self) -> AsyncIterator[Any]: self.t1 = time.time() + assert self.mockserver url = self.mockserver.url(f"/delay?n={self.n}&b={self.b}") yield Request(url, callback=self.parse, errback=self.errback) - def parse(self, response): + def parse(self, response: Response) -> Any: self.t2 = time.time() - def errback(self, failure): + def errback(self, failure: Failure) -> None: self.t2_err = time.time() class LogSpider(MetaSpider): name = "log_spider" - def log_debug(self, message: str, extra: dict[str, Any] | None = None): + def log_debug(self, message: str, extra: dict[str, Any] | None = None) -> None: self.logger.debug(message, extra=extra) - def log_info(self, message: str, extra: dict[str, Any] | None = None): + def log_info(self, message: str, extra: dict[str, Any] | None = None) -> None: self.logger.info(message, extra=extra) - def log_warning(self, message: str, extra: dict[str, Any] | None = None): + def log_warning(self, message: str, extra: dict[str, Any] | None = None) -> None: self.logger.warning(message, extra=extra) - def log_error(self, message: str, extra: dict[str, Any] | None = None): + def log_error(self, message: str, extra: dict[str, Any] | None = None) -> None: self.logger.error(message, extra=extra) - def log_critical(self, message: str, extra: dict[str, Any] | None = None): + def log_critical(self, message: str, extra: dict[str, Any] | None = None) -> None: self.logger.critical(message, extra=extra) - def parse(self, response): + def parse(self, response: Response) -> None: pass class SlowSpider(DelaySpider): name = "slow" - async def start(self): + async def start(self) -> AsyncIterator[Any]: + assert self.mockserver # 1st response is fast url = self.mockserver.url("/delay?n=0&b=0") yield Request(url, callback=self.parse, errback=self.errback) @@ -154,25 +173,25 @@ class SlowSpider(DelaySpider): url = self.mockserver.url(f"/delay?n={self.n}&b={self.b}") yield Request(url, callback=self.parse, errback=self.errback) - def parse(self, response): + def parse(self, response: Response) -> Iterator[Any]: yield Item() class SimpleSpider(MetaSpider): name = "simple" - def __init__(self, url="http://localhost:8998", *args, **kwargs): + def __init__(self, url: str = "http://localhost:8998", *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.start_urls = [url] - def parse(self, response): + def parse(self, response: Response) -> Any: self.logger.info(f"Got response {response.status}") class AsyncDefSpider(SimpleSpider): name = "asyncdef" - async def parse(self, response): + async def parse(self, response: Response) -> None: await defer.succeed(42) self.logger.info(f"Got response {response.status}") @@ -180,7 +199,7 @@ class AsyncDefSpider(SimpleSpider): class AsyncDefAsyncioSpider(SimpleSpider): name = "asyncdef_asyncio" - async def parse(self, response): + async def parse(self, response: Response) -> None: await asyncio.sleep(0.2) status = await get_from_asyncio_queue(response.status) self.logger.info(f"Got response {status}") @@ -189,7 +208,7 @@ class AsyncDefAsyncioSpider(SimpleSpider): class AsyncDefAsyncioReturnSpider(SimpleSpider): name = "asyncdef_asyncio_return" - async def parse(self, response): + async def parse(self, response: Response) -> Any: await asyncio.sleep(0.2) status = await get_from_asyncio_queue(response.status) self.logger.info(f"Got response {status}") @@ -199,7 +218,7 @@ class AsyncDefAsyncioReturnSpider(SimpleSpider): class AsyncDefAsyncioReturnSingleElementSpider(SimpleSpider): name = "asyncdef_asyncio_return_single_element" - async def parse(self, response): + async def parse(self, response: Response) -> Any: await asyncio.sleep(0.1) status = await get_from_asyncio_queue(response.status) self.logger.info(f"Got response {status}") @@ -209,7 +228,7 @@ class AsyncDefAsyncioReturnSingleElementSpider(SimpleSpider): class AsyncDefAsyncioReqsReturnSpider(SimpleSpider): name = "asyncdef_asyncio_reqs_return" - async def parse(self, response): + async def parse(self, response: Response) -> Any: await asyncio.sleep(0.2) req_id = response.meta.get("req_id", 0) status = await get_from_asyncio_queue(response.status) @@ -226,7 +245,7 @@ class AsyncDefAsyncioReqsReturnSpider(SimpleSpider): class AsyncDefAsyncioGenExcSpider(SimpleSpider): name = "asyncdef_asyncio_gen_exc" - async def parse(self, response): + async def parse(self, response: Response) -> AsyncIterator[Any]: for i in range(10): await asyncio.sleep(0.1) yield {"foo": i} @@ -237,7 +256,7 @@ class AsyncDefAsyncioGenExcSpider(SimpleSpider): class AsyncDefDeferredDirectSpider(SimpleSpider): name = "asyncdef_deferred_direct" - async def parse(self, response): + async def parse(self, response: Response) -> AsyncIterator[Any]: await defer.succeed(None) yield {"code": 200} @@ -245,7 +264,7 @@ class AsyncDefDeferredDirectSpider(SimpleSpider): class AsyncDefDeferredWrappedSpider(SimpleSpider): name = "asyncdef_deferred_wrapped" - async def parse(self, response): + async def parse(self, response: Response) -> AsyncIterator[Any]: await deferred_to_future(defer.succeed(None)) yield {"code": 200} @@ -253,7 +272,7 @@ class AsyncDefDeferredWrappedSpider(SimpleSpider): class AsyncDefDeferredMaybeWrappedSpider(SimpleSpider): name = "asyncdef_deferred_maybe_wrapped" - async def parse(self, response): + async def parse(self, response: Response) -> AsyncIterator[Any]: await maybe_deferred_to_future(defer.succeed(None)) yield {"code": 200} @@ -261,7 +280,7 @@ class AsyncDefDeferredMaybeWrappedSpider(SimpleSpider): class AsyncDefAsyncioGenSpider(SimpleSpider): name = "asyncdef_asyncio_gen" - async def parse(self, response): + async def parse(self, response: Response) -> AsyncIterator[Any]: await asyncio.sleep(0.2) yield {"foo": 42} self.logger.info(f"Got response {response.status}") @@ -270,7 +289,7 @@ class AsyncDefAsyncioGenSpider(SimpleSpider): class AsyncDefAsyncioGenLoopSpider(SimpleSpider): name = "asyncdef_asyncio_gen_loop" - async def parse(self, response): + async def parse(self, response: Response) -> AsyncIterator[Any]: for i in range(10): await asyncio.sleep(0.1) yield {"foo": i} @@ -283,7 +302,8 @@ class AsyncDefAsyncioGenComplexSpider(SimpleSpider): following_reqs = 3 depth = 2 - def _get_req(self, index, cb=None): + def _get_req(self, index: int, cb: CallbackT | None = None) -> Request: + assert self.mockserver return Request( self.mockserver.url(f"/status?n=200&request={index}"), meta={"index": index}, @@ -291,11 +311,11 @@ class AsyncDefAsyncioGenComplexSpider(SimpleSpider): callback=cb, ) - async def start(self): + async def start(self) -> AsyncIterator[Any]: for i in range(1, self.initial_reqs + 1): yield self._get_req(i) - async def parse(self, response): + async def parse(self, response: Response) -> AsyncIterator[Any]: index = response.meta["index"] yield {"index": index} if index < 10**self.depth: @@ -305,7 +325,7 @@ class AsyncDefAsyncioGenComplexSpider(SimpleSpider): await asyncio.sleep(0.1) yield {"index": index + 5} - async def parse2(self, response): + async def parse2(self, response: Response) -> AsyncIterator[Any]: await asyncio.sleep(0.1) yield {"index2": response.meta["index"]} @@ -313,7 +333,7 @@ class AsyncDefAsyncioGenComplexSpider(SimpleSpider): class ItemSpider(FollowAllSpider): name = "item" - def parse(self, response): + def parse(self, response: Response) -> Iterator[Any]: for request in super().parse(response): yield request yield Item() @@ -321,12 +341,18 @@ class ItemSpider(FollowAllSpider): class MaxItemsAndRequestsSpider(FollowAllSpider): - def __init__(self, max_items=10, max_requests=10, *args, **kwargs): + def __init__( + self, + max_items: int = 10, + max_requests: int = 10, + *args: Any, + **kwargs: Any, + ): super().__init__(*args, **kwargs) self.max_items = max_items self.max_requests = max_requests - def parse(self, response): + def parse(self, response: Response) -> Iterator[Any]: self.items_scraped = 0 self.pages_crawled = 1 # account for the start url for request in super().parse(response): @@ -344,12 +370,12 @@ class DefaultError(Exception): class ErrorSpider(FollowAllSpider): name = "error" - exception_cls = DefaultError + exception_cls: type[Exception] = DefaultError - def raise_exception(self): + def raise_exception(self) -> None: raise self.exception_cls("Expected exception") - def parse(self, response): + def parse(self, response: Response) -> Iterator[Any]: for request in super().parse(response): yield request self.raise_exception() @@ -359,14 +385,15 @@ class BrokenStartSpider(FollowAllSpider): fail_before_yield = False fail_yielding = False - def __init__(self, *a, **kw): + def __init__(self, *a: Any, **kw: Any): super().__init__(*a, **kw) - self.seedsseen = [] + self.seedsseen: list[Any] = [] - async def start(self): + async def start(self) -> AsyncIterator[Any]: if self.fail_before_yield: 1 / 0 + assert self.mockserver for s in range(100): qargs = {"total": 10, "seed": s} url = self.mockserver.url(f"/follow?{urlencode(qargs, doseq=True)}") @@ -376,18 +403,18 @@ class BrokenStartSpider(FollowAllSpider): assert self.seedsseen, "All seeds consumed before any download happened" - def parse(self, response): + def parse(self, response: Response) -> Iterator[Any]: self.seedsseen.append(response.meta.get("seed")) yield from super().parse(response) class StartItemSpider(FollowAllSpider): - async def start(self): + async def start(self) -> AsyncIterator[Any]: yield {"name": "test item"} class StartGoodAndBadOutput(FollowAllSpider): - async def start(self): + async def start(self) -> AsyncIterator[Any]: yield {"a": "a"} yield Request("data:,a") yield "data:,b" @@ -395,17 +422,18 @@ class StartGoodAndBadOutput(FollowAllSpider): class SingleRequestSpider(MetaSpider): - seed = None - callback_func = None - errback_func = None + seed: Request | str | None = None + callback_func: Callable[[Response], Any] | None = None + errback_func: Callable[[Failure], Any] | None = None - async def start(self): + async def start(self) -> AsyncIterator[Any]: if isinstance(self.seed, Request): yield self.seed.replace(callback=self.parse, errback=self.on_error) else: + assert self.seed yield Request(self.seed, callback=self.parse, errback=self.on_error) - def parse(self, response): + def parse(self, response: Response) -> Any: self.meta.setdefault("responses", []).append(response) if callable(self.callback_func): return self.callback_func(response) @@ -413,7 +441,7 @@ class SingleRequestSpider(MetaSpider): return response.meta["next"] return None - def on_error(self, failure): + def on_error(self, failure: Failure) -> Any: self.meta["failure"] = failure if callable(self.errback_func): return self.errback_func(failure) @@ -426,17 +454,18 @@ class DuplicateStartSpider(MockServerSpider): distinct_urls = 2 dupe_factor = 3 - async def start(self): + async def start(self) -> AsyncIterator[Any]: + assert self.mockserver for i in range(self.distinct_urls): for _ in range(self.dupe_factor): url = self.mockserver.url(f"/echo?headers=1&body=test{i}") yield Request(url, dont_filter=self.dont_filter) - def __init__(self, url="http://localhost:8998", *args, **kwargs): + def __init__(self, url: str = "http://localhost:8998", *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.visited = 0 - def parse(self, response): + def parse(self, response: Response) -> None: self.visited += 1 @@ -451,7 +480,7 @@ class CrawlSpiderWithParseMethod(MockServerSpider, CrawlSpider): } rules = (Rule(LinkExtractor(), callback="parse", follow=True),) - async def start(self): + async def start(self) -> AsyncIterator[Any]: test_body = b"""