Use a custom holder class so we can be sure __pytest_wrapper__ was set by us

This commit is contained in:
Bruno Oliveira 2018-08-09 09:20:37 -03:00
parent ef8ec01e39
commit 67106f056b
3 changed files with 18 additions and 7 deletions

View File

@ -228,6 +228,18 @@ else:
return val.encode("unicode-escape") 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): def get_real_func(obj):
""" gets the real function object of the (possibly) wrapped object by """ gets the real function object of the (possibly) wrapped object by
functools.wraps or functools.partial. 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 # 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) # want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774)
new_obj = getattr(obj, "__pytest_wrapped__", None) new_obj = getattr(obj, "__pytest_wrapped__", None)
if new_obj is not None: if isinstance(new_obj, _PytestWrapper):
obj = new_obj obj = new_obj.obj
break break
new_obj = getattr(obj, "__wrapped__", None) new_obj = getattr(obj, "__wrapped__", None)
if new_obj is None: if new_obj is None:

View File

@ -31,6 +31,7 @@ from _pytest.compat import (
safe_getattr, safe_getattr,
FuncargnamesCompatAttr, FuncargnamesCompatAttr,
get_real_method, get_real_method,
_PytestWrapper,
) )
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning
from _pytest.outcomes import fail, TEST_OUTCOME 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 # 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) # further than this point and lose useful wrappings like @mock.patch (#3774)
result.__pytest_wrapped__ = function result.__pytest_wrapped__ = _PytestWrapper(function)
return result return result

View File

@ -5,7 +5,7 @@ from functools import wraps
import six import six
import pytest 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 from _pytest.outcomes import OutcomeException
@ -29,8 +29,6 @@ def test_real_func_loop_limit():
return "<Evil left={left}>".format(left=self.left) return "<Evil left={left}>".format(left=self.left)
def __getattr__(self, attr): def __getattr__(self, attr):
if attr == "__pytest_wrapped__":
raise AttributeError
if not self.left: if not self.left:
raise RuntimeError("its over") raise RuntimeError("its over")
self.left -= 1 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 # special case for __pytest_wrapped__ attribute: used to obtain the function up until the point
# a function was wrapped by pytest itself # 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 assert get_real_func(wrapped_func2) is wrapped_func