Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

check that tests that are partial staticmethods are supported #5701

Merged
merged 1 commit into from
Aug 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/5701.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix collection of ``staticmethod`` objects defined with ``functools.partial``.
12 changes: 8 additions & 4 deletions src/_pytest/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def num_mock_patch_args(function):
)


def getfuncargnames(function, is_method=False, cls=None):
def getfuncargnames(function, *, name: str = "", is_method=False, cls=None):
nicoddemus marked this conversation as resolved.
Show resolved Hide resolved
"""Returns the names of a function's mandatory arguments.

This should return the names of all function arguments that:
Expand All @@ -91,11 +91,12 @@ def getfuncargnames(function, is_method=False, cls=None):
be treated as a bound method even though it's not unless, only in
the case of cls, the function is a static method.

The name parameter should be the original name in which the function was collected.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"original name" of what?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The name parameter should be the original name in which the function was collected.
The name parameter should be the original name of the collected function.

?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could mention that the default is function.__name__ then maybe.. (but I'm -0 myself)


@RonnyPfannschmidt: This function should be refactored when we
revisit fixtures. The fixture mechanism should ask the node for
the fixture names, and not try to obtain directly from the
function object well after collection has occurred.

"""
# The parameters attribute of a Signature object contains an
# ordered mapping of parameter names to Parameter instances. This
Expand All @@ -118,11 +119,14 @@ def getfuncargnames(function, is_method=False, cls=None):
)
and p.default is Parameter.empty
)
if not name:
name = function.__name__

# If this function should be treated as a bound method even though
# it's passed as an unbound method or function, remove the first
# parameter name.
if is_method or (
cls and not isinstance(cls.__dict__.get(function.__name__, None), staticmethod)
cls and not isinstance(cls.__dict__.get(name, None), staticmethod)
):
arg_names = arg_names[1:]
# Remove any names that will be replaced with mocks.
Expand Down Expand Up @@ -245,7 +249,7 @@ def get_real_method(obj, holder):
try:
is_method = hasattr(obj, "__func__")
obj = get_real_func(obj)
except Exception:
except Exception: # pragma: no cover
return obj
if is_method and hasattr(obj, "__get__") and callable(obj.__get__):
obj = obj.__get__(holder)
Expand Down
4 changes: 2 additions & 2 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,7 @@ def __init__(
where=baseid,
)
self.params = params
self.argnames = getfuncargnames(func, is_method=unittest)
self.argnames = getfuncargnames(func, name=argname, is_method=unittest)
self.unittest = unittest
self.ids = ids
self._finalizers = []
Expand Down Expand Up @@ -1143,7 +1143,7 @@ def _get_direct_parametrize_args(self, node):

def getfixtureinfo(self, node, func, cls, funcargs=True):
if funcargs and not getattr(node, "nofuncargs", False):
argnames = getfuncargnames(func, cls=cls)
argnames = getfuncargnames(func, name=node.name, cls=cls)
else:
argnames = ()

Expand Down
46 changes: 0 additions & 46 deletions testing/python/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1143,52 +1143,6 @@ class Test(object):
assert result.ret == ExitCode.NO_TESTS_COLLECTED


def test_collect_functools_partial(testdir):
"""
Test that collection of functools.partial object works, and arguments
to the wrapped functions are dealt with correctly (see #811).
"""
testdir.makepyfile(
"""
import functools
import pytest

@pytest.fixture
def fix1():
return 'fix1'

@pytest.fixture
def fix2():
return 'fix2'

def check1(i, fix1):
assert i == 2
assert fix1 == 'fix1'

def check2(fix1, i):
assert i == 2
assert fix1 == 'fix1'

def check3(fix1, i, fix2):
assert i == 2
assert fix1 == 'fix1'
assert fix2 == 'fix2'

test_ok_1 = functools.partial(check1, i=2)
test_ok_2 = functools.partial(check1, i=2, fix1='fix1')
test_ok_3 = functools.partial(check1, 2)
test_ok_4 = functools.partial(check2, i=2)
test_ok_5 = functools.partial(check3, i=2)
test_ok_6 = functools.partial(check3, i=2, fix1='fix1')

test_fail_1 = functools.partial(check2, 2)
test_fail_2 = functools.partial(check3, 2)
"""
)
result = testdir.inline_run()
result.assertoutcome(passed=6, failed=2)


@pytest.mark.filterwarnings("default")
def test_dont_collect_non_function_callable(testdir):
"""Test for issue https://github.com/pytest-dev/pytest/issues/331
Expand Down
46 changes: 43 additions & 3 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG


def test_getfuncargnames():
def test_getfuncargnames_functions():
"""Test getfuncargnames for normal functions"""

def f():
pass

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

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


def test_getfuncargnames_methods():
"""Test getfuncargnames for normal methods"""

class A:
def f(self, arg1, arg2="hello"):
pass

assert fixtures.getfuncargnames(A().f) == ("arg1",)


def test_getfuncargnames_staticmethod():
"""Test getfuncargnames for staticmethods"""

class A:
@staticmethod
def static(arg1, arg2):
def static(arg1, arg2, x=1):
pass

assert fixtures.getfuncargnames(A().f) == ("arg1",)
assert fixtures.getfuncargnames(A.static, cls=A) == ("arg1", "arg2")


def test_getfuncargnames_partial():
"""Check getfuncargnames for methods defined with functools.partial (#5701)"""
import functools

def check(arg1, arg2, i):
pass

class T:
test_ok = functools.partial(check, i=2)

values = fixtures.getfuncargnames(T().test_ok, name="test_ok")
assert values == ("arg1", "arg2")


def test_getfuncargnames_staticmethod_partial():
"""Check getfuncargnames for staticmethods defined with functools.partial (#5701)"""
import functools

def check(arg1, arg2, i):
pass

class T:
test_ok = staticmethod(functools.partial(check, i=2))
nicoddemus marked this conversation as resolved.
Show resolved Hide resolved

values = fixtures.getfuncargnames(T().test_ok, name="test_ok")
assert values == ("arg1", "arg2")


nicoddemus marked this conversation as resolved.
Show resolved Hide resolved
@pytest.mark.pytester_example_path("fixtures/fill_fixtures")
class TestFillFixtures:
def test_fillfuncargs_exposed(self):
Expand Down
11 changes: 11 additions & 0 deletions testing/test_compat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
from functools import partial
from functools import wraps

import pytest
Expand Down Expand Up @@ -72,6 +73,16 @@ def func():
assert get_real_func(wrapped_func2) is wrapped_func


def test_get_real_func_partial():
"""Test get_real_func handles partial instances correctly"""

def foo(x):
return x

assert get_real_func(foo) is foo
assert get_real_func(partial(foo)) is foo


def test_is_generator_asyncio(testdir):
testdir.makepyfile(
"""
Expand Down