From 67106f056b0633b35dd4a080ef120fa61b55cf37 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 9 Aug 2018 09:20:37 -0300 Subject: [PATCH] Use a custom holder class so we can be sure __pytest_wrapper__ was set by us --- src/_pytest/compat.py | 16 ++++++++++++++-- src/_pytest/fixtures.py | 3 ++- testing/test_compat.py | 6 ++---- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 57ad4fdd8..c3ecaf912 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -228,6 +228,18 @@ else: return val.encode("unicode-escape") +class _PytestWrapper(object): + """Dummy wrapper around a function object for internal use only. + + Used to correctly unwrap the underlying function object + when we are creating fixtures, because we wrap the function object ourselves with a decorator + to issue warnings when the fixture function is called directly. + """ + + def __init__(self, obj): + self.obj = obj + + def get_real_func(obj): """ gets the real function object of the (possibly) wrapped object by functools.wraps or functools.partial. @@ -238,8 +250,8 @@ def get_real_func(obj): # to trigger a warning if it gets called directly instead of by pytest: we don't # want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774) new_obj = getattr(obj, "__pytest_wrapped__", None) - if new_obj is not None: - obj = new_obj + if isinstance(new_obj, _PytestWrapper): + obj = new_obj.obj break new_obj = getattr(obj, "__wrapped__", None) if new_obj is None: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 0d63b151f..a6634cd11 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -31,6 +31,7 @@ from _pytest.compat import ( safe_getattr, FuncargnamesCompatAttr, get_real_method, + _PytestWrapper, ) from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning from _pytest.outcomes import fail, TEST_OUTCOME @@ -981,7 +982,7 @@ def wrap_function_to_warning_if_called_directly(function, fixture_marker): # keep reference to the original function in our own custom attribute so we don't unwrap # further than this point and lose useful wrappings like @mock.patch (#3774) - result.__pytest_wrapped__ = function + result.__pytest_wrapped__ = _PytestWrapper(function) return result diff --git a/testing/test_compat.py b/testing/test_compat.py index 0663fa149..a6249d14b 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -5,7 +5,7 @@ from functools import wraps import six import pytest -from _pytest.compat import is_generator, get_real_func, safe_getattr +from _pytest.compat import is_generator, get_real_func, safe_getattr, _PytestWrapper from _pytest.outcomes import OutcomeException @@ -29,8 +29,6 @@ def test_real_func_loop_limit(): return "".format(left=self.left) def __getattr__(self, attr): - if attr == "__pytest_wrapped__": - raise AttributeError if not self.left: raise RuntimeError("its over") self.left -= 1 @@ -66,7 +64,7 @@ def test_get_real_func(): # special case for __pytest_wrapped__ attribute: used to obtain the function up until the point # a function was wrapped by pytest itself - wrapped_func2.__pytest_wrapped__ = wrapped_func + wrapped_func2.__pytest_wrapped__ = _PytestWrapper(wrapped_func) assert get_real_func(wrapped_func2) is wrapped_func