diff --git a/changelog/13377.bugfix.rst b/changelog/13377.bugfix.rst new file mode 100644 index 00000000000..15755481f7f --- /dev/null +++ b/changelog/13377.bugfix.rst @@ -0,0 +1,12 @@ +Fixed handling of test methods with positional-only parameter syntax. + +Now, methods are supported that formally define ``self`` as positional-only +and/or fixture parameters as keyword-only, e.g.: + +.. code-block:: python + + class TestClass: + + def test_method(self, /, *, fixture): ... + +Before, this caused an internal error in pytest. diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 2cbb17eca38..f113a2197f3 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -122,7 +122,7 @@ def getfuncargnames( # creates a tuple of the names of the parameters that don't have # defaults. try: - parameters = signature(function).parameters + parameters = signature(function).parameters.values() except (ValueError, TypeError) as e: from _pytest.outcomes import fail @@ -133,7 +133,7 @@ def getfuncargnames( arg_names = tuple( p.name - for p in parameters.values() + for p in parameters if ( p.kind is Parameter.POSITIONAL_OR_KEYWORD or p.kind is Parameter.KEYWORD_ONLY @@ -144,9 +144,9 @@ def getfuncargnames( name = function.__name__ # If this function should be treated as a bound method even though - # it's passed as an unbound method or function, remove the first - # parameter name. - if ( + # it's passed as an unbound method or function, and its first parameter + # wasn't defined as positional only, remove the first parameter name. + if not any(p.kind is Parameter.POSITIONAL_ONLY for p in parameters) and ( # Not using `getattr` because we don't want to resolve the staticmethod. # Not using `cls.__dict__` because we want to check the entire MRO. cls diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 32453739e8c..fb76fe6cf96 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -48,7 +48,23 @@ class A: def f(self, arg1, arg2="hello"): raise NotImplementedError() + def g(self, /, arg1, arg2="hello"): + raise NotImplementedError() + + def h(self, *, arg1, arg2="hello"): + raise NotImplementedError() + + def j(self, arg1, *, arg2, arg3="hello"): + raise NotImplementedError() + + def k(self, /, arg1, *, arg2, arg3="hello"): + raise NotImplementedError() + assert getfuncargnames(A().f) == ("arg1",) + assert getfuncargnames(A().g) == ("arg1",) + assert getfuncargnames(A().h) == ("arg1",) + assert getfuncargnames(A().j) == ("arg1", "arg2") + assert getfuncargnames(A().k) == ("arg1", "arg2") def test_getfuncargnames_staticmethod(): @@ -5033,3 +5049,22 @@ def test_foo(another_fixture): ) result = pytester.runpytest() result.assert_outcomes(passed=1) + + +def test_collect_positional_only(pytester: Pytester) -> None: + """Support the collection of tests with positional-only arguments (#13376).""" + pytester.makepyfile( + """ + import pytest + + class Test: + @pytest.fixture + def fix(self): + return 1 + + def test_method(self, /, fix): + assert fix == 1 + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1)