diff --git a/scrapy/utils/decorators.py b/scrapy/utils/decorators.py index 2924c81f9..bd9c435de 100644 --- a/scrapy/utils/decorators.py +++ b/scrapy/utils/decorators.py @@ -109,12 +109,23 @@ def _warn_spider_arg( | Callable[_P, AsyncGenerator[_T]] ): """Decorator to warn if a ``spider`` argument is passed to a function.""" + parameters = _signature(func).parameters + spider_parameter = parameters.get("spider") + spider_index = ( + list(parameters).index("spider") + if spider_parameter is not None + and spider_parameter.kind + in ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + ) + else None + ) - sig = _signature(func) - - def check_args(*args: _P.args, **kwargs: _P.kwargs) -> None: - bound = sig.bind(*args, **kwargs) - if "spider" in bound.arguments: + def check_args(args: tuple[Any, ...], kwargs: dict[str, Any]) -> None: + if "spider" in kwargs or ( + spider_index is not None and len(args) > spider_index + ): warnings.warn( f"Passing a 'spider' argument to {func.__qualname__}() is deprecated and " "the argument will be removed in a future Scrapy version.", @@ -126,7 +137,7 @@ def _warn_spider_arg( @wraps(func) async def async_inner(*args: _P.args, **kwargs: _P.kwargs) -> _T: - check_args(*args, **kwargs) + check_args(args, kwargs) return cast("_T", await func(*args, **kwargs)) return async_inner @@ -137,7 +148,7 @@ def _warn_spider_arg( async def asyncgen_inner( *args: _P.args, **kwargs: _P.kwargs ) -> AsyncGenerator[_T]: - check_args(*args, **kwargs) + check_args(args, kwargs) async for item in func(*args, **kwargs): yield item @@ -145,7 +156,7 @@ def _warn_spider_arg( @wraps(func) def sync_inner(*args: _P.args, **kwargs: _P.kwargs) -> _T: - check_args(*args, **kwargs) + check_args(args, kwargs) return func(*args, **kwargs) return sync_inner diff --git a/tests/test_utils_decorators.py b/tests/test_utils_decorators.py index 4c29d2917..92677882e 100644 --- a/tests/test_utils_decorators.py +++ b/tests/test_utils_decorators.py @@ -103,6 +103,26 @@ class TestWarnSpiderArg: ): assert parse("response", spider="spider") == "response" + def test_sync_warns_with_positional_spider_arg(self): + @_warn_spider_arg + def parse(response: str, spider: str | None = None) -> str: + return response + + with pytest.warns( + ScrapyDeprecationWarning, match=r"Passing a 'spider' argument" + ): + assert parse("response", "spider") == "response" + + def test_sync_warns_with_keyword_only_spider_arg(self): + @_warn_spider_arg + def parse(response: str, *, spider: str | None = None) -> str: + return response + + with pytest.warns( + ScrapyDeprecationWarning, match=r"Passing a 'spider' argument" + ): + assert parse("response", spider="spider") == "response" + def test_sync_no_warning_without_spider_arg(self): @_warn_spider_arg def parse(response: str, spider: str | None = None) -> str: