Fix issue where fixtures would lose the decorated functionality
Fix #3774
This commit is contained in:
		
							parent
							
								
									a76cc8f8c4
								
							
						
					
					
						commit
						ef8ec01e39
					
				|  | @ -234,6 +234,13 @@ def get_real_func(obj): | ||||||
|     """ |     """ | ||||||
|     start_obj = obj |     start_obj = obj | ||||||
|     for i in range(100): |     for i in range(100): | ||||||
|  |         # __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function | ||||||
|  |         # 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 | ||||||
|  |             break | ||||||
|         new_obj = getattr(obj, "__wrapped__", None) |         new_obj = getattr(obj, "__wrapped__", None) | ||||||
|         if new_obj is None: |         if new_obj is None: | ||||||
|             break |             break | ||||||
|  |  | ||||||
|  | @ -954,9 +954,6 @@ def _ensure_immutable_ids(ids): | ||||||
| def wrap_function_to_warning_if_called_directly(function, fixture_marker): | def wrap_function_to_warning_if_called_directly(function, fixture_marker): | ||||||
|     """Wrap the given fixture function so we can issue warnings about it being called directly, instead of |     """Wrap the given fixture function so we can issue warnings about it being called directly, instead of | ||||||
|     used as an argument in a test function. |     used as an argument in a test function. | ||||||
| 
 |  | ||||||
|     The warning is emitted only in Python 3, because I didn't find a reliable way to make the wrapper function |  | ||||||
|     keep the original signature, and we probably will drop Python 2 in Pytest 4 anyway. |  | ||||||
|     """ |     """ | ||||||
|     is_yield_function = is_generator(function) |     is_yield_function = is_generator(function) | ||||||
|     msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__) |     msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__) | ||||||
|  | @ -982,6 +979,10 @@ def wrap_function_to_warning_if_called_directly(function, fixture_marker): | ||||||
|     if six.PY2: |     if six.PY2: | ||||||
|         result.__wrapped__ = function |         result.__wrapped__ = function | ||||||
| 
 | 
 | ||||||
|  |     # 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 | ||||||
|  | 
 | ||||||
|     return result |     return result | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1044,3 +1044,10 @@ def test_frame_leak_on_failing_test(testdir): | ||||||
|     ) |     ) | ||||||
|     result = testdir.runpytest_subprocess() |     result = testdir.runpytest_subprocess() | ||||||
|     result.stdout.fnmatch_lines(["*1 failed, 1 passed in*"]) |     result.stdout.fnmatch_lines(["*1 failed, 1 passed in*"]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_fixture_mock_integration(testdir): | ||||||
|  |     """Test that decorators applied to fixture are left working (#3774)""" | ||||||
|  |     p = testdir.copy_example("acceptance/fixture_mock_integration.py") | ||||||
|  |     result = testdir.runpytest(p) | ||||||
|  |     result.stdout.fnmatch_lines("*1 passed*") | ||||||
|  |  | ||||||
|  | @ -0,0 +1,17 @@ | ||||||
|  | """Reproduces issue #3774""" | ||||||
|  | 
 | ||||||
|  | import mock | ||||||
|  | 
 | ||||||
|  | import pytest | ||||||
|  | 
 | ||||||
|  | config = {"mykey": "ORIGINAL"} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @pytest.fixture(scope="function") | ||||||
|  | @mock.patch.dict(config, {"mykey": "MOCKED"}) | ||||||
|  | def my_fixture(): | ||||||
|  |     return config["mykey"] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_foobar(my_fixture): | ||||||
|  |     assert my_fixture == "MOCKED" | ||||||
|  | @ -1,5 +1,8 @@ | ||||||
| from __future__ import absolute_import, division, print_function | from __future__ import absolute_import, division, print_function | ||||||
| import sys | import sys | ||||||
|  | from functools import wraps | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
|  | @ -26,6 +29,8 @@ 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 | ||||||
|  | @ -38,6 +43,33 @@ def test_real_func_loop_limit(): | ||||||
|         print(res) |         print(res) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def test_get_real_func(): | ||||||
|  |     """Check that get_real_func correctly unwraps decorators until reaching the real function""" | ||||||
|  | 
 | ||||||
|  |     def decorator(f): | ||||||
|  |         @wraps(f) | ||||||
|  |         def inner(): | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |         if six.PY2: | ||||||
|  |             inner.__wrapped__ = f | ||||||
|  |         return inner | ||||||
|  | 
 | ||||||
|  |     def func(): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     wrapped_func = decorator(decorator(func)) | ||||||
|  |     assert get_real_func(wrapped_func) is func | ||||||
|  | 
 | ||||||
|  |     wrapped_func2 = decorator(decorator(wrapped_func)) | ||||||
|  |     assert get_real_func(wrapped_func2) is 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 | ||||||
|  |     assert get_real_func(wrapped_func2) is wrapped_func | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @pytest.mark.skipif( | @pytest.mark.skipif( | ||||||
|     sys.version_info < (3, 4), reason="asyncio available in Python 3.4+" |     sys.version_info < (3, 4), reason="asyncio available in Python 3.4+" | ||||||
| ) | ) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue