Enable mypy warn_return_any. (#7492)

This commit is contained in:
Andrey Rakhmatullin 2026-05-05 01:24:47 +05:00 committed by GitHub
parent 33452f3aeb
commit fc14a0ce59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 87 additions and 77 deletions

View File

@ -91,7 +91,6 @@ extra_checks = false # weird addErrback() errors
untyped_calls_exclude = [ untyped_calls_exclude = [
"twisted", "twisted",
] ]
warn_return_any = false # 37 errors
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = "tests.*" module = "tests.*"

View File

@ -144,17 +144,16 @@ class Command(BaseRunSpiderCommand):
def iterate_spider_output(self, result: _T) -> Iterable[Any]: ... def iterate_spider_output(self, result: _T) -> Iterable[Any]: ...
def iterate_spider_output(self, result: Any) -> Iterable[Any] | Deferred[Any]: def iterate_spider_output(self, result: Any) -> Iterable[Any] | Deferred[Any]:
d: Deferred[Any]
if inspect.isasyncgen(result): if inspect.isasyncgen(result):
d = deferred_from_coro( d = deferred_from_coro(
collect_asyncgen(aiter_errback(result, self.handle_exception)) collect_asyncgen(aiter_errback(result, self.handle_exception))
) )
d.addCallback(self.iterate_spider_output) return d.addCallback(self.iterate_spider_output)
return d d = deferred_from_coro(result)
if inspect.iscoroutine(result): if inspect.iscoroutine(result):
d = deferred_from_coro(result) return d.addCallback(self.iterate_spider_output)
d.addCallback(self.iterate_spider_output) return arg_to_iter(d)
return d
return arg_to_iter(deferred_from_coro(result))
def add_items(self, lvl: int, new_items: list[Any]) -> None: def add_items(self, lvl: int, new_items: list[Any]) -> None:
old_items = self.items.get(lvl, []) old_items = self.items.get(lvl, [])

View File

@ -128,11 +128,12 @@ class Downloader:
) -> Generator[Deferred[Any], Any, Response | Request]: ) -> Generator[Deferred[Any], Any, Response | Request]:
self.active.add(request) self.active.add(request)
try: try:
return ( result: Response | Request = yield (
yield deferred_from_coro( deferred_from_coro(
self.middleware.download_async(self._enqueue_request, request) self.middleware.download_async(self._enqueue_request, request)
) )
) )
return result
finally: finally:
self.active.remove(request) self.active.remove(request)
@ -163,7 +164,8 @@ class Downloader:
return key, self.slots[key] return key, self.slots[key]
def get_slot_key(self, request: Request) -> str: def get_slot_key(self, request: Request) -> str:
if (meta_slot := request.meta.get(self.DOWNLOAD_SLOT)) is not None: meta_slot: str | None = request.meta.get(self.DOWNLOAD_SLOT)
if meta_slot is not None:
return meta_slot return meta_slot
key = urlparse_cached(request).hostname or "" key = urlparse_cached(request).hostname or ""

View File

@ -108,7 +108,7 @@ class _ScrapyClientContextFactory(BrowserLikePolicyForHTTPS):
def _get_cert_options(self) -> CertificateOptions: def _get_cert_options(self) -> CertificateOptions:
with _filter_method_warning(): with _filter_method_warning():
return CertificateOptions( return CertificateOptions( # type: ignore[no-any-return]
method=self._ssl_method, method=self._ssl_method,
fixBrokenPeers=True, fixBrokenPeers=True,
acceptableCiphers=self.tls_ciphers, acceptableCiphers=self.tls_ciphers,
@ -122,7 +122,7 @@ class _ScrapyClientContextFactory(BrowserLikePolicyForHTTPS):
def _get_context(self) -> SSL.Context: def _get_context(self) -> SSL.Context:
cert_options = self._get_cert_options() cert_options = self._get_cert_options()
ctx = cert_options.getContext() ctx: SSL.Context = cert_options.getContext()
ctx.set_options(0x4) # OP_LEGACY_SERVER_CONNECT ctx.set_options(0x4) # OP_LEGACY_SERVER_CONNECT
return ctx return ctx
@ -135,7 +135,7 @@ class _ScrapyClientContextFactory(BrowserLikePolicyForHTTPS):
# Otherwise use the normal Twisted function. # Otherwise use the normal Twisted function.
# Note that this doesn't use self._get_context(). # Note that this doesn't use self._get_context().
with _filter_method_warning(): with _filter_method_warning():
return optionsForClientTLS( return optionsForClientTLS( # type: ignore[no-any-return]
hostname=hostname.decode("ascii"), hostname=hostname.decode("ascii"),
extraCertificateOptions={ extraCertificateOptions={
"method": self._ssl_method, "method": self._ssl_method,
@ -186,7 +186,7 @@ class BrowserLikeContextFactory(_ScrapyClientContextFactory):
def creatorForNetloc(self, hostname: bytes, port: int) -> ClientTLSOptions: def creatorForNetloc(self, hostname: bytes, port: int) -> ClientTLSOptions:
with _filter_method_warning(): with _filter_method_warning():
return optionsForClientTLS( return optionsForClientTLS( # type: ignore[no-any-return]
hostname=hostname.decode("ascii"), hostname=hostname.decode("ascii"),
extraCertificateOptions={"method": self._ssl_method}, extraCertificateOptions={"method": self._ssl_method},
) )

View File

@ -39,7 +39,7 @@ class S3DownloadHandler(BaseDownloadHandler):
) )
) )
_http_handler = build_from_crawler( _http_handler: BaseDownloadHandler = build_from_crawler(
load_object(crawler.settings.getwithbase("DOWNLOAD_HANDLERS")["https"]), load_object(crawler.settings.getwithbase("DOWNLOAD_HANDLERS")["https"]),
crawler, crawler,
) )

View File

@ -143,7 +143,7 @@ class H2Agent:
) )
def get_endpoint(self, uri: URI) -> HostnameEndpoint: def get_endpoint(self, uri: URI) -> HostnameEndpoint:
return self.endpoint_factory.endpointForURI(uri) return self.endpoint_factory.endpointForURI(uri) # type: ignore[no-any-return]
def get_key(self, uri: URI) -> ConnectionKeyT: def get_key(self, uri: URI) -> ConnectionKeyT:
""" """
@ -187,7 +187,7 @@ class ScrapyProxyH2Agent(H2Agent):
self._proxy_uri = proxy_uri self._proxy_uri = proxy_uri
def get_endpoint(self, uri: URI) -> HostnameEndpoint: def get_endpoint(self, uri: URI) -> HostnameEndpoint:
return self.endpoint_factory.endpointForURI(self._proxy_uri) return self.endpoint_factory.endpointForURI(self._proxy_uri) # type: ignore[no-any-return]
def get_key(self, uri: URI) -> ConnectionKeyT: def get_key(self, uri: URI) -> ConnectionKeyT:
"""We use the proxy uri instead of uri obtained from request url""" """We use the proxy uri instead of uri obtained from request url"""

View File

@ -4,7 +4,7 @@ import json
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, cast
# working around https://github.com/sphinx-doc/sphinx/issues/10400 # working around https://github.com/sphinx-doc/sphinx/issues/10400
from twisted.internet.defer import Deferred # noqa: TC002 from twisted.internet.defer import Deferred # noqa: TC002
@ -334,7 +334,7 @@ class Scheduler(BaseScheduler):
cls = crawler.settings[f"SCHEDULER_START_{queue}_QUEUE"] cls = crawler.settings[f"SCHEDULER_START_{queue}_QUEUE"]
if not cls: if not cls:
return None return None
return load_object(cls) return cast("type[BaseQueue]", load_object(cls))
def has_pending_requests(self) -> bool: def has_pending_requests(self) -> bool:
return len(self) > 0 return len(self) > 0

View File

@ -74,7 +74,7 @@ class CookieJar:
@property @property
def _cookies(self) -> dict[str, dict[str, dict[str, Cookie]]]: def _cookies(self) -> dict[str, dict[str, dict[str, Cookie]]]:
return self.jar._cookies # type: ignore[attr-defined] return self.jar._cookies # type: ignore[attr-defined,no-any-return]
def clear_session_cookies(self) -> None: def clear_session_cookies(self) -> None:
return self.jar.clear_session_cookies() return self.jar.clear_session_cookies()

View File

@ -323,9 +323,10 @@ class GCSFilesStore:
) -> Deferred[StatInfo]: ) -> Deferred[StatInfo]:
blob_path = self._get_blob_path(path) blob_path = self._get_blob_path(path)
return deferred_from_coro( d: Deferred[Any] = deferred_from_coro(
run_in_thread(self.bucket.get_blob, blob_path) run_in_thread(self.bucket.get_blob, blob_path)
).addCallback(self._onsuccess) )
return d.addCallback(self._onsuccess)
def _get_content_type(self, headers: dict[str, str] | None) -> str: def _get_content_type(self, headers: dict[str, str] | None) -> str:
if headers and "Content-Type" in headers: if headers and "Content-Type" in headers:

View File

@ -29,9 +29,9 @@ from scrapy.utils.misc import arg_to_iter
from scrapy.utils.python import global_object_name from scrapy.utils.python import global_object_name
if TYPE_CHECKING: if TYPE_CHECKING:
# typing.Self requires Python 3.11 from collections.abc import Awaitable, Callable
from collections.abc import Awaitable
# typing.Self requires Python 3.11
from typing_extensions import Self from typing_extensions import Self
from scrapy import Spider from scrapy import Spider
@ -153,7 +153,7 @@ class MediaPipeline(ABC):
) -> FileInfo: ) -> FileInfo:
fp = self._fingerprinter.fingerprint(request) fp = self._fingerprinter.fingerprint(request)
eb = request.errback eb: Callable[[Failure], FileInfo] | None = request.errback
request.callback = NO_CALLBACK request.callback = NO_CALLBACK
request.errback = None request.errback = None

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import logging import logging
import sys import sys
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, cast
from urllib.robotparser import RobotFileParser from urllib.robotparser import RobotFileParser
from protego import Protego from protego import Protego
@ -103,7 +103,7 @@ class RerpRobotParser(RobotParser):
def allowed(self, url: str | bytes, user_agent: str | bytes) -> bool: def allowed(self, url: str | bytes, user_agent: str | bytes) -> bool:
user_agent = to_unicode(user_agent) user_agent = to_unicode(user_agent)
url = to_unicode(url) url = to_unicode(url)
return self.rp.is_allowed(user_agent, url) return cast("bool", self.rp.is_allowed(user_agent, url))
class ProtegoRobotParser(RobotParser): class ProtegoRobotParser(RobotParser):

View File

@ -30,7 +30,7 @@ class Root(Resource):
def _getarg( def _getarg(
request: Request, name: bytes, default: Any = None, type_: type = str request: Request, name: bytes, default: Any = None, type_: type = str
) -> Any: ) -> Any:
return type_(request.args[name][0]) if name in request.args else default # type: ignore[index,operator] return type_(request.args[name][0]) if name in request.args else default
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -13,7 +13,7 @@ import warnings
import weakref import weakref
from collections import OrderedDict from collections import OrderedDict
from collections.abc import Mapping from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, AnyStr, TypeVar from typing import TYPE_CHECKING, Any, AnyStr, TypeVar, cast
from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.exceptions import ScrapyDeprecationWarning
@ -181,7 +181,7 @@ class LocalWeakReferencedCache(weakref.WeakKeyDictionary):
def __getitem__(self, key: _KT) -> _VT | None: def __getitem__(self, key: _KT) -> _VT | None:
try: try:
return super().__getitem__(key) return cast("_VT", super().__getitem__(key))
except (TypeError, KeyError): except (TypeError, KeyError):
return None # key is either not weak-referenceable or not cached return None # key is either not weak-referenceable or not cached

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import inspect import inspect
import warnings import warnings
from functools import wraps from functools import wraps
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, overload from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast, overload
from twisted.internet.defer import Deferred, maybeDeferred from twisted.internet.defer import Deferred, maybeDeferred
@ -115,7 +115,7 @@ def _warn_spider_arg(
@wraps(func) @wraps(func)
async def async_inner(*args: _P.args, **kwargs: _P.kwargs) -> _T: async def async_inner(*args: _P.args, **kwargs: _P.kwargs) -> _T:
check_args(*args, **kwargs) check_args(*args, **kwargs)
return await func(*args, **kwargs) return cast("_T", await func(*args, **kwargs))
return async_inner return async_inner

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import inspect import inspect
import warnings import warnings
from typing import TYPE_CHECKING, Any, overload from typing import TYPE_CHECKING, Any, cast, overload
from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.utils.python import get_func_args_dict from scrapy.utils.python import get_func_args_dict
@ -68,7 +68,7 @@ def create_deprecated_class(
def __new__( # pylint: disable=bad-classmethod-argument def __new__( # pylint: disable=bad-classmethod-argument
metacls, name: str, bases: tuple[type, ...], clsdict_: dict[str, Any] metacls, name: str, bases: tuple[type, ...], clsdict_: dict[str, Any]
) -> type: ) -> type:
cls = super().__new__(metacls, name, bases, clsdict_) cls: type = super().__new__(metacls, name, bases, clsdict_)
if metacls.deprecated_class is None: if metacls.deprecated_class is None:
metacls.deprecated_class = cls metacls.deprecated_class = cls
return cls return cls
@ -100,7 +100,7 @@ def create_deprecated_class(
# is the deprecated class itself - subclasses of the # is the deprecated class itself - subclasses of the
# deprecated class should not use custom `__subclasscheck__` # deprecated class should not use custom `__subclasscheck__`
# method. # method.
return super().__subclasscheck__(sub) return cast("bool", super().__subclasscheck__(sub))
if not inspect.isclass(sub): if not inspect.isclass(sub):
raise TypeError("issubclass() arg 1 must be a class") raise TypeError("issubclass() arg 1 must be a class")

View File

@ -13,7 +13,7 @@ from contextlib import contextmanager
from functools import partial from functools import partial
from importlib import import_module from importlib import import_module
from pkgutil import iter_modules from pkgutil import iter_modules
from typing import IO, TYPE_CHECKING, Any, ParamSpec, Protocol, TypeVar, overload from typing import IO, TYPE_CHECKING, Any, ParamSpec, Protocol, TypeVar, cast, overload
from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.item import Item from scrapy.item import Item
@ -51,7 +51,7 @@ def arg_to_iter(arg: Any) -> Iterable[Any]:
if arg is None: if arg is None:
return () return ()
if not isinstance(arg, _ITERABLE_SINGLE_VALUES) and hasattr(arg, "__iter__"): if not isinstance(arg, _ITERABLE_SINGLE_VALUES) and hasattr(arg, "__iter__"):
return arg return cast("Iterable[Any]", arg)
return [arg] return [arg]

View File

@ -33,12 +33,12 @@ def listen_tcp(portrange: list[int], host: str, factory: ServerFactory) -> Port:
if len(portrange) > 2: if len(portrange) > 2:
raise ValueError(f"invalid portrange: {portrange}") raise ValueError(f"invalid portrange: {portrange}")
if not portrange: if not portrange:
return reactor.listenTCP(0, factory, interface=host) return reactor.listenTCP(0, factory, interface=host) # type: ignore[no-any-return]
if len(portrange) == 1: if len(portrange) == 1:
return reactor.listenTCP(portrange[0], factory, interface=host) return reactor.listenTCP(portrange[0], factory, interface=host) # type: ignore[no-any-return]
for x in range(portrange[0], portrange[1] + 1): for x in range(portrange[0], portrange[1] + 1):
try: try:
return reactor.listenTCP(x, factory, interface=host) return reactor.listenTCP(x, factory, interface=host) # type: ignore[no-any-return]
except error.CannotListenError: except error.CannotListenError:
if x == portrange[1]: if x == portrange[1]:
raise raise

View File

@ -7,6 +7,7 @@ import logging
import warnings import warnings
from collections.abc import Awaitable, Callable, Generator, Sequence from collections.abc import Awaitable, Callable, Generator, Sequence
from typing import Any as TypingAny from typing import Any as TypingAny
from typing import cast
from pydispatch.dispatcher import ( from pydispatch.dispatcher import (
Anonymous, Anonymous,
@ -185,7 +186,7 @@ async def _send_catch_log_asyncio(
handlers: list[Awaitable[TypingAny]] = [] handlers: list[Awaitable[TypingAny]] = []
for receiver in liveReceivers(getAllReceivers(sender, signal)): for receiver in liveReceivers(getAllReceivers(sender, signal)):
async def handler(receiver: Callable) -> TypingAny: async def handler(receiver: Callable) -> tuple[Callable, TypingAny]:
result: TypingAny result: TypingAny
try: try:
result = await ensure_awaitable( result = await ensure_awaitable(
@ -208,7 +209,10 @@ async def _send_catch_log_asyncio(
handlers.append(handler(receiver)) handlers.append(handler(receiver))
return await asyncio.gather(*handlers, return_exceptions=True) return cast(
"list[tuple[TypingAny, TypingAny]]",
await asyncio.gather(*handlers, return_exceptions=True),
)
def disconnect_all(signal: TypingAny = Any, sender: TypingAny = Any) -> None: def disconnect_all(signal: TypingAny = Any, sender: TypingAny = Any) -> None:

View File

@ -41,11 +41,10 @@ def iterate_spider_output(
) -> Iterable[Any] | AsyncGenerator[_T] | Deferred[_T]: ) -> Iterable[Any] | AsyncGenerator[_T] | Deferred[_T]:
if inspect.isasyncgen(result): if inspect.isasyncgen(result):
return result return result
d: Deferred[_T] = deferred_from_coro(result)
if inspect.iscoroutine(result): if inspect.iscoroutine(result):
d = deferred_from_coro(result) return d.addCallback(iterate_spider_output)
d.addCallback(iterate_spider_output) return arg_to_iter(d)
return d
return arg_to_iter(deferred_from_coro(result))
def iter_spider_classes(module: ModuleType) -> Iterable[type[Spider]]: def iter_spider_classes(module: ModuleType) -> Iterable[type[Spider]]:

View File

@ -239,7 +239,7 @@ class ArbitraryLengthPayloadResource(LeafResource):
class NoMetaRefreshRedirect(Redirect): class NoMetaRefreshRedirect(Redirect):
def render(self, request: server.Request) -> bytes: def render(self, request: server.Request) -> bytes:
content = Redirect.render(self, request) content: bytes = Redirect.render(self, request)
return content.replace( return content.replace(
b'http-equiv="refresh"', b'http-no-equiv="do-not-refresh-me"' b'http-equiv="refresh"', b'http-no-equiv="do-not-refresh-me"'
) )

View File

@ -1,22 +1,26 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import load_pem_x509_certificate from cryptography.x509 import load_pem_x509_certificate
from OpenSSL import SSL from OpenSSL import SSL
from OpenSSL.crypto import FILETYPE_PEM, load_certificate, load_privatekey from OpenSSL.crypto import FILETYPE_PEM, load_certificate, load_privatekey
from twisted.internet.ssl import CertificateOptions, ContextFactory from twisted.internet.ssl import CertificateOptions
from scrapy.utils._deps_compat import PYOPENSSL_WANTS_X509_PKEY from scrapy.utils._deps_compat import PYOPENSSL_WANTS_X509_PKEY
from scrapy.utils.python import to_bytes from scrapy.utils.python import to_bytes
if TYPE_CHECKING:
from twisted.internet.interfaces import IOpenSSLContextFactory
def ssl_context_factory( def ssl_context_factory(
keyfile: str = "keys/localhost.key", keyfile: str = "keys/localhost.key",
certfile: str = "keys/localhost.crt", certfile: str = "keys/localhost.crt",
cipher_string: str | None = None, cipher_string: str | None = None,
) -> ContextFactory: ) -> IOpenSSLContextFactory:
keyfile_path = Path(__file__).parent.parent / keyfile keyfile_path = Path(__file__).parent.parent / keyfile
certfile_path = Path(__file__).parent.parent / certfile certfile_path = Path(__file__).parent.parent / certfile
@ -27,7 +31,7 @@ def ssl_context_factory(
cert = load_certificate(FILETYPE_PEM, certfile_path.read_bytes()) # type: ignore[assignment] cert = load_certificate(FILETYPE_PEM, certfile_path.read_bytes()) # type: ignore[assignment]
key = load_privatekey(FILETYPE_PEM, keyfile_path.read_bytes()) # type: ignore[assignment] key = load_privatekey(FILETYPE_PEM, keyfile_path.read_bytes()) # type: ignore[assignment]
factory = CertificateOptions( factory: CertificateOptions = CertificateOptions(
privateKey=key, privateKey=key,
certificate=cert, certificate=cert,
) )

View File

@ -23,8 +23,8 @@ if TYPE_CHECKING:
class TestRobotsTxtMiddleware: class TestRobotsTxtMiddleware:
def setup_method(self): def setup_method(self) -> None:
self.crawler = mock.MagicMock() self.crawler: mock.MagicMock = mock.MagicMock()
self.crawler.settings = Settings() self.crawler.settings = Settings()
self.crawler.engine.download_async = mock.AsyncMock() self.crawler.engine.download_async = mock.AsyncMock()

View File

@ -234,7 +234,7 @@ class TestHttps2ClientProtocol:
pem = self.key_file.read_text( pem = self.key_file.read_text(
encoding="utf-8" encoding="utf-8"
) + self.certificate_file.read_text(encoding="utf-8") ) + self.certificate_file.read_text(encoding="utf-8")
return PrivateCertificate.loadPEM(pem) return PrivateCertificate.loadPEM(pem) # type: ignore[no-any-return]
@async_yield_fixture # type: ignore[untyped-decorator] @async_yield_fixture # type: ignore[untyped-decorator]
async def client( async def client(

View File

@ -4,7 +4,7 @@ import warnings
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import deque from collections import deque
from contextlib import AbstractAsyncContextManager, asynccontextmanager from contextlib import AbstractAsyncContextManager, asynccontextmanager
from typing import TYPE_CHECKING, Any, NamedTuple from typing import TYPE_CHECKING, Any, NamedTuple, cast
from unittest.mock import Mock from unittest.mock import Mock
import pytest import pytest
@ -29,9 +29,9 @@ if TYPE_CHECKING:
class MemoryScheduler(BaseScheduler): class MemoryScheduler(BaseScheduler):
paused = False paused = False
def __init__(self, *args, **kwargs): def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.queue = deque( self.queue: deque[Request] = deque(
Request(value) if isinstance(value, str) else value Request(value) if isinstance(value, str) else value
for value in getattr(self, "queue", []) for value in getattr(self, "queue", [])
) )
@ -68,7 +68,7 @@ class MockDownloader:
def get_slot_key(self, request: Request) -> str: def get_slot_key(self, request: Request) -> str:
if Downloader.DOWNLOAD_SLOT in request.meta: if Downloader.DOWNLOAD_SLOT in request.meta:
return request.meta[Downloader.DOWNLOAD_SLOT] return cast("str", request.meta[Downloader.DOWNLOAD_SLOT])
return urlparse_cached(request).hostname or "" return urlparse_cached(request).hostname or ""

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import AsyncIterator, Iterable from collections.abc import AsyncIterator, Iterable
from inspect import isasyncgen from inspect import isasyncgen
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, cast
from unittest import mock from unittest import mock
import pytest import pytest
@ -27,7 +27,7 @@ if TYPE_CHECKING:
class TestSpiderMiddleware: class TestSpiderMiddleware:
def setup_method(self): def setup_method(self) -> None:
self.request = Request("http://example.com/index.html") self.request = Request("http://example.com/index.html")
self.response = Response(self.request.url, request=self.request) self.response = Response(self.request.url, request=self.request)
self.crawler = get_crawler(Spider, {"SPIDER_MIDDLEWARES_BASE": {}}) self.crawler = get_crawler(Spider, {"SPIDER_MIDDLEWARES_BASE": {}})
@ -39,11 +39,10 @@ class TestSpiderMiddleware:
Raise exception in case of failure. Raise exception in case of failure.
""" """
def scrape_func( async def scrape_func(
response: Response | Failure, request: Request response: Response | Failure, request: Request
) -> defer.Deferred[Iterable[Any]]: ) -> Iterable[Any]:
it = mock.MagicMock() return mock.MagicMock()
return defer.succeed(it)
return await self.mwman.scrape_response_async( return await self.mwman.scrape_response_async(
scrape_func, self.response, self.request scrape_func, self.response, self.request
@ -141,7 +140,7 @@ class TestBaseAsyncSpiderMiddleware(TestSpiderMiddleware):
async def _scrape_func( async def _scrape_func(
self, response: Response | Failure, request: Request self, response: Response | Failure, request: Request
) -> Iterable[Any] | AsyncIterator[Any]: ) -> Iterable[Any] | AsyncIterator[Any]:
return self._callback() return cast("Iterable[Any] | AsyncIterator[Any]", self._callback())
async def _get_middleware_result( async def _get_middleware_result(
self, *mw_classes: type[Any], start_index: int | None = None self, *mw_classes: type[Any], start_index: int | None = None

View File

@ -35,7 +35,8 @@ def inline_callbacks_test(
async def wrapper_coro(*args: _P.args, **kwargs: _P.kwargs) -> None: async def wrapper_coro(*args: _P.args, **kwargs: _P.kwargs) -> None:
await deferred_to_future(inlineCallbacks(f)(*args, **kwargs)) await deferred_to_future(inlineCallbacks(f)(*args, **kwargs))
return wrapper_coro # Likely https://github.com/python/mypy/issues/17171
return wrapper_coro # type: ignore[no-any-return]
@wraps(f) @wraps(f)
@inlineCallbacks @inlineCallbacks

View File

@ -59,7 +59,7 @@ def test_spider_parse_override_no_kwargs() -> None:
@pytest.mark.mypy_testing @pytest.mark.mypy_testing
def test_spider_parse_override_specific_kwargs() -> None: def test_spider_parse_override_specific_kwargs() -> None:
spider = SpecificKwargsSpider() spider = SpecificKwargsSpider()
reveal_type(spider.parse) # R: def (response: scrapy.http.response.Response, page: builtins.int) -> Any reveal_type(spider.parse) # R: def (response: scrapy.http.response.Response, page: int) -> Any
@pytest.mark.mypy_testing @pytest.mark.mypy_testing

24
tox.ini
View File

@ -44,26 +44,28 @@ commands =
[testenv:typing] [testenv:typing]
basepython = python3.10 basepython = python3.10
deps = deps =
mypy==1.19.1 mypy==1.20.2
typing-extensions==4.15.0 typing-extensions==4.15.0
Pillow==12.1.1 Pillow==12.2.0
Protego==0.6.0 Protego==0.6.0
attrs==25.4.0 Twisted==25.5.0
boto3-stubs[s3]==1.42.59 attrs==26.1.0
boto3-stubs[s3]==1.43.2
botocore-stubs==1.42.41 botocore-stubs==1.42.41
h2==4.3.0 h2==4.3.0
httpx==0.28.1 httpx==0.28.1
itemadapter==0.13.1 itemadapter==0.13.1
ptpython==3.0.32 ptpython==3.0.32
ipython # newer ones require newer Python
pyOpenSSL==25.3.0 ipython==8.39.0
pytest==9.0.2 pyOpenSSL==26.1.0
types-Pygments==2.19.0.20251121 pytest==9.0.3
types-defusedxml==0.7.0.20250822 types-Pygments==2.20.0.20260408
types-defusedxml==0.7.0.20260504
types-lxml==2026.2.16 types-lxml==2026.2.16
types-pexpect==4.9.0.20260127 types-pexpect==4.9.0.20260408
uvloop==0.22.1 uvloop==0.22.1
w3lib==2.4.0 w3lib==2.4.1
zstandard==0.25.0 zstandard==0.25.0
commands = commands =
mypy {posargs:scrapy tests} mypy {posargs:scrapy tests}