Skip to content

Commit 61c204a

Browse files
Fix handling of positional-only parameters in test methods (#13377)
Fixes #13376
1 parent 103b2b6 commit 61c204a

File tree

3 files changed

+52
-5
lines changed

3 files changed

+52
-5
lines changed

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.

src/_pytest/compat.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def getfuncargnames(
122122
# creates a tuple of the names of the parameters that don't have
123123
# defaults.
124124
try:
125-
parameters = signature(function).parameters
125+
parameters = signature(function).parameters.values()
126126
except (ValueError, TypeError) as e:
127127
from _pytest.outcomes import fail
128128

@@ -133,7 +133,7 @@ def getfuncargnames(
133133

134134
arg_names = tuple(
135135
p.name
136-
for p in parameters.values()
136+
for p in parameters
137137
if (
138138
p.kind is Parameter.POSITIONAL_OR_KEYWORD
139139
or p.kind is Parameter.KEYWORD_ONLY
@@ -144,9 +144,9 @@ def getfuncargnames(
144144
name = function.__name__
145145

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

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():
@@ -5033,3 +5049,22 @@ def test_foo(another_fixture):
50335049
)
50345050
result = pytester.runpytest()
50355051
result.assert_outcomes(passed=1)
5052+
5053+
5054+
def test_collect_positional_only(pytester: Pytester) -> None:
5055+
"""Support the collection of tests with positional-only arguments (#13376)."""
5056+
pytester.makepyfile(
5057+
"""
5058+
import pytest
5059+
5060+
class Test:
5061+
@pytest.fixture
5062+
def fix(self):
5063+
return 1
5064+
5065+
def test_method(self, /, fix):
5066+
assert fix == 1
5067+
"""
5068+
)
5069+
result = pytester.runpytest()
5070+
result.assert_outcomes(passed=1)

0 commit comments

Comments
 (0)