From 31c8fbed1eaadf9d2c7d9e79598ed63cf5c29993 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 13:46:25 +0200 Subject: [PATCH] fix: update code to use new fixture definitions --- src/_pytest/compat.py | 15 +------ src/_pytest/fixtures.py | 86 +++++++++++-------------------------- testing/code/test_source.py | 11 ++--- testing/python/fixtures.py | 15 +++++++ testing/test_collection.py | 4 +- 5 files changed, 50 insertions(+), 81 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 614848e0d..02256cfc7 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -210,6 +210,7 @@ def ascii_escaped(val: bytes | str) -> str: return ret.translate(_non_printable_ascii_translate_table) +# TODO: remove and replace with FixtureFunctionDefinition @dataclasses.dataclass class _PytestWrapper: """Dummy wrapper around a function object for internal use only. @@ -249,20 +250,6 @@ def get_real_func(obj): return obj -def get_real_method(obj, holder): - """Attempt 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: # pragma: no cover - return obj - if is_method and hasattr(obj, "__get__") and callable(obj.__get__): - obj = obj.__get__(holder) - return obj - - def getimfunc(func): try: return func.__func__ diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 58721c7d2..3b31fccf4 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -45,8 +45,6 @@ from _pytest._io import TerminalWriter from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never from _pytest.compat import get_real_func - -# from _pytest.compat import get_real_method from _pytest.compat import getfuncargnames from _pytest.compat import getimfunc from _pytest.compat import getlocation @@ -1146,31 +1144,6 @@ def pytest_fixture_setup( return result -def wrap_function_to_error_out_if_called_directly( - function: FixtureFunction, - fixture_marker: "FixtureFunctionMarker", -) -> FixtureFunction: - """Wrap the given fixture function so we can raise an error about it being called directly, - instead of used as an argument in a test function.""" - name = fixture_marker.name or function.__name__ - message = ( - f'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' - "but are created automatically when test functions request them as parameters.\n" - "See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n" - "https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code." - ) - - @functools.wraps(function) - def result(*args, **kwargs): - fail(message, pytrace=False) - - # 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__ = _PytestWrapper(function) # type: ignore[attr-defined] - - return cast(FixtureFunction, result) - - @final @dataclasses.dataclass(frozen=True) class FixtureFunctionMarker: @@ -1191,7 +1164,7 @@ class FixtureFunctionMarker: if inspect.isclass(function): raise ValueError("class fixtures not supported (maybe in the future)") - if getattr(function, "_pytestfixturefunction", False): + if isinstance(function, FixtureFunctionDefinition): raise ValueError( f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}" ) @@ -1201,8 +1174,6 @@ class FixtureFunctionMarker: fixture_definition = FixtureFunctionDefinition(function, self) - # function = wrap_function_to_error_out_if_called_directly(function, self) - name = self.name or function.__name__ if name == "request": location = getlocation(function) @@ -1211,16 +1182,16 @@ class FixtureFunctionMarker: pytrace=False, ) - # Type ignored because https://github.com/python/mypy/issues/2087. - # function._pytestfixturefunction = self # type: ignore[attr-defined] - # return function return fixture_definition def __repr__(self): return "fixture" +# TODO: write docstring class FixtureFunctionDefinition: + """Since deco_fixture is now an instance of FixtureFunctionDef the getsource function will not work on it.""" + def __init__( self, function: Callable[..., object], @@ -1228,19 +1199,15 @@ class FixtureFunctionDefinition: instance: Optional[type] = None, ): self.name = fixture_function_marker.name or function.__name__ + self.__name__ = self.name self._pytestfixturefunction = fixture_function_marker self.__pytest_wrapped__ = _PytestWrapper(function) - self.fixture_function = function self.fixture_function_marker = fixture_function_marker - self.scope = fixture_function_marker.scope - self.params = fixture_function_marker.params - self.autouse = fixture_function_marker.autouse - self.ids = fixture_function_marker.ids self.fixture_function = function self.instance = instance def __repr__(self) -> str: - return f"fixture {self.fixture_function}" + return f"pytest_fixture({self.fixture_function})" def __get__(self, instance, owner=None): return FixtureFunctionDefinition( @@ -1248,7 +1215,13 @@ class FixtureFunctionDefinition: ) def __call__(self, *args: Any, **kwds: Any) -> Any: - return self.get_real_func(*args, **kwds) + message = ( + f'Fixture "{self.name}" called directly. Fixtures are not meant to be called directly,\n' + "but are created automatically when test functions request them as parameters.\n" + "See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n" + "https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly" + ) + fail(message, pytrace=False) def get_real_func(self): if self.instance is not None: @@ -1792,26 +1765,19 @@ class FixtureManager: # Magic globals with __getattr__ might have got us a wrong # fixture attribute. continue - - if marker.name: - name = marker.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. - # func = get_real_method(obj, holderobj) - func = obj.get_real_func() - - self._register_fixture( - name=name, - nodeid=nodeid, - func=func, - scope=marker.scope, - params=marker.params, - ids=marker.ids, - autouse=marker.autouse, - ) + if isinstance(obj, FixtureFunctionDefinition): + if marker.name: + name = marker.name + func = obj.get_real_func() + self._register_fixture( + name=name, + nodeid=nodeid, + func=func, + scope=marker.scope, + params=marker.params, + ids=marker.ids, + autouse=marker.autouse, + ) def getfixturedefs( self, argname: str, node: nodes.Node diff --git a/testing/code/test_source.py b/testing/code/test_source.py index a00259976..9ac673572 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -478,12 +478,13 @@ def test_source_with_decorator() -> None: def deco_fixture(): assert False - src = inspect.getsource(deco_fixture) + # Since deco_fixture is now an instance of FixtureFunctionDef the getsource function will not work on it. + with pytest.raises(Exception): + inspect.getsource(deco_fixture) + src = inspect.getsource(deco_fixture.get_real_func()) assert src == " @pytest.fixture\n def deco_fixture():\n assert False\n" - # currently Source does not unwrap decorators, testing the - # existing behavior here for explicitness, but perhaps we should revisit/change this - # in the future - assert str(Source(deco_fixture)).startswith("@functools.wraps(function)") + # Make sure the decorator is not a wrapped function + assert not str(Source(deco_fixture)).startswith("@functools.wraps(function)") assert ( textwrap.indent(str(Source(get_real_func(deco_fixture))), " ") + "\n" == src ) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index d3cff38f9..5076c3ed8 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4465,6 +4465,21 @@ def test_fixture_double_decorator(pytester: Pytester) -> None: ) +def test_fixture_class(pytester: Pytester) -> None: + """Check if an error is raised when using @pytest.fixture on a class.""" + pytester.makepyfile( + """ + import pytest + + @pytest.fixture + class A: + pass + """ + ) + result = pytester.runpytest() + result.assert_outcomes(errors=1) + + def test_fixture_param_shadowing(pytester: Pytester) -> None: """Parametrized arguments would be shadowed if a fixture with the same name also exists (#5036)""" pytester.makepyfile( diff --git a/testing/test_collection.py b/testing/test_collection.py index 8ff38a334..c1eb236aa 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1310,7 +1310,7 @@ def test_collect_handles_raising_on_dunder_class(pytester: Pytester) -> None: """ ) result = pytester.runpytest() - result.stdout.fnmatch_lines(["*1 passed in*"]) + result.assert_outcomes(passed=1) assert result.ret == 0 @@ -1374,7 +1374,7 @@ def test_collect_pyargs_with_testpaths( with monkeypatch.context() as mp: mp.chdir(root) result = pytester.runpytest_subprocess() - result.stdout.fnmatch_lines(["*1 passed in*"]) + result.assert_outcomes(passed=1) def test_initial_conftests_with_testpaths(pytester: Pytester) -> None: