Skip to content

Commit 28c6c5b

Browse files
authored
check that tests that are partial staticmethods are supported (#5701)
check that tests that are partial staticmethods are supported
2 parents 0ba774a + 1372558 commit 28c6c5b

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
@@ -80,7 +80,7 @@ def num_mock_patch_args(function):
8080
)
8181

8282

83-
def getfuncargnames(function, is_method=False, cls=None):
83+
def getfuncargnames(function, *, name: str = "", is_method=False, cls=None):
8484
"""Returns the names of a function's mandatory arguments.
8585
8686
This should return the names of all function arguments that:
@@ -93,11 +93,12 @@ def getfuncargnames(function, is_method=False, cls=None):
9393
be treated as a bound method even though it's not unless, only in
9494
the case of cls, the function is a static method.
9595
96+
The name parameter should be the original name in which the function was collected.
97+
9698
@RonnyPfannschmidt: This function should be refactored when we
9799
revisit fixtures. The fixture mechanism should ask the node for
98100
the fixture names, and not try to obtain directly from the
99101
function object well after collection has occurred.
100-
101102
"""
102103
# The parameters attribute of a Signature object contains an
103104
# ordered mapping of parameter names to Parameter instances. This
@@ -120,11 +121,14 @@ def getfuncargnames(function, is_method=False, cls=None):
120121
)
121122
and p.default is Parameter.empty
122123
)
124+
if not name:
125+
name = function.__name__
126+
123127
# If this function should be treated as a bound method even though
124128
# it's passed as an unbound method or function, remove the first
125129
# parameter name.
126130
if is_method or (
127-
cls and not isinstance(cls.__dict__.get(function.__name__, None), staticmethod)
131+
cls and not isinstance(cls.__dict__.get(name, None), staticmethod)
128132
):
129133
arg_names = arg_names[1:]
130134
# Remove any names that will be replaced with mocks.
@@ -247,7 +251,7 @@ def get_real_method(obj, holder):
247251
try:
248252
is_method = hasattr(obj, "__func__")
249253
obj = get_real_func(obj)
250-
except Exception:
254+
except Exception: # pragma: no cover
251255
return obj
252256
if is_method and hasattr(obj, "__get__") and callable(obj.__get__):
253257
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)