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

Refactor direct fixture call warning to avoid incompatibility with plugins #3754

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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/3747.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix compatibility problem with plugins and the the warning code issued by fixture functions when they are called directly.
Copy link

Choose a reason for hiding this comment

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

drive-by-nit: "the the"

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks!

15 changes: 15 additions & 0 deletions src/_pytest/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,21 @@ def get_real_func(obj):
return obj


def get_real_method(obj, holder):
Copy link
Member

Choose a reason for hiding this comment

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

this needs to handle __wrapped__ on python2

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm get_real_func already handles __wrapped__. Can you clarify this a bit?

Copy link
Member

Choose a reason for hiding this comment

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

@nicoddemus my understanding is that method descriptors are not exposing it readily, i will investigate in a bit

"""
Attempts to obtain the real function object that might be wrapping ``obj``, while at the same time
returning a bound method to ``holder`` if the original object was a bound method.
"""
try:
is_method = hasattr(obj, "__func__")
obj = get_real_func(obj)
except Exception:
return obj
if is_method and hasattr(obj, "__get__") and callable(obj.__get__):
obj = obj.__get__(holder)
return obj


def getfslineno(obj):
# xxx let decorators etc specify a sane ordering
obj = get_real_func(obj)
Expand Down
27 changes: 13 additions & 14 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
getfuncargnames,
safe_getattr,
FuncargnamesCompatAttr,
get_real_method,
)
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning
from _pytest.outcomes import fail, TEST_OUTCOME
Expand Down Expand Up @@ -931,13 +932,6 @@ def pytest_fixture_setup(fixturedef, request):
request._check_scope(argname, request.scope, fixdef.scope)
kwargs[argname] = result

# if function has been defined with @pytest.fixture, we want to
# pass the special __being_called_by_pytest parameter so we don't raise a warning
# this is an ugly hack, see #3720 for an opportunity to improve this
defined_using_fixture_decorator = hasattr(fixturedef.func, "_pytestfixturefunction")
if defined_using_fixture_decorator:
kwargs["__being_called_by_pytest"] = True

fixturefunc = resolve_fixture_function(fixturedef, request)
my_cache_key = request.param_index
try:
Expand Down Expand Up @@ -973,9 +967,7 @@ def wrap_function_to_warning_if_called_directly(function, fixture_marker):
@functools.wraps(function)
def result(*args, **kwargs):
__tracebackhide__ = True
__being_called_by_pytest = kwargs.pop("__being_called_by_pytest", False)
if not __being_called_by_pytest:
warnings.warn(warning, stacklevel=3)
warnings.warn(warning, stacklevel=3)
for x in function(*args, **kwargs):
yield x

Expand All @@ -984,9 +976,7 @@ def result(*args, **kwargs):
@functools.wraps(function)
def result(*args, **kwargs):
__tracebackhide__ = True
__being_called_by_pytest = kwargs.pop("__being_called_by_pytest", False)
if not __being_called_by_pytest:
warnings.warn(warning, stacklevel=3)
warnings.warn(warning, stacklevel=3)
return function(*args, **kwargs)

if six.PY2:
Expand Down Expand Up @@ -1279,9 +1269,9 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False):
# The attribute can be an arbitrary descriptor, so the attribute
# access below can raise. safe_getatt() ignores such exceptions.
obj = safe_getattr(holderobj, name, None)
marker = getfixturemarker(obj)
# fixture functions have a pytest_funcarg__ prefix (pre-2.3 style)
# or are "@pytest.fixture" marked
marker = getfixturemarker(obj)
if marker is None:
if not name.startswith(self._argprefix):
continue
Expand All @@ -1303,6 +1293,15 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False):
name = marker.name
assert not name.startswith(self._argprefix), FIXTURE_MSG.format(name)

# during fixture definition we wrap the original fixture function
# to issue a warning if called directly, so here we unwrap it in order to not emit the warning
# when pytest itself calls the fixture function
if six.PY2 and unittest:
Copy link
Member Author

Choose a reason for hiding this comment

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

This was the best I could come with; unbound methods are a headache to deal here.

Copy link
Member

Choose a reason for hiding this comment

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

cant wait for the python2 drop ^^

Copy link
Member Author

Choose a reason for hiding this comment

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

You and me both! 😁

# hack on Python 2 because of the unbound methods
obj = get_real_func(obj)
else:
obj = get_real_method(obj, holderobj)

fixture_def = FixtureDef(
self,
nodeid,
Expand Down
1 change: 0 additions & 1 deletion testing/deprecated_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,6 @@ def test_func():
)


# @pytest.mark.skipif(six.PY2, reason="We issue the warning in Python 3 only")
def test_call_fixture_function_deprecated():
"""Check if a warning is raised if a fixture function is called directly (#3661)"""

Expand Down
7 changes: 3 additions & 4 deletions testing/python/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -1470,10 +1470,9 @@ def test_hello(self, item, fm):
print (faclist)
assert len(faclist) == 3
kwargs = {'__being_called_by_pytest': True}
assert faclist[0].func(item._request, **kwargs) == "conftest"
assert faclist[1].func(item._request, **kwargs) == "module"
assert faclist[2].func(item._request, **kwargs) == "class"
assert faclist[0].func(item._request) == "conftest"
assert faclist[1].func(item._request) == "module"
assert faclist[2].func(item._request) == "class"
"""
)
reprec = testdir.inline_run("-s")
Expand Down