Disable mypy allow_any_generics. (#7532)

This commit is contained in:
Andrey Rakhmatullin 2026-05-18 18:54:06 +05:00 committed by GitHub
parent f875af4a86
commit 55c17a8985
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 112 additions and 88 deletions

View File

@ -86,7 +86,6 @@ pattern = "^(?P<version>.+)$"
[tool.mypy] [tool.mypy]
strict = true strict = true
allow_any_generics = true # 67 errors
extra_checks = false # weird addErrback() errors extra_checks = false # weird addErrback() errors
untyped_calls_exclude = [ untyped_calls_exclude = [
"twisted", "twisted",

View File

@ -147,7 +147,7 @@ class Command(ScrapyCommand):
name: str, name: str,
url: str, url: str,
template_name: str, template_name: str,
template_file: str | os.PathLike, template_file: str | os.PathLike[str],
) -> None: ) -> None:
"""Generate the spider module, based on the given template""" """Generate the spider module, based on the given template"""
assert self.settings is not None assert self.settings is not None

View File

@ -27,7 +27,7 @@ class Contract:
request_cls: type[Request] | None = None request_cls: type[Request] | None = None
name: str name: str
def __init__(self, method: Callable, *args: Any): def __init__(self, method: Callable[..., Any], *args: Any):
self.testcase_pre = _create_testcase(method, f"@{self.name} pre-hook") self.testcase_pre = _create_testcase(method, f"@{self.name} pre-hook")
self.testcase_post = _create_testcase(method, f"@{self.name} post-hook") self.testcase_post = _create_testcase(method, f"@{self.name} post-hook")
self.args: tuple[Any, ...] = args self.args: tuple[Any, ...] = args
@ -105,7 +105,7 @@ class ContractsManager:
return methods return methods
def extract_contracts(self, method: Callable) -> list[Contract]: def extract_contracts(self, method: Callable[..., Any]) -> list[Contract]:
contracts: list[Contract] = [] contracts: list[Contract] = []
assert method.__doc__ is not None assert method.__doc__ is not None
for line_ in method.__doc__.split("\n"): for line_ in method.__doc__.split("\n"):
@ -134,7 +134,9 @@ class ContractsManager:
return requests return requests
def from_method(self, method: Callable, results: TestResult) -> Request | None: def from_method(
self, method: Callable[..., Any], results: TestResult
) -> Request | None:
contracts = self.extract_contracts(method) contracts = self.extract_contracts(method)
if contracts: if contracts:
request_cls = Request request_cls = Request
@ -170,7 +172,7 @@ class ContractsManager:
return None return None
def _clean_req( def _clean_req(
self, request: Request, method: Callable, results: TestResult self, request: Request, method: Callable[..., Any], results: TestResult
) -> None: ) -> None:
"""stop the request from returning objects and records any errors""" """stop the request from returning objects and records any errors"""
@ -195,7 +197,7 @@ class ContractsManager:
request.errback = eb_wrapper request.errback = eb_wrapper
def _create_testcase(method: Callable, desc: str) -> TestCase: def _create_testcase(method: Callable[..., Any], desc: str) -> TestCase:
spider = method.__self__.name # type: ignore[attr-defined] spider = method.__self__.name # type: ignore[attr-defined]
class ContractTestCase(TestCase): class ContractTestCase(TestCase):

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Any, cast
from scrapy.core.downloader.handlers.base import BaseDownloadHandler from scrapy.core.downloader.handlers.base import BaseDownloadHandler
from scrapy.exceptions import NotConfigured from scrapy.exceptions import NotConfigured
@ -9,6 +9,8 @@ from scrapy.utils.httpobj import urlparse_cached
from scrapy.utils.misc import build_from_crawler, load_object from scrapy.utils.misc import build_from_crawler, load_object
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Mapping
from scrapy import Request from scrapy import Request
from scrapy.crawler import Crawler from scrapy.crawler import Crawler
from scrapy.http import Response from scrapy.http import Response
@ -59,7 +61,7 @@ class S3DownloadHandler(BaseDownloadHandler):
awsrequest = botocore.awsrequest.AWSRequest( awsrequest = botocore.awsrequest.AWSRequest(
method=request.method, method=request.method,
url=f"{scheme}://s3.amazonaws.com/{bucket}{path}", url=f"{scheme}://s3.amazonaws.com/{bucket}{path}",
headers=request.headers.to_unicode_dict(), headers=cast("Mapping[str, Any]", request.headers.to_unicode_dict()),
data=request.body, data=request.body,
) )
assert self._signer assert self._signer

View File

@ -8,7 +8,7 @@ from __future__ import annotations
import warnings import warnings
from functools import wraps from functools import wraps
from typing import TYPE_CHECKING, Any, cast from typing import TYPE_CHECKING, Any
from scrapy.exceptions import ScrapyDeprecationWarning, _InvalidOutput from scrapy.exceptions import ScrapyDeprecationWarning, _InvalidOutput
from scrapy.http import Request, Response from scrapy.http import Request, Response
@ -87,7 +87,7 @@ class DownloaderMiddlewareManager(MiddlewareManager):
result = await self._process_exception(ex, request) result = await self._process_exception(ex, request)
return await self._process_response(result, request) return await self._process_response(result, request)
def _handle_mw_method(self, method: Callable, **kwargs: Any) -> Any: def _handle_mw_method(self, method: Callable[..., Any], **kwargs: Any) -> Any:
if method in self._mw_methods_requiring_spider: if method in self._mw_methods_requiring_spider:
kwargs["spider"] = self._spider kwargs["spider"] = self._spider
@ -99,7 +99,7 @@ class DownloaderMiddlewareManager(MiddlewareManager):
download_func: Callable[[Request], Coroutine[Any, Any, Response]], download_func: Callable[[Request], Coroutine[Any, Any, Response]],
) -> Response | Request: ) -> Response | Request:
for method in self.methods["process_request"]: for method in self.methods["process_request"]:
method = cast("Callable", method) assert method is not None
response = await ensure_awaitable( response = await ensure_awaitable(
self._handle_mw_method(method, request=request), self._handle_mw_method(method, request=request),
_warn=global_object_name(method), _warn=global_object_name(method),
@ -122,7 +122,7 @@ class DownloaderMiddlewareManager(MiddlewareManager):
return response return response
for method in self.methods["process_response"]: for method in self.methods["process_response"]:
method = cast("Callable", method) assert method is not None
response = await ensure_awaitable( response = await ensure_awaitable(
self._handle_mw_method(method, request=request, response=response), self._handle_mw_method(method, request=request, response=response),
_warn=global_object_name(method), _warn=global_object_name(method),
@ -141,7 +141,7 @@ class DownloaderMiddlewareManager(MiddlewareManager):
self, exception: Exception, request: Request | Response self, exception: Exception, request: Request | Response
) -> Response | Request: ) -> Response | Request:
for method in self.methods["process_exception"]: for method in self.methods["process_exception"]:
method = cast("Callable", method) assert method is not None
response = await ensure_awaitable( response = await ensure_awaitable(
self._handle_mw_method(method, request=request, exception=exception), self._handle_mw_method(method, request=request, exception=exception),
_warn=global_object_name(method), _warn=global_object_name(method),

View File

@ -11,7 +11,7 @@ from collections.abc import AsyncIterator, Callable, Coroutine, Iterable
from functools import wraps from functools import wraps
from inspect import isasyncgenfunction from inspect import isasyncgenfunction
from itertools import islice from itertools import islice
from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar, cast from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar
from warnings import warn from warnings import warn
from twisted.python.failure import Failure from twisted.python.failure import Failure
@ -79,7 +79,7 @@ class SpiderMiddlewareManager(MiddlewareManager):
request: Request, request: Request,
) -> Iterable[_T] | AsyncIterator[_T]: ) -> Iterable[_T] | AsyncIterator[_T]:
for method in self.methods["process_spider_input"]: for method in self.methods["process_spider_input"]:
method = cast("Callable", method) assert method is not None
try: try:
if method in self._mw_methods_requiring_spider: if method in self._mw_methods_requiring_spider:
result = method(response=response, spider=self._spider) result = method(response=response, spider=self._spider)
@ -248,9 +248,13 @@ class SpiderMiddlewareManager(MiddlewareManager):
# This method is only needed until _async compatibility methods are removed. # This method is only needed until _async compatibility methods are removed.
@staticmethod @staticmethod
def _get_process_spider_output(mw: Any) -> Callable | None: def _get_process_spider_output(mw: Any) -> Callable[..., Any] | None:
normal_method: Callable | None = getattr(mw, "process_spider_output", None) normal_method: Callable[..., Any] | None = getattr(
async_method: Callable | None = getattr(mw, "process_spider_output_async", None) mw, "process_spider_output", None
)
async_method: Callable[..., Any] | None = getattr(
mw, "process_spider_output_async", None
)
if not async_method: if not async_method:
if normal_method and not isasyncgenfunction(normal_method): if normal_method and not isasyncgenfunction(normal_method):
raise TypeError( raise TypeError(

View File

@ -313,7 +313,9 @@ class FilesystemCacheStorage:
self.expiration_secs: int = settings.getint("HTTPCACHE_EXPIRATION_SECS") self.expiration_secs: int = settings.getint("HTTPCACHE_EXPIRATION_SECS")
self.use_gzip: bool = settings.getbool("HTTPCACHE_GZIP") self.use_gzip: bool = settings.getbool("HTTPCACHE_GZIP")
# https://github.com/python/mypy/issues/10740 # https://github.com/python/mypy/issues/10740
self._open: Callable[Concatenate[str | os.PathLike, str, ...], IO[bytes]] = ( self._open: Callable[
Concatenate[str | os.PathLike[str], str, ...], IO[bytes]
] = (
gzip.open if self.use_gzip else open # type: ignore[assignment] gzip.open if self.use_gzip else open # type: ignore[assignment]
) )

View File

@ -220,7 +220,7 @@ class TextResponse(Response):
def follow_all( def follow_all(
self, self,
urls: Iterable[str | Link] | parsel.SelectorList | None = None, urls: Iterable[str | Link] | parsel.SelectorList[Any] | None = None,
callback: CallbackT | None = None, callback: CallbackT | None = None,
method: str = "GET", method: str = "GET",
headers: Mapping[AnyStr, Any] | Iterable[tuple[AnyStr, Any]] | None = None, headers: Mapping[AnyStr, Any] | Iterable[tuple[AnyStr, Any]] | None = None,

View File

@ -50,8 +50,8 @@ class MiddlewareManager(ABC):
) )
self.middlewares: tuple[Any, ...] = middlewares self.middlewares: tuple[Any, ...] = middlewares
# Only process_spider_output and process_spider_exception can be None. # Only process_spider_output and process_spider_exception can be None.
self.methods: dict[str, deque[Callable | None]] = defaultdict(deque) self.methods: dict[str, deque[Callable[..., Any] | None]] = defaultdict(deque)
self._mw_methods_requiring_spider: set[Callable] = set() self._mw_methods_requiring_spider: set[Callable[..., Any]] = set()
for mw in middlewares: for mw in middlewares:
self._add_middleware(mw) self._add_middleware(mw)
@ -116,7 +116,7 @@ class MiddlewareManager(ABC):
def _add_middleware(self, mw: Any) -> None: # noqa: B027 def _add_middleware(self, mw: Any) -> None: # noqa: B027
pass pass
def _check_mw_method_spider_arg(self, method: Callable) -> None: def _check_mw_method_spider_arg(self, method: Callable[..., Any]) -> None:
if argument_is_required(method, "spider"): if argument_is_required(method, "spider"):
warnings.warn( warnings.warn(
f"{method.__qualname__}() requires a spider argument," f"{method.__qualname__}() requires a spider argument,"

View File

@ -46,7 +46,9 @@ def _identity_process_request(request: Request, response: Response) -> Request |
return request return request
def _get_method(method: Callable | str | None, spider: Spider) -> Callable | None: def _get_method(
method: Callable[..., Any] | str | None, spider: Spider
) -> Callable[..., Any] | None:
if callable(method): if callable(method):
return method return method
if isinstance(method, str): if isinstance(method, str):

View File

@ -26,7 +26,7 @@ if TYPE_CHECKING:
def _with_mkdir(queue_class: type[queue.BaseQueue]) -> type[queue.BaseQueue]: def _with_mkdir(queue_class: type[queue.BaseQueue]) -> type[queue.BaseQueue]:
class DirectoriesCreated(queue_class): # type: ignore[valid-type,misc] class DirectoriesCreated(queue_class): # type: ignore[valid-type,misc]
def __init__(self, path: str | PathLike, *args: Any, **kwargs: Any): def __init__(self, path: str | PathLike[str], *args: Any, **kwargs: Any):
dirname = Path(path).parent dirname = Path(path).parent
if not dirname.exists(): if not dirname.exists():
dirname.mkdir(parents=True, exist_ok=True) dirname.mkdir(parents=True, exist_ok=True)

View File

@ -148,7 +148,7 @@ class AsyncioLoopingCall:
self._func: Callable[_P, _T] = func self._func: Callable[_P, _T] = func
self._args: tuple[Any, ...] = args self._args: tuple[Any, ...] = args
self._kwargs: dict[str, Any] = kwargs self._kwargs: dict[str, Any] = kwargs
self._task: asyncio.Task | None = None self._task: asyncio.Task[None] | None = None
self.interval: float | None = None self.interval: float | None = None
self._start_time: float | None = None self._start_time: float | None = None

View File

@ -71,8 +71,8 @@ def arglist_to_dict(arglist: list[str]) -> dict[str, str]:
def closest_scrapy_cfg( def closest_scrapy_cfg(
path: str | os.PathLike = ".", path: str | os.PathLike[str] = ".",
prevpath: str | os.PathLike | None = None, prevpath: str | os.PathLike[str] | None = None,
) -> str: ) -> str:
"""Return the path to the closest scrapy.cfg file by traversing the current """Return the path to the closest scrapy.cfg file by traversing the current
directory and its parents directory and its parents

View File

@ -28,7 +28,7 @@ _KT = TypeVar("_KT")
_VT = TypeVar("_VT") _VT = TypeVar("_VT")
class CaselessDict(dict): class CaselessDict(dict): # type: ignore[type-arg]
__slots__ = () __slots__ = ()
def __new__(cls, *args: Any, **kwargs: Any) -> Self: def __new__(cls, *args: Any, **kwargs: Any) -> Self:
@ -99,20 +99,20 @@ class CaselessDict(dict):
return dict.pop(self, self.normkey(key), *args) return dict.pop(self, self.normkey(key), *args)
class CaseInsensitiveDict(collections.UserDict): class CaseInsensitiveDict(collections.UserDict[str | bytes, Any]):
"""A dict-like structure that accepts strings or bytes """A dict-like structure that accepts strings or bytes
as keys and allows case-insensitive lookups. as keys and allows case-insensitive lookups.
""" """
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
self._keys: dict = {} self._keys: dict[str | bytes, Any] = {}
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def __getitem__(self, key: AnyStr) -> Any: def __getitem__(self, key: str | bytes) -> Any:
normalized_key = self._normkey(key) normalized_key = self._normkey(key)
return super().__getitem__(self._keys[normalized_key.lower()]) return super().__getitem__(self._keys[normalized_key.lower()])
def __setitem__(self, key: AnyStr, value: Any) -> None: def __setitem__(self, key: str | bytes, value: Any) -> None:
normalized_key = self._normkey(key) normalized_key = self._normkey(key)
try: try:
lower_key = self._keys[normalized_key.lower()] lower_key = self._keys[normalized_key.lower()]
@ -122,19 +122,19 @@ class CaseInsensitiveDict(collections.UserDict):
super().__setitem__(normalized_key, self._normvalue(value)) super().__setitem__(normalized_key, self._normvalue(value))
self._keys[normalized_key.lower()] = normalized_key self._keys[normalized_key.lower()] = normalized_key
def __delitem__(self, key: AnyStr) -> None: def __delitem__(self, key: str | bytes) -> None:
normalized_key = self._normkey(key) normalized_key = self._normkey(key)
stored_key = self._keys.pop(normalized_key.lower()) stored_key = self._keys.pop(normalized_key.lower())
super().__delitem__(stored_key) super().__delitem__(stored_key)
def __contains__(self, key: AnyStr) -> bool: # type: ignore[override] def __contains__(self, key: str | bytes) -> bool: # type: ignore[override]
normalized_key = self._normkey(key) normalized_key = self._normkey(key)
return normalized_key.lower() in self._keys return normalized_key.lower() in self._keys
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{self.__class__.__name__}: {super().__repr__()}>" return f"<{self.__class__.__name__}: {super().__repr__()}>"
def _normkey(self, key: AnyStr) -> AnyStr: def _normkey(self, key: str | bytes) -> str | bytes:
return key return key
def _normvalue(self, value: Any) -> Any: def _normvalue(self, value: Any) -> Any:
@ -158,7 +158,7 @@ class LocalCache(OrderedDict[_KT, _VT]):
super().__setitem__(key, value) super().__setitem__(key, value)
class LocalWeakReferencedCache(weakref.WeakKeyDictionary): class LocalWeakReferencedCache(weakref.WeakKeyDictionary[_KT, _VT | None]):
""" """
A weakref.WeakKeyDictionary implementation that uses LocalCache as its A weakref.WeakKeyDictionary implementation that uses LocalCache as its
underlying data structure, making it ordered and capable of being size-limited. underlying data structure, making it ordered and capable of being size-limited.
@ -172,9 +172,9 @@ class LocalWeakReferencedCache(weakref.WeakKeyDictionary):
def __init__(self, limit: int | None = None): def __init__(self, limit: int | None = None):
super().__init__() super().__init__()
self.data: LocalCache = LocalCache(limit=limit) self.data: LocalCache[_KT, _VT] = LocalCache(limit=limit)
def __setitem__(self, key: _KT, value: _VT) -> None: def __setitem__(self, key: _KT, value: _VT | None) -> None:
# if raised, key is not weak-referenceable, skip caching # if raised, key is not weak-referenceable, skip caching
with contextlib.suppress(TypeError): with contextlib.suppress(TypeError):
super().__setitem__(key, value) super().__setitem__(key, value)

View File

@ -173,7 +173,7 @@ def parallel(
return DeferredList([coop.coiterate(work) for _ in range(count)]) return DeferredList([coop.coiterate(work) for _ in range(count)])
class _AsyncCooperatorAdapter(Iterator, Generic[_T]): class _AsyncCooperatorAdapter(Iterator[Deferred[Any]], Generic[_T]):
"""A class that wraps an async iterable into a normal iterator suitable """A class that wraps an async iterable into a normal iterator suitable
for using in Cooperator.coiterate(). As it's only needed for parallel_async(), for using in Cooperator.coiterate(). As it's only needed for parallel_async(),
it calls the callable directly in the callback, instead of providing a more it calls the callable directly in the callback, instead of providing a more

View File

@ -260,7 +260,8 @@ def logformatter_adapter(
return (level, message, args) return (level, message, args)
class SpiderLoggerAdapter(logging.LoggerAdapter): # LoggerAdapter is only parameterized since Python 3.11
class SpiderLoggerAdapter(logging.LoggerAdapter): # type: ignore[type-arg]
def process( def process(
self, msg: str, kwargs: MutableMapping[str, Any] self, msg: str, kwargs: MutableMapping[str, Any]
) -> tuple[str, MutableMapping[str, Any]]: ) -> tuple[str, MutableMapping[str, Any]]:

View File

@ -28,7 +28,7 @@ if TYPE_CHECKING:
_ITERABLE_SINGLE_VALUES = dict, Item, str, bytes _ITERABLE_SINGLE_VALUES = dict, Item, str, bytes
_ITER_T = TypeVar("_ITER_T", bound=dict | Item | str | bytes) _ITER_T = TypeVar("_ITER_T", bound=dict[Any, Any] | Item | str | bytes)
_T = TypeVar("_T") _T = TypeVar("_T")
_T_co = TypeVar("_T_co", covariant=True) _T_co = TypeVar("_T_co", covariant=True)
_P = ParamSpec("_P") _P = ParamSpec("_P")
@ -252,7 +252,9 @@ def walk_callable(node: ast.AST) -> Iterable[ast.AST]:
yield node yield node
_generator_callbacks_cache = LocalWeakReferencedCache(limit=128) _generator_callbacks_cache: LocalWeakReferencedCache[Callable[..., Any], bool] = (
LocalWeakReferencedCache(limit=128)
)
def _returns_none(return_node: ast.Return) -> bool: def _returns_none(return_node: ast.Return) -> bool:

View File

@ -55,7 +55,7 @@ class CallLaterOnce(Generic[_T]):
self._a: tuple[Any, ...] = a self._a: tuple[Any, ...] = a
self._kw: dict[str, Any] = kw self._kw: dict[str, Any] = kw
self._call: CallLaterResult | None = None self._call: CallLaterResult | None = None
self._deferreds: list[Deferred] = [] self._deferreds: list[Deferred[None]] = []
def schedule(self, delay: float = 0) -> None: def schedule(self, delay: float = 0) -> None:
# circular import # circular import

View File

@ -186,7 +186,9 @@ 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) -> tuple[Callable, TypingAny]: async def handler(
receiver: Callable[..., Any],
) -> tuple[Callable[..., Any], TypingAny]:
result: TypingAny result: TypingAny
try: try:
result = await ensure_awaitable( result = await ensure_awaitable(

View File

@ -11,7 +11,7 @@ if TYPE_CHECKING:
from os import PathLike from os import PathLike
def render_templatefile(path: str | PathLike, **kwargs: Any) -> None: def render_templatefile(path: str | PathLike[str], **kwargs: Any) -> None:
path_obj = Path(path) path_obj = Path(path)
raw = path_obj.read_text("utf8") raw = path_obj.read_text("utf8")

View File

@ -30,7 +30,9 @@ if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
live_refs: defaultdict[type, WeakKeyDictionary] = defaultdict(WeakKeyDictionary) live_refs: defaultdict[type, WeakKeyDictionary[object, float]] = defaultdict(
WeakKeyDictionary
)
class object_ref: class object_ref:

View File

@ -23,7 +23,7 @@ class NoRequestsSpider(scrapy.Spider):
yield yield
def log_task_exception(task: Task) -> None: def log_task_exception(task: Task[None]) -> None:
try: try:
task.result() task.result()
except Exception: except Exception:

View File

@ -17,7 +17,7 @@ class AsyncioReactorSpider(scrapy.Spider):
} }
def log_task_exception(task: Task) -> None: def log_task_exception(task: Task[None]) -> None:
try: try:
task.result() task.result()
except Exception: except Exception:

View File

@ -4,7 +4,7 @@ from collections import defaultdict
from typing import Any from typing import Any
class DummyDB(dict): class DummyDB(dict): # type: ignore[type-arg]
"""Provide dummy DBM-like interface.""" """Provide dummy DBM-like interface."""
def close(self): def close(self):

View File

@ -22,6 +22,7 @@ from scrapy.utils.defer import deferred_to_future, maybe_deferred_to_future
from scrapy.utils.test import get_from_asyncio_queue from scrapy.utils.test import get_from_asyncio_queue
if TYPE_CHECKING: if TYPE_CHECKING:
from scrapy.settings import _SettingsKey
from tests.mockserver.http import MockServer from tests.mockserver.http import MockServer
@ -94,19 +95,19 @@ class DelaySpider(MetaSpider):
class LogSpider(MetaSpider): class LogSpider(MetaSpider):
name = "log_spider" name = "log_spider"
def log_debug(self, message: str, extra: dict | None = None): def log_debug(self, message: str, extra: dict[str, Any] | None = None):
self.logger.debug(message, extra=extra) self.logger.debug(message, extra=extra)
def log_info(self, message: str, extra: dict | None = None): def log_info(self, message: str, extra: dict[str, Any] | None = None):
self.logger.info(message, extra=extra) self.logger.info(message, extra=extra)
def log_warning(self, message: str, extra: dict | None = None): def log_warning(self, message: str, extra: dict[str, Any] | None = None):
self.logger.warning(message, extra=extra) self.logger.warning(message, extra=extra)
def log_error(self, message: str, extra: dict | None = None): def log_error(self, message: str, extra: dict[str, Any] | None = None):
self.logger.error(message, extra=extra) self.logger.error(message, extra=extra)
def log_critical(self, message: str, extra: dict | None = None): def log_critical(self, message: str, extra: dict[str, Any] | None = None):
self.logger.critical(message, extra=extra) self.logger.critical(message, extra=extra)
def parse(self, response): def parse(self, response):
@ -417,7 +418,7 @@ class CrawlSpiderWithParseMethod(MockServerSpider, CrawlSpider):
""" """
name = "crawl_spider_with_parse_method" name = "crawl_spider_with_parse_method"
custom_settings: dict = { custom_settings: dict[_SettingsKey, Any] = {
"RETRY_HTTP_CODES": [], # no need to retry "RETRY_HTTP_CODES": [], # no need to retry
} }
rules = (Rule(LinkExtractor(), callback="parse", follow=True),) rules = (Rule(LinkExtractor(), callback="parse", follow=True),)

View File

@ -9,7 +9,7 @@ from tests.test_commands import TestProjectBase
from tests.utils.cmdline import call, proc from tests.utils.cmdline import call, proc
def find_in_file(filename: Path, regex: str) -> re.Match | None: def find_in_file(filename: Path, regex: str) -> re.Match[str] | None:
"""Find first pattern occurrence in file""" """Find first pattern occurrence in file"""
pattern = re.compile(regex) pattern = re.compile(regex)
with filename.open("r", encoding="utf-8") as f: with filename.open("r", encoding="utf-8") as f:

View File

@ -4,7 +4,7 @@ import os
import sys import sys
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING
import pytest import pytest
from pexpect.popen_spawn import PopenSpawn from pexpect.popen_spawn import PopenSpawn
@ -151,8 +151,7 @@ class TestInteractiveShell:
env = os.environ.copy() env = os.environ.copy()
env["SCRAPY_PYTHON_SHELL"] = "python" env["SCRAPY_PYTHON_SHELL"] = "python"
logfile = BytesIO() logfile = BytesIO()
# https://github.com/python/typeshed/issues/14915 p = PopenSpawn(args, env=env, timeout=5)
p = PopenSpawn(args, env=cast("os._Environ", env), timeout=5)
p.logfile_read = logfile p.logfile_read = logfile
p.expect_exact("Available Scrapy objects") p.expect_exact("Available Scrapy objects")
p.sendline(f"fetch('{mockserver.url('/')}')") p.sendline(f"fetch('{mockserver.url('/')}')")

View File

@ -79,7 +79,7 @@ class TestStartprojectCommand:
def get_permissions_dict( def get_permissions_dict(
path: str | os.PathLike, renamings=None, ignore=None path: str | os.PathLike[str], renamings=None, ignore=None
) -> dict[str, str]: ) -> dict[str, str]:
def get_permissions(path: Path) -> str: def get_permissions(path: Path) -> str:
return oct(path.stat().st_mode) return oct(path.stat().st_mode)

View File

@ -41,7 +41,7 @@ from tests.spiders import ItemSpider
from tests.utils.decorators import coroutine_test, inline_callbacks_test from tests.utils.decorators import coroutine_test, inline_callbacks_test
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable, Iterable from collections.abc import Awaitable, Callable, Iterable
def path_to_url(path: str | Path) -> str: def path_to_url(path: str | Path) -> str:
@ -1312,7 +1312,9 @@ class TestFeedExporterSignals:
self.feed_slot_closed_received = True self.feed_slot_closed_received = True
async def run_signaled_feed_exporter( async def run_signaled_feed_exporter(
self, feed_exporter_signal_handler: Callable, feed_slot_signal_handler: Callable self,
feed_exporter_signal_handler: Callable[[], Awaitable[None] | None],
feed_slot_signal_handler: Callable[[Any], Awaitable[None] | None],
) -> None: ) -> None:
crawler = get_crawler(settings_dict=self.settings) crawler = get_crawler(settings_dict=self.settings)
feed_exporter = FeedExporter.from_crawler(crawler) feed_exporter = FeedExporter.from_crawler(crawler)

View File

@ -29,7 +29,7 @@ if TYPE_CHECKING:
from os import PathLike from os import PathLike
def build_url(path: str | PathLike) -> str: def build_url(path: str | PathLike[str]) -> str:
path_str = str(path) path_str = str(path)
if path_str[0] != "/": if path_str[0] != "/":
path_str = "/" + path_str path_str = "/" + path_str

View File

@ -61,7 +61,7 @@ def make_html_body(val: str) -> bytes:
class DummySpider(Spider): class DummySpider(Spider):
name = "dummy" name = "dummy"
start_urls: list = [] start_urls = []
def parse(self, response): def parse(self, response):
print(response) print(response)

View File

@ -39,7 +39,7 @@ class AttrsNameItem:
@dataclasses.dataclass @dataclasses.dataclass
class NameDataClass: class NameDataClass:
name: list = dataclasses.field(default_factory=list) name: list[str] = dataclasses.field(default_factory=list)
# test item loaders # test item loaders

View File

@ -383,11 +383,11 @@ class TestFilesPipelineFieldsItem(TestFilesPipelineFieldsMixin):
class FilesPipelineTestDataClass: class FilesPipelineTestDataClass:
name: str name: str
# default fields # default fields
file_urls: list = dataclasses.field(default_factory=list) file_urls: list[str] = dataclasses.field(default_factory=list)
files: list = dataclasses.field(default_factory=list) files: list[dict[str, str]] = dataclasses.field(default_factory=list)
# overridden fields # overridden fields
custom_file_urls: list = dataclasses.field(default_factory=list) custom_file_urls: list[str] = dataclasses.field(default_factory=list)
custom_files: list = dataclasses.field(default_factory=list) custom_files: list[dict[str, str]] = dataclasses.field(default_factory=list)
class TestFilesPipelineFieldsDataClass(TestFilesPipelineFieldsMixin): class TestFilesPipelineFieldsDataClass(TestFilesPipelineFieldsMixin):

View File

@ -314,11 +314,11 @@ class TestImagesPipelineFieldsItem(TestImagesPipelineFieldsMixin):
class ImagesPipelineTestDataClass: class ImagesPipelineTestDataClass:
name: str name: str
# default fields # default fields
image_urls: list = dataclasses.field(default_factory=list) image_urls: list[str] = dataclasses.field(default_factory=list)
images: list = dataclasses.field(default_factory=list) images: list[dict[str, str]] = dataclasses.field(default_factory=list)
# overridden fields # overridden fields
custom_image_urls: list = dataclasses.field(default_factory=list) custom_image_urls: list[str] = dataclasses.field(default_factory=list)
custom_images: list = dataclasses.field(default_factory=list) custom_images: list[dict[str, str]] = dataclasses.field(default_factory=list)
class TestImagesPipelineFieldsDataClass(TestImagesPipelineFieldsMixin): class TestImagesPipelineFieldsDataClass(TestImagesPipelineFieldsMixin):

View File

@ -41,10 +41,10 @@ class MinimalScheduler:
class SimpleScheduler(MinimalScheduler): class SimpleScheduler(MinimalScheduler):
def open(self, spider: Spider) -> defer.Deferred: def open(self, spider: Spider) -> defer.Deferred[str]:
return defer.succeed("open") return defer.succeed("open")
def close(self, reason: str) -> defer.Deferred: def close(self, reason: str) -> defer.Deferred[str]:
return defer.succeed("close") return defer.succeed("close")
def __len__(self) -> int: def __len__(self) -> int:

View File

@ -110,7 +110,7 @@ class TestBaseAsyncSpiderMiddleware(TestSpiderMiddleware):
Should work for process_spider_output and, when it's supported, process_start. Should work for process_spider_output and, when it's supported, process_start.
""" """
ITEM_TYPE: type | tuple ITEM_TYPE: type | tuple[type, ...]
RESULT_COUNT = 3 # to simplify checks, let everything return 3 objects RESULT_COUNT = 3 # to simplify checks, let everything return 3 objects
@staticmethod @staticmethod

View File

@ -2,6 +2,7 @@ import copy
import warnings import warnings
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Iterator, Mapping, MutableMapping from collections.abc import Iterator, Mapping, MutableMapping
from typing import Any
import pytest import pytest
@ -20,7 +21,7 @@ from scrapy.utils.python import garbage_collect
class TestCaseInsensitiveDictBase(ABC): class TestCaseInsensitiveDictBase(ABC):
@property @property
@abstractmethod @abstractmethod
def dict_class(self) -> type[MutableMapping]: def dict_class(self) -> type[MutableMapping[str, Any]]:
raise NotImplementedError raise NotImplementedError
def test_init_dict(self): def test_init_dict(self):
@ -206,7 +207,7 @@ class TestCaseInsensitiveDictBase(ABC):
class TestCaseInsensitiveDict(TestCaseInsensitiveDictBase): class TestCaseInsensitiveDict(TestCaseInsensitiveDictBase):
dict_class = CaseInsensitiveDict dict_class = CaseInsensitiveDict # type: ignore[assignment]
def test_repr(self): def test_repr(self):
d1 = self.dict_class({"foo": "bar"}) d1 = self.dict_class({"foo": "bar"})

View File

@ -144,7 +144,9 @@ class TestStreamLogger:
], ],
) )
def test_spider_logger_adapter_process( def test_spider_logger_adapter_process(
base_extra: Mapping[str, Any], log_extra: MutableMapping, expected_extra: dict base_extra: Mapping[str, Any],
log_extra: MutableMapping[str, Any],
expected_extra: dict[str, Any],
) -> None: ) -> None:
logger = logging.getLogger("test") logger = logging.getLogger("test")
spider_logger_adapter = SpiderLoggerAdapter(logger, base_extra) spider_logger_adapter = SpiderLoggerAdapter(logger, base_extra)

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import json import json
from hashlib import sha1 from hashlib import sha1
from typing import Any
from weakref import WeakKeyDictionary from weakref import WeakKeyDictionary
import pytest import pytest
@ -56,13 +57,13 @@ def test_request_httprepr_for_non_http_request(r: Request) -> None:
class TestFingerprint: class TestFingerprint:
function: staticmethod = staticmethod(fingerprint) function: staticmethod[[Request], bytes] = staticmethod(fingerprint)
cache: ( cache: (
WeakKeyDictionary[Request, dict[tuple[tuple[bytes, ...] | None, bool], bytes]] WeakKeyDictionary[Request, dict[tuple[tuple[bytes, ...] | None, bool], bytes]]
| WeakKeyDictionary[Request, dict[tuple[tuple[bytes, ...] | None, bool], str]] | WeakKeyDictionary[Request, dict[tuple[tuple[bytes, ...] | None, bool], str]]
) = _fingerprint_cache ) = _fingerprint_cache
default_cache_key = (None, False) default_cache_key = (None, False)
known_hashes: tuple[tuple[Request, bytes | str, dict], ...] = ( known_hashes: tuple[tuple[Request, bytes | str, dict[str, Any]], ...] = (
( (
Request("http://example.org"), Request("http://example.org"),
b"xs\xd7\x0c3uj\x15\xfe\xd7d\x9b\xa9\t\xe0d\xbf\x9cXD", b"xs\xd7\x0c3uj\x15\xfe\xd7d\x9b\xa9\t\xe0d\xbf\x9cXD",

10
tox.ini
View File

@ -44,13 +44,13 @@ commands =
[testenv:typing] [testenv:typing]
basepython = python3.10 basepython = python3.10
deps = deps =
mypy==1.20.2 mypy==2.1.0
typing-extensions==4.15.0 typing-extensions==4.15.0
Pillow==12.2.0 Pillow==12.2.0
Protego==0.6.0 Protego==0.6.0
Twisted==26.4.0 Twisted==26.4.0
attrs==26.1.0 attrs==26.1.0
boto3-stubs[s3]==1.43.2 boto3-stubs[s3]==1.43.9
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
@ -58,12 +58,12 @@ deps =
ptpython==3.0.32 ptpython==3.0.32
# newer ones require newer Python # newer ones require newer Python
ipython==8.39.0 ipython==8.39.0
pyOpenSSL==26.1.0 pyOpenSSL==26.2.0
pytest==9.0.3 pytest==9.0.3
types-Pygments==2.20.0.20260408 types-Pygments==2.20.0.20260508
types-defusedxml==0.7.0.20260504 types-defusedxml==0.7.0.20260504
types-lxml==2026.2.16 types-lxml==2026.2.16
types-pexpect==4.9.0.20260408 types-pexpect==4.9.0.20260508
uvloop==0.22.1 uvloop==0.22.1
w3lib==2.4.1 w3lib==2.4.1
zstandard==0.25.0 zstandard==0.25.0