diff --git a/changelog.d/1475.removed.rst b/changelog.d/1475.removed.rst new file mode 100644 index 00000000..9d6d6eed --- /dev/null +++ b/changelog.d/1475.removed.rst @@ -0,0 +1 @@ +Removed the deprecated ``scope`` keyword argument of the ``asyncio`` marker. Passing ``scope=`` (or any other unknown keyword argument) to ``@pytest.mark.asyncio`` now raises ``pytest.UsageError`` instead of emitting a deprecation warning. Use ``loop_scope=`` instead. diff --git a/docs/how-to-guides/migrate_from_0_23.rst b/docs/how-to-guides/migrate_from_0_23.rst index 280b0a80..0806c626 100644 --- a/docs/how-to-guides/migrate_from_0_23.rst +++ b/docs/how-to-guides/migrate_from_0_23.rst @@ -7,4 +7,4 @@ The following steps assume that your test suite has no re-implementations of the 1. Explicitly set the *loop_scope* of async fixtures by replacing occurrences of ``@pytest.fixture(scope="…")`` and ``@pytest_asyncio.fixture(scope="…")`` with ``@pytest_asyncio.fixture(loop_scope="…", scope="…")`` such that *loop_scope* and *scope* are the same. If you use auto mode, resolve all import errors from missing imports of *pytest_asyncio*. If your async fixtures all use the same *loop_scope*, you may choose to set the *asyncio_default_fixture_loop_scope* configuration option to that loop scope, instead. 2. If you haven't set *asyncio_default_fixture_loop_scope*, set it to *function* to address the deprecation warning about the unset configuration option. -3. Change all occurrences of ``pytest.mark.asyncio(scope="…")`` to ``pytest.mark.asyncio(loop_scope="…")`` to address the deprecation warning about the *scope* argument to the *asyncio* marker. +3. Change all occurrences of ``pytest.mark.asyncio(scope="…")`` to ``pytest.mark.asyncio(loop_scope="…")``. The *scope* argument to the *asyncio* marker was deprecated in v0.23 and removed in v2.0; passing it now raises a usage error. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 38b75e41..8c80459e 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -582,7 +582,7 @@ def _loop_scope(self) -> _ScopeName: marker = self.get_closest_marker("asyncio") assert marker is not None default_loop_scope = _get_default_test_loop_scope(self.config) - loop_scope = marker.kwargs.get("loop_scope") or marker.kwargs.get("scope") + loop_scope = marker.kwargs.get("loop_scope") if loop_scope is None: return default_loop_scope else: @@ -946,16 +946,6 @@ def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None: return hook_result -_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR = """\ -An asyncio pytest marker defines both "scope" and "loop_scope", \ -but it should only use "loop_scope". -""" - -_MARKER_SCOPE_KWARG_DEPRECATION_WARNING = """\ -The "scope" keyword argument to the asyncio marker has been deprecated. \ -Please use the "loop_scope" argument instead. -""" - _INVALID_LOOP_FACTORIES_KWARG = """\ mark.asyncio 'loop_factories' must be a non-empty sequence of strings. """ @@ -972,13 +962,7 @@ def _parse_asyncio_marker( ) -> tuple[_ScopeName | None, Sequence[str] | None]: assert asyncio_marker.name == "asyncio" _validate_asyncio_marker(asyncio_marker) - if "scope" in asyncio_marker.kwargs: - if "loop_scope" in asyncio_marker.kwargs: - raise pytest.UsageError(_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR) - warnings.warn(PytestDeprecationWarning(_MARKER_SCOPE_KWARG_DEPRECATION_WARNING)) - scope = asyncio_marker.kwargs.get("loop_scope") or asyncio_marker.kwargs.get( - "scope" - ) + scope = asyncio_marker.kwargs.get("loop_scope") if scope is not None: assert scope in {"function", "class", "module", "package", "session"} marker_value = asyncio_marker.kwargs.get("loop_factories") @@ -994,16 +978,22 @@ def _parse_asyncio_marker( return scope, marker_value +_ALLOWED_ASYNCIO_MARKER_KWARGS = {"loop_scope", "loop_factories"} + + def _validate_asyncio_marker(asyncio_marker: Mark) -> None: - if asyncio_marker.args or ( - asyncio_marker.kwargs - and set(asyncio_marker.kwargs) - {"loop_scope", "scope", "loop_factories"} - ): - msg = ( - "mark.asyncio accepts only keyword arguments 'loop_scope' and" - " 'loop_factories'." + if asyncio_marker.args: + raise pytest.UsageError( + "mark.asyncio does not accept positional arguments. " + "Use the 'loop_scope' or 'loop_factories' keyword arguments instead." + ) + unknown_kwargs = sorted(set(asyncio_marker.kwargs) - _ALLOWED_ASYNCIO_MARKER_KWARGS) + if unknown_kwargs: + formatted = ", ".join(repr(name) for name in unknown_kwargs) + raise pytest.UsageError( + f"mark.asyncio received unexpected keyword argument(s): {formatted}. " + "Only 'loop_scope' and 'loop_factories' are accepted." ) - raise ValueError(msg) def _get_default_test_loop_scope(config: Config) -> Any: diff --git a/tests/markers/test_function_scope.py b/tests/markers/test_function_scope.py index 180d5c71..bd7d8c72 100644 --- a/tests/markers/test_function_scope.py +++ b/tests/markers/test_function_scope.py @@ -61,20 +61,7 @@ async def test_raises(): """)) result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(errors=1) - - -def test_warns_when_scope_argument_is_present(pytester: Pytester): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile(dedent("""\ - import pytest - - @pytest.mark.asyncio(scope="function") - async def test_warns(): - ... - """)) - result = pytester.runpytest("--asyncio-mode=strict", "-W", "default") - result.assert_outcomes(passed=1, warnings=1) - result.stdout.fnmatch_lines("*DeprecationWarning*") + result.stdout.fnmatch_lines("*unexpected keyword argument*'scope'*") def test_asyncio_mark_respects_the_loop_policy( diff --git a/tests/markers/test_invalid_arguments.py b/tests/markers/test_invalid_arguments.py index df19ecfb..8943ed45 100644 --- a/tests/markers/test_invalid_arguments.py +++ b/tests/markers/test_invalid_arguments.py @@ -34,9 +34,7 @@ async def test_anything(): """)) result = pytester.runpytest("--assert=plain") result.assert_outcomes(errors=1) - result.stdout.fnmatch_lines( - ["*ValueError: mark.asyncio accepts only keyword arguments*"] - ) + result.stdout.fnmatch_lines(["*mark.asyncio does not accept positional arguments*"]) def test_error_when_wrong_keyword_argument_is_passed( @@ -53,7 +51,7 @@ async def test_anything(): result = pytester.runpytest("--assert=plain") result.assert_outcomes(errors=1) result.stdout.fnmatch_lines( - ["*ValueError: mark.asyncio accepts only keyword arguments*"] + ["*mark.asyncio received unexpected keyword argument(s): 'cope'*"] ) @@ -71,7 +69,25 @@ async def test_anything(): result = pytester.runpytest("--assert=plain") result.assert_outcomes(errors=1) result.stdout.fnmatch_lines( - ["*ValueError: mark.asyncio accepts only keyword arguments*"] + ["*mark.asyncio received unexpected keyword argument(s): 'more'*"] + ) + + +def test_error_when_scope_keyword_argument_is_passed( + pytester: pytest.Pytester, +): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile(dedent("""\ + import pytest + + @pytest.mark.asyncio(scope="session") + async def test_anything(): + pass + """)) + result = pytester.runpytest("--assert=plain") + result.assert_outcomes(errors=1) + result.stdout.fnmatch_lines( + ["*mark.asyncio received unexpected keyword argument(s): 'scope'*"] )