From 7fd947167df0e60da476c82a1247edb359f17950 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Tue, 18 Jun 2024 12:24:48 +0200 Subject: [PATCH 01/20] feat: use repr for pytest fixtures --- src/_pytest/assertion/rewrite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index b29a254f5..4037aa959 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -462,7 +462,7 @@ def _format_assertmsg(obj: object) -> str: def _should_repr_global_name(obj: object) -> bool: if callable(obj): - return False + return hasattr(obj, "__pytest_wrapped__") try: return not hasattr(obj, "__name__") From 46d2ccbfd4b7a2f75da65c330adcbabbd974365f Mon Sep 17 00:00:00 2001 From: Glyphack Date: Wed, 19 Jun 2024 13:36:28 +0200 Subject: [PATCH 02/20] (fixture) create a new class to represent fixtures --- src/_pytest/fixtures.py | 75 ++++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 12 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 383084e07..58721c7d2 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -45,7 +45,8 @@ 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 get_real_method from _pytest.compat import getfuncargnames from _pytest.compat import getimfunc from _pytest.compat import getlocation @@ -1186,7 +1187,7 @@ class FixtureFunctionMarker: def __post_init__(self, _ispytest: bool) -> None: check_ispytest(_ispytest) - def __call__(self, function: FixtureFunction) -> FixtureFunction: + def __call__(self, function: FixtureFunction) -> "FixtureFunctionDefinition": if inspect.isclass(function): raise ValueError("class fixtures not supported (maybe in the future)") @@ -1198,7 +1199,9 @@ class FixtureFunctionMarker: if hasattr(function, "pytestmark"): warnings.warn(MARKED_FIXTURE, stacklevel=2) - function = wrap_function_to_error_out_if_called_directly(function, self) + 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": @@ -1209,13 +1212,53 @@ class FixtureFunctionMarker: ) # Type ignored because https://github.com/python/mypy/issues/2087. - function._pytestfixturefunction = self # type: ignore[attr-defined] - return function + # function._pytestfixturefunction = self # type: ignore[attr-defined] + # return function + return fixture_definition + + def __repr__(self): + return "fixture" + + +class FixtureFunctionDefinition: + def __init__( + self, + function: Callable[..., object], + fixture_function_marker: FixtureFunctionMarker, + instance: Optional[type] = None, + ): + self.name = fixture_function_marker.name or function.__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}" + + def __get__(self, instance, owner=None): + return FixtureFunctionDefinition( + self.fixture_function, self.fixture_function_marker, instance + ) + + def __call__(self, *args: Any, **kwds: Any) -> Any: + return self.get_real_func(*args, **kwds) + + def get_real_func(self): + if self.instance is not None: + return self.fixture_function.__get__(self.instance) + return self.fixture_function @overload def fixture( - fixture_function: FixtureFunction, + fixture_function: Callable[..., object], *, scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ..., params: Optional[Iterable[object]] = ..., @@ -1224,7 +1267,7 @@ def fixture( Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] ] = ..., name: Optional[str] = ..., -) -> FixtureFunction: ... +) -> FixtureFunctionDefinition: ... @overload @@ -1238,7 +1281,7 @@ def fixture( Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] ] = ..., name: Optional[str] = None, -) -> FixtureFunctionMarker: ... +) -> FixtureFunctionDefinition: ... def fixture( @@ -1251,7 +1294,7 @@ def fixture( Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] ] = None, name: Optional[str] = None, -) -> Union[FixtureFunctionMarker, FixtureFunction]: +) -> Union[FixtureFunctionMarker, FixtureFunctionDefinition]: """Decorator to mark a fixture factory function. This decorator can be used, with or without parameters, to define a @@ -1321,7 +1364,7 @@ def fixture( def yield_fixture( fixture_function=None, *args, - scope="function", + scope: _ScopeName = "function", params=None, autouse=False, ids=None, @@ -1644,6 +1687,13 @@ class FixtureManager: ] = None, autouse: bool = False, ) -> None: + if name == "fixt2": + print(name) + print(func) + print(nodeid) + print(scope) + print(ids) + print(autouse) """Register a fixture :param name: @@ -1735,7 +1785,7 @@ class FixtureManager: self._holderobjseen.add(holderobj) for name in dir(holderobj): # The attribute can be an arbitrary descriptor, so the attribute - # access below can raise. safe_getatt() ignores such exceptions. + # access below can raise. safe_getattr() ignores such exceptions. obj = safe_getattr(holderobj, name, None) marker = getfixturemarker(obj) if not isinstance(marker, FixtureFunctionMarker): @@ -1750,7 +1800,8 @@ class FixtureManager: # 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 = get_real_method(obj, holderobj) + func = obj.get_real_func() self._register_fixture( name=name, From 31c8fbed1eaadf9d2c7d9e79598ed63cf5c29993 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 13:46:25 +0200 Subject: [PATCH 03/20] 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: From bc2be19b63fc879bd8ed6078974d0d7e58d0ce8d Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 13:53:25 +0200 Subject: [PATCH 04/20] Update AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 347efad57..63e5dd60a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -376,6 +376,7 @@ Serhii Mozghovyi Seth Junot Shantanu Jain Sharad Nair +Shaygan Hooshyari Shubham Adep Simon Blanchard Simon Gomizelj From bf41c9fc560b22be6ab9f6e22fffb99cec97b304 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 13:57:38 +0200 Subject: [PATCH 05/20] chore: add changelog entry --- changelog/11525.improvement.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/11525.improvement.rst diff --git a/changelog/11525.improvement.rst b/changelog/11525.improvement.rst new file mode 100644 index 000000000..cc74b5ba9 --- /dev/null +++ b/changelog/11525.improvement.rst @@ -0,0 +1 @@ +Fixtures are now shown as a pytest fixture in tests output. From ef46a374a04169b0e29c01e19eddc4f5e346dd59 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 14:15:28 +0200 Subject: [PATCH 06/20] add test case for asserting missing fixture --- src/_pytest/assertion/rewrite.py | 2 +- src/_pytest/fixtures.py | 3 +++ testing/test_assertrewrite.py | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 4037aa959..a2a4b135c 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -462,7 +462,7 @@ def _format_assertmsg(obj: object) -> str: def _should_repr_global_name(obj: object) -> bool: if callable(obj): - return hasattr(obj, "__pytest_wrapped__") + return hasattr(obj, "_pytestfixturefunction") try: return not hasattr(obj, "__name__") diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 3b31fccf4..71aea5eee 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1200,6 +1200,9 @@ class FixtureFunctionDefinition: ): self.name = fixture_function_marker.name or function.__name__ self.__name__ = self.name + # This attribute is only used to check if an arbitrary python object is a fixture. + # Using isinstance on every object in code might execute code that is not intended to be executed. + # Like lazy loaded classes. self._pytestfixturefunction = fixture_function_marker self.__pytest_wrapped__ = _PytestWrapper(function) self.fixture_function_marker = fixture_function_marker diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 8db9dbbe5..b6e4c4112 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -744,6 +744,23 @@ class TestAssertionRewrite: assert "UnicodeDecodeError" not in msg assert "UnicodeEncodeError" not in msg + def test_assert_fixture(self, pytester: Pytester) -> None: + pytester.makepyfile( + """\ + import pytest + @pytest.fixture + def fixt(): + return 42 + + def test_something(): # missing "fixt" argument + assert fixt == 42 + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + ["*assert pytest_fixture() == 42*"] + ) + class TestRewriteOnImport: def test_pycache_is_a_file(self, pytester: Pytester) -> None: From 91fc20876a96efd5137025bd266dd9206161d61c Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 15:16:47 +0200 Subject: [PATCH 07/20] refactor: removed PytestWrapper class --- src/_pytest/compat.py | 28 ++++++---------------------- src/_pytest/fixtures.py | 2 -- testing/test_compat.py | 16 +++++++++------- 3 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 02256cfc7..25e66f65c 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -3,7 +3,6 @@ from __future__ import annotations -import dataclasses import enum import functools import inspect @@ -210,30 +209,15 @@ 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. - - 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. - """ - - obj: Any - - def get_real_func(obj): """Get the real function object of the (possibly) wrapped object by - functools.wraps or functools.partial.""" + functools.wraps or functools.partial or pytest.fixture""" + from _pytest.fixtures import FixtureFunctionDefinition + start_obj = obj - 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 isinstance(new_obj, _PytestWrapper): - obj = new_obj.obj + for _ in range(100): + if isinstance(obj, FixtureFunctionDefinition): + obj = obj.get_real_func() 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 71aea5eee..500e163be 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -42,7 +42,6 @@ from _pytest._code import Source from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr 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 getfuncargnames @@ -1204,7 +1203,6 @@ class FixtureFunctionDefinition: # Using isinstance on every object in code might execute code that is not intended to be executed. # Like lazy loaded classes. self._pytestfixturefunction = fixture_function_marker - self.__pytest_wrapped__ = _PytestWrapper(function) self.fixture_function_marker = fixture_function_marker self.fixture_function = function self.instance = instance diff --git a/testing/test_compat.py b/testing/test_compat.py index 73ac1bad8..9c8a2f9d4 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -7,7 +7,6 @@ import sys from typing import TYPE_CHECKING from typing import Union -from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never from _pytest.compat import get_real_func from _pytest.compat import is_generator @@ -52,8 +51,8 @@ def test_real_func_loop_limit() -> None: with pytest.raises( ValueError, match=( - "could not find real function of \n" - "stopped at " + "could not find real function of \n" + "stopped at " ), ): get_real_func(evil) @@ -78,10 +77,13 @@ def test_get_real_func() -> None: 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__ = _PytestWrapper(wrapped_func) - assert get_real_func(wrapped_func2) is wrapped_func + # obtain the function up until the point a function was wrapped by pytest itself + @pytest.fixture + def wrapped_func3(): + pass + + wrapped_func4 = decorator(wrapped_func3) + assert get_real_func(wrapped_func4) is wrapped_func3.get_real_func() def test_get_real_func_partial() -> None: From 1f4d9ab2cb0b1bb72013d85eefe27a3eee920bd6 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 15:21:37 +0200 Subject: [PATCH 08/20] chore: update changelog --- changelog/11525.improvement.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/changelog/11525.improvement.rst b/changelog/11525.improvement.rst index cc74b5ba9..e418718ba 100644 --- a/changelog/11525.improvement.rst +++ b/changelog/11525.improvement.rst @@ -1 +1,5 @@ -Fixtures are now shown as a pytest fixture in tests output. +The fixtures are now represented as fixture in test output. + +Fixtures are now a class object of type `FixtureFunctionDefinition`. + +-- by :user:`the-compiler` and :user:`glyphack`. From 794390b24c415a28f4ea681f7157b7e1d836cadb Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 15:31:24 +0200 Subject: [PATCH 09/20] refactor: remove _pytestfixturefunction attribute --- src/_pytest/assertion/rewrite.py | 2 +- src/_pytest/compat.py | 2 +- src/_pytest/fixtures.py | 25 ++++++++++++------------- testing/code/test_source.py | 2 +- testing/test_compat.py | 2 +- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index a2a4b135c..818933b6d 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -462,7 +462,7 @@ def _format_assertmsg(obj: object) -> str: def _should_repr_global_name(obj: object) -> bool: if callable(obj): - return hasattr(obj, "_pytestfixturefunction") + return hasattr(obj, "_fixture_function_marker") try: return not hasattr(obj, "__name__") diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 25e66f65c..bd458d9aa 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -217,7 +217,7 @@ def get_real_func(obj): start_obj = obj for _ in range(100): if isinstance(obj, FixtureFunctionDefinition): - obj = obj.get_real_func() + obj = obj._get_wrapped_function() 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 500e163be..538a262cf 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -155,7 +155,7 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: exceptions.""" return cast( Optional[FixtureFunctionMarker], - safe_getattr(obj, "_pytestfixturefunction", None), + safe_getattr(obj, "_fixture_function_marker", None), ) @@ -1193,7 +1193,7 @@ class FixtureFunctionDefinition: def __init__( self, - function: Callable[..., object], + function: Callable[..., Any], fixture_function_marker: FixtureFunctionMarker, instance: Optional[type] = None, ): @@ -1202,17 +1202,16 @@ class FixtureFunctionDefinition: # This attribute is only used to check if an arbitrary python object is a fixture. # Using isinstance on every object in code might execute code that is not intended to be executed. # Like lazy loaded classes. - self._pytestfixturefunction = fixture_function_marker - self.fixture_function_marker = fixture_function_marker - self.fixture_function = function - self.instance = instance + self._fixture_function_marker = fixture_function_marker + self._fixture_function = function + self._instance = instance def __repr__(self) -> str: - return f"pytest_fixture({self.fixture_function})" + return f"pytest_fixture({self._fixture_function})" def __get__(self, instance, owner=None): return FixtureFunctionDefinition( - self.fixture_function, self.fixture_function_marker, instance + self._fixture_function, self._fixture_function_marker, instance ) def __call__(self, *args: Any, **kwds: Any) -> Any: @@ -1224,10 +1223,10 @@ class FixtureFunctionDefinition: ) fail(message, pytrace=False) - def get_real_func(self): - if self.instance is not None: - return self.fixture_function.__get__(self.instance) - return self.fixture_function + def _get_wrapped_function(self): + if self._instance is not None: + return self._fixture_function.__get__(self._instance) + return self._fixture_function @overload @@ -1769,7 +1768,7 @@ class FixtureManager: if isinstance(obj, FixtureFunctionDefinition): if marker.name: name = marker.name - func = obj.get_real_func() + func = obj._get_wrapped_function() self._register_fixture( name=name, nodeid=nodeid, diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 9ac673572..1bf88fed4 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -481,7 +481,7 @@ def test_source_with_decorator() -> None: # 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()) + src = inspect.getsource(deco_fixture._get_wrapped_function()) assert src == " @pytest.fixture\n def deco_fixture():\n assert False\n" # Make sure the decorator is not a wrapped function assert not str(Source(deco_fixture)).startswith("@functools.wraps(function)") diff --git a/testing/test_compat.py b/testing/test_compat.py index 9c8a2f9d4..fb820b165 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -83,7 +83,7 @@ def test_get_real_func() -> None: pass wrapped_func4 = decorator(wrapped_func3) - assert get_real_func(wrapped_func4) is wrapped_func3.get_real_func() + assert get_real_func(wrapped_func4) is wrapped_func3._get_wrapped_function() def test_get_real_func_partial() -> None: From 7a1b23d69165f950d0696e2a05e85c8ac93dd068 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 15:44:26 +0200 Subject: [PATCH 10/20] refactor: remove unused code --- src/_pytest/fixtures.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 538a262cf..7db3aa06f 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1183,9 +1183,6 @@ class FixtureFunctionMarker: return fixture_definition - def __repr__(self): - return "fixture" - # TODO: write docstring class FixtureFunctionDefinition: @@ -1660,13 +1657,6 @@ class FixtureManager: ] = None, autouse: bool = False, ) -> None: - if name == "fixt2": - print(name) - print(func) - print(nodeid) - print(scope) - print(ids) - print(autouse) """Register a fixture :param name: From 91249e0d86723258cf346b518af80214785fd360 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 15:45:38 +0200 Subject: [PATCH 11/20] ci: set no cover for test function --- testing/test_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_compat.py b/testing/test_compat.py index fb820b165..5af45b4ca 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -80,7 +80,7 @@ def test_get_real_func() -> None: # obtain the function up until the point a function was wrapped by pytest itself @pytest.fixture def wrapped_func3(): - pass + pass # pragma: no cover wrapped_func4 = decorator(wrapped_func3) assert get_real_func(wrapped_func4) is wrapped_func3._get_wrapped_function() From d25a8d9d468d6f4fc1ce8edf66182d44d176c411 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 15:47:52 +0200 Subject: [PATCH 12/20] docs: fix docstring --- src/_pytest/fixtures.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 7db3aa06f..7e81b4293 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1184,10 +1184,7 @@ class FixtureFunctionMarker: return fixture_definition -# 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[..., Any], From 3512997e76c33861aed5dfc8df6958cb815636c1 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 16:39:39 +0200 Subject: [PATCH 13/20] refactor: replace attribute check with type check --- src/_pytest/fixtures.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 7e81b4293..d8024800e 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -153,10 +153,9 @@ def get_scope_node(node: nodes.Node, scope: Scope) -> Optional[nodes.Node]: def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: """Return fixturemarker or None if it doesn't exist or raised exceptions.""" - return cast( - Optional[FixtureFunctionMarker], - safe_getattr(obj, "_fixture_function_marker", None), - ) + if type(obj) is FixtureFunctionDefinition: + return obj._fixture_function_marker + return None # Algorithm for sorting on a per-parametrized resource setup basis. @@ -1193,7 +1192,7 @@ class FixtureFunctionDefinition: ): self.name = fixture_function_marker.name or function.__name__ self.__name__ = self.name - # This attribute is only used to check if an arbitrary python object is a fixture. + # This attribute is used to check if an arbitrary python object is a fixture. # Using isinstance on every object in code might execute code that is not intended to be executed. # Like lazy loaded classes. self._fixture_function_marker = fixture_function_marker @@ -1744,15 +1743,9 @@ class FixtureManager: self._holderobjseen.add(holderobj) for name in dir(holderobj): - # The attribute can be an arbitrary descriptor, so the attribute - # access below can raise. safe_getattr() ignores such exceptions. obj = safe_getattr(holderobj, name, None) - marker = getfixturemarker(obj) - if not isinstance(marker, FixtureFunctionMarker): - # Magic globals with __getattr__ might have got us a wrong - # fixture attribute. - continue - if isinstance(obj, FixtureFunctionDefinition): + if type(obj) is FixtureFunctionDefinition: + marker = obj._fixture_function_marker if marker.name: name = marker.name func = obj._get_wrapped_function() From 6c4682cb07beb3db55697902d05ccb7371b6c3d3 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 16:47:05 +0200 Subject: [PATCH 14/20] chore: update changelog --- changelog/11525.improvement.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/changelog/11525.improvement.rst b/changelog/11525.improvement.rst index e418718ba..0a4c41abb 100644 --- a/changelog/11525.improvement.rst +++ b/changelog/11525.improvement.rst @@ -1,5 +1,3 @@ The fixtures are now represented as fixture in test output. -Fixtures are now a class object of type `FixtureFunctionDefinition`. - -- by :user:`the-compiler` and :user:`glyphack`. From a412a90e5dcbf7c4dc0cb5b6370e95f0a9113b46 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 17:00:09 +0200 Subject: [PATCH 15/20] test: add test for _get_wrapped_function --- src/_pytest/fixtures.py | 6 ++++-- testing/python/fixtures.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d8024800e..ea64a17de 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1216,9 +1216,11 @@ class FixtureFunctionDefinition: ) fail(message, pytrace=False) - def _get_wrapped_function(self): + def _get_wrapped_function(self) -> Callable[..., Any]: if self._instance is not None: - return self._fixture_function.__get__(self._instance) + return cast( + Callable[..., Any], self._fixture_function.__get__(self._instance) + ) return self._fixture_function diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 5076c3ed8..b07d4cdfa 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1,4 +1,5 @@ # mypy: allow-untyped-defs +import inspect import os from pathlib import Path import sys @@ -3295,6 +3296,33 @@ class TestFixtureMarker: assert output1 == output2 +class FixtureFunctionDefTestClass: + def __init__(self) -> None: + self.i = 10 + + @pytest.fixture + def fixture_function_def_test_method(self): + return self.i + + +@pytest.fixture +def fixture_function_def_test_func(): + return 9 + + +def test_get_wrapped_func_returns_method(): + obj = FixtureFunctionDefTestClass() + wrapped_function_result = ( + obj.fixture_function_def_test_method._get_wrapped_function() + ) + assert inspect.ismethod(wrapped_function_result) + assert wrapped_function_result() == 10 + + +def test_get_wrapped_func_returns_function(): + assert fixture_function_def_test_func._get_wrapped_function()() == 9 + + class TestRequestScopeAccess: pytestmark = pytest.mark.parametrize( ("scope", "ok", "error"), From efd0e0914ec8078836299fa4fdca458a29f12733 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 17:05:10 +0200 Subject: [PATCH 16/20] refactor: remove unused code --- src/_pytest/assertion/rewrite.py | 3 ++- src/_pytest/fixtures.py | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 818933b6d..a15be6837 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -35,6 +35,7 @@ from _pytest._io.saferepr import saferepr from _pytest._version import version from _pytest.assertion import util from _pytest.config import Config +from _pytest.fixtures import getfixturemarker from _pytest.main import Session from _pytest.pathlib import absolutepath from _pytest.pathlib import fnmatch_ex @@ -462,7 +463,7 @@ def _format_assertmsg(obj: object) -> str: def _should_repr_global_name(obj: object) -> bool: if callable(obj): - return hasattr(obj, "_fixture_function_marker") + return getfixturemarker(obj) is not None try: return not hasattr(obj, "__name__") diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index ea64a17de..449232c80 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1192,9 +1192,6 @@ class FixtureFunctionDefinition: ): self.name = fixture_function_marker.name or function.__name__ self.__name__ = self.name - # This attribute is used to check if an arbitrary python object is a fixture. - # Using isinstance on every object in code might execute code that is not intended to be executed. - # Like lazy loaded classes. self._fixture_function_marker = fixture_function_marker self._fixture_function = function self._instance = instance From 34b407f98381f4f387b1e13ef9209166321c88b5 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 17:09:15 +0200 Subject: [PATCH 17/20] refactor: replace type with isinstance --- src/_pytest/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 449232c80..5b6f4dddc 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -153,7 +153,7 @@ def get_scope_node(node: nodes.Node, scope: Scope) -> Optional[nodes.Node]: def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: """Return fixturemarker or None if it doesn't exist or raised exceptions.""" - if type(obj) is FixtureFunctionDefinition: + if isinstance(obj, FixtureFunctionDefinition): return obj._fixture_function_marker return None From 2a7cdc6c05782d582aacc60aedb987f6dc16175e Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 17:15:23 +0200 Subject: [PATCH 18/20] docs: update docstring --- src/_pytest/fixtures.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 5b6f4dddc..4f099ebf7 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -151,8 +151,7 @@ def get_scope_node(node: nodes.Node, scope: Scope) -> Optional[nodes.Node]: def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: - """Return fixturemarker or None if it doesn't exist or raised - exceptions.""" + """Return fixturemarker or None if it doesn't exist""" if isinstance(obj, FixtureFunctionDefinition): return obj._fixture_function_marker return None From 289a9d5da8b2fa6d7f2445b55dd2bc4d774942da Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 17:22:18 +0200 Subject: [PATCH 19/20] refactor: use instance check to print repr --- src/_pytest/assertion/rewrite.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index a15be6837..550651e6d 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -35,7 +35,7 @@ from _pytest._io.saferepr import saferepr from _pytest._version import version from _pytest.assertion import util from _pytest.config import Config -from _pytest.fixtures import getfixturemarker +from _pytest.fixtures import FixtureFunctionDefinition from _pytest.main import Session from _pytest.pathlib import absolutepath from _pytest.pathlib import fnmatch_ex @@ -463,7 +463,7 @@ def _format_assertmsg(obj: object) -> str: def _should_repr_global_name(obj: object) -> bool: if callable(obj): - return getfixturemarker(obj) is not None + return isinstance(obj, FixtureFunctionDefinition) try: return not hasattr(obj, "__name__") From 260b5a761493d94716228d77daa0961afc40f234 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 17:48:21 +0200 Subject: [PATCH 20/20] docs: ignore FixtureFunctionDefinition from docs This is an internal class users don't need to know about it. --- doc/en/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/en/conf.py b/doc/en/conf.py index 3e87003c5..a5a09fa29 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -190,6 +190,7 @@ nitpick_ignore = [ ("py:class", "TerminalReporter"), ("py:class", "_pytest._code.code.TerminalRepr"), ("py:class", "_pytest.fixtures.FixtureFunctionMarker"), + ("py:class", "_pytest.fixtures.FixtureFunctionDefinition"), ("py:class", "_pytest.logging.LogCaptureHandler"), ("py:class", "_pytest.mark.structures.ParameterSet"), # Intentionally undocumented/private