Skip to content

Commit 7c34357

Browse files
Fix handling of positional-only parameters in test methods (#13377) (#13383)
Fixes #13376 (cherry picked from commit 61c204a) Co-authored-by: Stefan Zimmermann <[email protected]>
1 parent fd2495c commit 7c34357

File tree

3 files changed

+52
-5
lines changed

3 files changed

+52
-5
lines changed

Diff for: changelog/13377.bugfix.rst

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Fixed handling of test methods with positional-only parameter syntax.
2+
3+
Now, methods are supported that formally define ``self`` as positional-only
4+
and/or fixture parameters as keyword-only, e.g.:
5+
6+
.. code-block:: python
7+
8+
class TestClass:
9+
10+
def test_method(self, /, *, fixture): ...
11+
12+
Before, this caused an internal error in pytest.

Diff for: src/_pytest/compat.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def getfuncargnames(
128128
# creates a tuple of the names of the parameters that don't have
129129
# defaults.
130130
try:
131-
parameters = signature(function).parameters
131+
parameters = signature(function).parameters.values()
132132
except (ValueError, TypeError) as e:
133133
from _pytest.outcomes import fail
134134

@@ -139,7 +139,7 @@ def getfuncargnames(
139139

140140
arg_names = tuple(
141141
p.name
142-
for p in parameters.values()
142+
for p in parameters
143143
if (
144144
p.kind is Parameter.POSITIONAL_OR_KEYWORD
145145
or p.kind is Parameter.KEYWORD_ONLY
@@ -150,9 +150,9 @@ def getfuncargnames(
150150
name = function.__name__
151151

152152
# If this function should be treated as a bound method even though
153-
# it's passed as an unbound method or function, remove the first
154-
# parameter name.
155-
if (
153+
# it's passed as an unbound method or function, and its first parameter
154+
# wasn't defined as positional only, remove the first parameter name.
155+
if not any(p.kind is Parameter.POSITIONAL_ONLY for p in parameters) and (
156156
# Not using `getattr` because we don't want to resolve the staticmethod.
157157
# Not using `cls.__dict__` because we want to check the entire MRO.
158158
cls

Diff for: testing/python/fixtures.py

+35
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,23 @@ class A:
4848
def f(self, arg1, arg2="hello"):
4949
raise NotImplementedError()
5050

51+
def g(self, /, arg1, arg2="hello"):
52+
raise NotImplementedError()
53+
54+
def h(self, *, arg1, arg2="hello"):
55+
raise NotImplementedError()
56+
57+
def j(self, arg1, *, arg2, arg3="hello"):
58+
raise NotImplementedError()
59+
60+
def k(self, /, arg1, *, arg2, arg3="hello"):
61+
raise NotImplementedError()
62+
5163
assert getfuncargnames(A().f) == ("arg1",)
64+
assert getfuncargnames(A().g) == ("arg1",)
65+
assert getfuncargnames(A().h) == ("arg1",)
66+
assert getfuncargnames(A().j) == ("arg1", "arg2")
67+
assert getfuncargnames(A().k) == ("arg1", "arg2")
5268

5369

5470
def test_getfuncargnames_staticmethod():
@@ -4920,3 +4936,22 @@ def test_result():
49204936
)
49214937
result = pytester.runpytest()
49224938
assert result.ret == 0
4939+
4940+
4941+
def test_collect_positional_only(pytester: Pytester) -> None:
4942+
"""Support the collection of tests with positional-only arguments (#13376)."""
4943+
pytester.makepyfile(
4944+
"""
4945+
import pytest
4946+
4947+
class Test:
4948+
@pytest.fixture
4949+
def fix(self):
4950+
return 1
4951+
4952+
def test_method(self, /, fix):
4953+
assert fix == 1
4954+
"""
4955+
)
4956+
result = pytester.runpytest()
4957+
result.assert_outcomes(passed=1)

0 commit comments

Comments
 (0)