Skip to content

Commit 1372558

Browse files
graingertnicoddemus
authored andcommitted
Fix collection of staticmethods defined with functools.partial
Related to #5701
1 parent a295a3d commit 1372558

File tree

6 files changed

+65
-55
lines changed

6 files changed

+65
-55
lines changed

changelog/5701.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix collection of ``staticmethod`` objects defined with ``functools.partial``.

src/_pytest/compat.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def num_mock_patch_args(function):
7878
)
7979

8080

81-
def getfuncargnames(function, is_method=False, cls=None):
81+
def getfuncargnames(function, *, name: str = "", is_method=False, cls=None):
8282
"""Returns the names of a function's mandatory arguments.
8383
8484
This should return the names of all function arguments that:
@@ -91,11 +91,12 @@ def getfuncargnames(function, is_method=False, cls=None):
9191
be treated as a bound method even though it's not unless, only in
9292
the case of cls, the function is a static method.
9393
94+
The name parameter should be the original name in which the function was collected.
95+
9496
@RonnyPfannschmidt: This function should be refactored when we
9597
revisit fixtures. The fixture mechanism should ask the node for
9698
the fixture names, and not try to obtain directly from the
9799
function object well after collection has occurred.
98-
99100
"""
100101
# The parameters attribute of a Signature object contains an
101102
# ordered mapping of parameter names to Parameter instances. This
@@ -118,11 +119,14 @@ def getfuncargnames(function, is_method=False, cls=None):
118119
)
119120
and p.default is Parameter.empty
120121
)
122+
if not name:
123+
name = function.__name__
124+
121125
# If this function should be treated as a bound method even though
122126
# it's passed as an unbound method or function, remove the first
123127
# parameter name.
124128
if is_method or (
125-
cls and not isinstance(cls.__dict__.get(function.__name__, None), staticmethod)
129+
cls and not isinstance(cls.__dict__.get(name, None), staticmethod)
126130
):
127131
arg_names = arg_names[1:]
128132
# Remove any names that will be replaced with mocks.
@@ -245,7 +249,7 @@ def get_real_method(obj, holder):
245249
try:
246250
is_method = hasattr(obj, "__func__")
247251
obj = get_real_func(obj)
248-
except Exception:
252+
except Exception: # pragma: no cover
249253
return obj
250254
if is_method and hasattr(obj, "__get__") and callable(obj.__get__):
251255
obj = obj.__get__(holder)

src/_pytest/fixtures.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,7 @@ def __init__(
828828
where=baseid,
829829
)
830830
self.params = params
831-
self.argnames = getfuncargnames(func, is_method=unittest)
831+
self.argnames = getfuncargnames(func, name=argname, is_method=unittest)
832832
self.unittest = unittest
833833
self.ids = ids
834834
self._finalizers = []
@@ -1143,7 +1143,7 @@ def _get_direct_parametrize_args(self, node):
11431143

11441144
def getfixtureinfo(self, node, func, cls, funcargs=True):
11451145
if funcargs and not getattr(node, "nofuncargs", False):
1146-
argnames = getfuncargnames(func, cls=cls)
1146+
argnames = getfuncargnames(func, name=node.name, cls=cls)
11471147
else:
11481148
argnames = ()
11491149

testing/python/collect.py

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,52 +1143,6 @@ class Test(object):
11431143
assert result.ret == ExitCode.NO_TESTS_COLLECTED
11441144

11451145

1146-
def test_collect_functools_partial(testdir):
1147-
"""
1148-
Test that collection of functools.partial object works, and arguments
1149-
to the wrapped functions are dealt with correctly (see #811).
1150-
"""
1151-
testdir.makepyfile(
1152-
"""
1153-
import functools
1154-
import pytest
1155-
1156-
@pytest.fixture
1157-
def fix1():
1158-
return 'fix1'
1159-
1160-
@pytest.fixture
1161-
def fix2():
1162-
return 'fix2'
1163-
1164-
def check1(i, fix1):
1165-
assert i == 2
1166-
assert fix1 == 'fix1'
1167-
1168-
def check2(fix1, i):
1169-
assert i == 2
1170-
assert fix1 == 'fix1'
1171-
1172-
def check3(fix1, i, fix2):
1173-
assert i == 2
1174-
assert fix1 == 'fix1'
1175-
assert fix2 == 'fix2'
1176-
1177-
test_ok_1 = functools.partial(check1, i=2)
1178-
test_ok_2 = functools.partial(check1, i=2, fix1='fix1')
1179-
test_ok_3 = functools.partial(check1, 2)
1180-
test_ok_4 = functools.partial(check2, i=2)
1181-
test_ok_5 = functools.partial(check3, i=2)
1182-
test_ok_6 = functools.partial(check3, i=2, fix1='fix1')
1183-
1184-
test_fail_1 = functools.partial(check2, 2)
1185-
test_fail_2 = functools.partial(check3, 2)
1186-
"""
1187-
)
1188-
result = testdir.inline_run()
1189-
result.assertoutcome(passed=6, failed=2)
1190-
1191-
11921146
@pytest.mark.filterwarnings("default")
11931147
def test_dont_collect_non_function_callable(testdir):
11941148
"""Test for issue https://github.com/pytest-dev/pytest/issues/331

testing/python/fixtures.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG
1111

1212

13-
def test_getfuncargnames():
13+
def test_getfuncargnames_functions():
14+
"""Test getfuncargnames for normal functions"""
15+
1416
def f():
1517
pass
1618

@@ -31,18 +33,56 @@ def j(arg1, arg2, arg3="hello"):
3133

3234
assert fixtures.getfuncargnames(j) == ("arg1", "arg2")
3335

36+
37+
def test_getfuncargnames_methods():
38+
"""Test getfuncargnames for normal methods"""
39+
3440
class A:
3541
def f(self, arg1, arg2="hello"):
3642
pass
3743

44+
assert fixtures.getfuncargnames(A().f) == ("arg1",)
45+
46+
47+
def test_getfuncargnames_staticmethod():
48+
"""Test getfuncargnames for staticmethods"""
49+
50+
class A:
3851
@staticmethod
39-
def static(arg1, arg2):
52+
def static(arg1, arg2, x=1):
4053
pass
4154

42-
assert fixtures.getfuncargnames(A().f) == ("arg1",)
4355
assert fixtures.getfuncargnames(A.static, cls=A) == ("arg1", "arg2")
4456

4557

58+
def test_getfuncargnames_partial():
59+
"""Check getfuncargnames for methods defined with functools.partial (#5701)"""
60+
import functools
61+
62+
def check(arg1, arg2, i):
63+
pass
64+
65+
class T:
66+
test_ok = functools.partial(check, i=2)
67+
68+
values = fixtures.getfuncargnames(T().test_ok, name="test_ok")
69+
assert values == ("arg1", "arg2")
70+
71+
72+
def test_getfuncargnames_staticmethod_partial():
73+
"""Check getfuncargnames for staticmethods defined with functools.partial (#5701)"""
74+
import functools
75+
76+
def check(arg1, arg2, i):
77+
pass
78+
79+
class T:
80+
test_ok = staticmethod(functools.partial(check, i=2))
81+
82+
values = fixtures.getfuncargnames(T().test_ok, name="test_ok")
83+
assert values == ("arg1", "arg2")
84+
85+
4686
@pytest.mark.pytester_example_path("fixtures/fill_fixtures")
4787
class TestFillFixtures:
4888
def test_fillfuncargs_exposed(self):

testing/test_compat.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
from functools import partial
23
from functools import wraps
34

45
import pytest
@@ -72,6 +73,16 @@ def func():
7273
assert get_real_func(wrapped_func2) is wrapped_func
7374

7475

76+
def test_get_real_func_partial():
77+
"""Test get_real_func handles partial instances correctly"""
78+
79+
def foo(x):
80+
return x
81+
82+
assert get_real_func(foo) is foo
83+
assert get_real_func(partial(foo)) is foo
84+
85+
7586
def test_is_generator_asyncio(testdir):
7687
testdir.makepyfile(
7788
"""

0 commit comments

Comments
 (0)