diff --git a/pyproject.toml b/pyproject.toml index 5ec1d05a4..e996db7e5 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/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"""