Skip to content

Commit 67106f0

Browse files
committed
Use a custom holder class so we can be sure __pytest_wrapper__ was set by us
1 parent ef8ec01 commit 67106f0

File tree

3 files changed

+18
-7
lines changed

3 files changed

+18
-7
lines changed

src/_pytest/compat.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,18 @@ def ascii_escaped(val):
228228
return val.encode("unicode-escape")
229229

230230

231+
class _PytestWrapper(object):
232+
"""Dummy wrapper around a function object for internal use only.
233+
234+
Used to correctly unwrap the underlying function object
235+
when we are creating fixtures, because we wrap the function object ourselves with a decorator
236+
to issue warnings when the fixture function is called directly.
237+
"""
238+
239+
def __init__(self, obj):
240+
self.obj = obj
241+
242+
231243
def get_real_func(obj):
232244
""" gets the real function object of the (possibly) wrapped object by
233245
functools.wraps or functools.partial.
@@ -238,8 +250,8 @@ def get_real_func(obj):
238250
# to trigger a warning if it gets called directly instead of by pytest: we don't
239251
# want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774)
240252
new_obj = getattr(obj, "__pytest_wrapped__", None)
241-
if new_obj is not None:
242-
obj = new_obj
253+
if isinstance(new_obj, _PytestWrapper):
254+
obj = new_obj.obj
243255
break
244256
new_obj = getattr(obj, "__wrapped__", None)
245257
if new_obj is None:

src/_pytest/fixtures.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
safe_getattr,
3232
FuncargnamesCompatAttr,
3333
get_real_method,
34+
_PytestWrapper,
3435
)
3536
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning
3637
from _pytest.outcomes import fail, TEST_OUTCOME
@@ -981,7 +982,7 @@ def result(*args, **kwargs):
981982

982983
# keep reference to the original function in our own custom attribute so we don't unwrap
983984
# further than this point and lose useful wrappings like @mock.patch (#3774)
984-
result.__pytest_wrapped__ = function
985+
result.__pytest_wrapped__ = _PytestWrapper(function)
985986

986987
return result
987988

testing/test_compat.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import six
66

77
import pytest
8-
from _pytest.compat import is_generator, get_real_func, safe_getattr
8+
from _pytest.compat import is_generator, get_real_func, safe_getattr, _PytestWrapper
99
from _pytest.outcomes import OutcomeException
1010

1111

@@ -29,8 +29,6 @@ def __repr__(self):
2929
return "<Evil left={left}>".format(left=self.left)
3030

3131
def __getattr__(self, attr):
32-
if attr == "__pytest_wrapped__":
33-
raise AttributeError
3432
if not self.left:
3533
raise RuntimeError("its over")
3634
self.left -= 1
@@ -66,7 +64,7 @@ def func():
6664

6765
# special case for __pytest_wrapped__ attribute: used to obtain the function up until the point
6866
# a function was wrapped by pytest itself
69-
wrapped_func2.__pytest_wrapped__ = wrapped_func
67+
wrapped_func2.__pytest_wrapped__ = _PytestWrapper(wrapped_func)
7068
assert get_real_func(wrapped_func2) is wrapped_func
7169

7270

0 commit comments

Comments
 (0)