From da6a4671715c3d1e27b75a1015ac11491c1e9156 Mon Sep 17 00:00:00 2001 From: Stefan Zimmermann Date: Tue, 15 Apr 2025 16:56:17 +0200 Subject: [PATCH 1/4] Fix parameter handling of non-static test methods by supporting positional-only self parameters in _pytest.compat.getfuncargnames() Fixes #13376 Co-authored-by: Bruno Oliveira --- changelog/13377.bugfix.rst | 27 +++++++++++++++++++++++++++ src/_pytest/compat.py | 10 +++++----- testing/python/fixtures.py | 16 ++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 changelog/13377.bugfix.rst diff --git a/changelog/13377.bugfix.rst b/changelog/13377.bugfix.rst new file mode 100644 index 00000000000..63381089806 --- /dev/null +++ b/changelog/13377.bugfix.rst @@ -0,0 +1,27 @@ +Fixed handling of test methods with special 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 led to: + +.. code-block:: python-console + + pyfuncitem = + + @hookimpl(trylast=True) + def pytest_pyfunc_call(pyfuncitem: Function) -> object | None: + testfunction = pyfuncitem.obj + if is_async_function(testfunction): + async_fail(pyfuncitem.nodeid) + funcargs = pyfuncitem.funcargs + testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} + > result = testfunction(**testargs) + ^^^^^^^^^^^^^^^^^^^^^^^^ + E TypeError: TestClass.test_method() missing 1 required positional argument: 'fixture' 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..fba14e56f1c 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(): From 5c218d15953e980561af5cb255c3c4dd0b674388 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 18 Apr 2025 17:44:59 -0300 Subject: [PATCH 2/4] Integration test --- testing/python/fixtures.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index fba14e56f1c..fb76fe6cf96 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -5049,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) From 02e179f7627f0c0d8af0c3dc869bce64294fd112 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 18 Apr 2025 17:45:44 -0300 Subject: [PATCH 3/4] Apply suggestions from code review --- changelog/13377.bugfix.rst | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/changelog/13377.bugfix.rst b/changelog/13377.bugfix.rst index 63381089806..d02666947c0 100644 --- a/changelog/13377.bugfix.rst +++ b/changelog/13377.bugfix.rst @@ -9,19 +9,4 @@ and/or fixture parameters as keyword-only, e.g.: def test_method(self, /, *, fixture): ... -Before, this led to: - -.. code-block:: python-console - - pyfuncitem = - - @hookimpl(trylast=True) - def pytest_pyfunc_call(pyfuncitem: Function) -> object | None: - testfunction = pyfuncitem.obj - if is_async_function(testfunction): - async_fail(pyfuncitem.nodeid) - funcargs = pyfuncitem.funcargs - testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} - > result = testfunction(**testargs) - ^^^^^^^^^^^^^^^^^^^^^^^^ - E TypeError: TestClass.test_method() missing 1 required positional argument: 'fixture' +Before, this caused an internal error in pytest. From 83f0211715ddc5746e1ed6d3346f60256480f74c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 18 Apr 2025 17:46:18 -0300 Subject: [PATCH 4/4] Update 13377.bugfix.rst --- changelog/13377.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/13377.bugfix.rst b/changelog/13377.bugfix.rst index d02666947c0..15755481f7f 100644 --- a/changelog/13377.bugfix.rst +++ b/changelog/13377.bugfix.rst @@ -1,4 +1,4 @@ -Fixed handling of test methods with special parameter syntax. +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.: