Skip to content

Commit a9e5dc7

Browse files
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 <[email protected]>
1 parent 103b2b6 commit a9e5dc7

File tree

3 files changed

+49
-5
lines changed

3 files changed

+49
-5
lines changed

Diff for: changelog/13377.bugfix.rst

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Fixed handling of test methods with special 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+
13+
Before, this led to:
14+
15+
.. code-block:: python-console
16+
17+
pyfuncitem = <Function test_value>
18+
19+
@hookimpl(trylast=True)
20+
def pytest_pyfunc_call(pyfuncitem: Function) -> object | None:
21+
testfunction = pyfuncitem.obj
22+
if is_async_function(testfunction):
23+
async_fail(pyfuncitem.nodeid)
24+
funcargs = pyfuncitem.funcargs
25+
testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
26+
> result = testfunction(**testargs)
27+
^^^^^^^^^^^^^^^^^^^^^^^^
28+
E TypeError: TestClass.test_method() missing 1 required positional argument: 'fixture'

Diff for: 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

Diff for: testing/python/fixtures.py

+16
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():

0 commit comments

Comments
 (0)