From 41c7c2c19b3bbc5cd27914ae35438951720ff90c Mon Sep 17 00:00:00 2001 From: Glyphack Date: Tue, 18 Jun 2024 12:24:48 +0200 Subject: [PATCH 01/38] 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 37c09b03467..eeaaccb0b28 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -472,7 +472,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 cee9044541d962e23ab104f48a0b75b619c7c569 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Wed, 19 Jun 2024 13:36:28 +0200 Subject: [PATCH 02/38] (fixture) create a new class to represent fixtures --- src/_pytest/fixtures.py | 91 +++++++++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 5817e88f47d..743c455a0d7 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -45,7 +45,8 @@ 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 @@ -1223,7 +1224,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)") @@ -1235,7 +1236,9 @@ def __call__(self, function: FixtureFunction) -> FixtureFunction: 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": @@ -1246,20 +1249,62 @@ def __call__(self, function: FixtureFunction) -> FixtureFunction: ) # 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: _ScopeName | Callable[[str, Config], _ScopeName] = ..., params: Iterable[object] | None = ..., autouse: bool = ..., - ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., - name: str | None = ..., -) -> FixtureFunction: ... + ids: Optional[ + Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] + ] = ..., + name: Optional[str] = ..., +) -> FixtureFunctionDefinition: ... @overload @@ -1269,9 +1314,11 @@ def fixture( scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., params: Iterable[object] | None = ..., autouse: bool = ..., - ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., - name: str | None = None, -) -> FixtureFunctionMarker: ... + ids: Optional[ + Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] + ] = ..., + name: Optional[str] = None, +) -> FixtureFunctionDefinition: ... def fixture( @@ -1280,9 +1327,11 @@ def fixture( scope: _ScopeName | Callable[[str, Config], _ScopeName] = "function", params: Iterable[object] | None = None, autouse: bool = False, - ids: Sequence[object | None] | Callable[[Any], object | None] | None = None, - name: str | None = None, -) -> FixtureFunctionMarker | FixtureFunction: + ids: Optional[ + Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] + ] = None, + name: Optional[str] = None, +) -> Union[FixtureFunctionMarker, FixtureFunctionDefinition]: """Decorator to mark a fixture factory function. This decorator can be used, with or without parameters, to define a @@ -1352,7 +1401,7 @@ def fixture( def yield_fixture( fixture_function=None, *args, - scope="function", + scope: _ScopeName = "function", params=None, autouse=False, ids=None, @@ -1671,6 +1720,13 @@ def _register_fixture( ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = 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: @@ -1787,7 +1843,8 @@ def parsefactories( # 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 004f29fc4b581be224832a3511630cc3f3f752ed Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 13:46:25 +0200 Subject: [PATCH 03/38] fix: update code to use new fixture definitions --- src/_pytest/compat.py | 15 +------ src/_pytest/fixtures.py | 88 +++++++++++-------------------------- testing/code/test_source.py | 11 ++--- testing/python/fixtures.py | 15 +++++++ testing/test_collection.py | 4 +- 5 files changed, 50 insertions(+), 83 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 82aea5e635e..7718b890cb7 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -205,6 +205,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. @@ -244,20 +245,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 743c455a0d7..af0d8e4ffd4 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -45,8 +45,6 @@ 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 @@ -1185,31 +1183,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: @@ -1228,7 +1201,7 @@ def __call__(self, function: FixtureFunction) -> "FixtureFunctionDefinition": 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}" ) @@ -1238,8 +1211,6 @@ def __call__(self, function: FixtureFunction) -> "FixtureFunctionDefinition": 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) @@ -1248,16 +1219,16 @@ def __call__(self, function: FixtureFunction) -> "FixtureFunctionDefinition": 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], @@ -1265,19 +1236,15 @@ def __init__( 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( @@ -1285,7 +1252,13 @@ def __get__(self, instance, owner=None): ) 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: @@ -1833,28 +1806,19 @@ def parsefactories( # fixture attribute. continue - # OK we know it is a fixture -- now safe to look up on the _instance_. - obj = getattr(holderobj, name) - - 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_ub, FixtureFunctionDefinition): + if marker.name: + name = marker.name + func = obj_ub.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 a00259976c4..9ac6735721c 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -478,12 +478,13 @@ def deco_mark(): 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 c939b221f22..7cd6b9f224e 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4509,6 +4509,21 @@ def fixt(): ) +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 7d28610e015..ccd57eeef43 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1284,7 +1284,7 @@ def test_1(): """ ) result = pytester.runpytest() - result.stdout.fnmatch_lines(["*1 passed in*"]) + result.assert_outcomes(passed=1) assert result.ret == 0 @@ -1348,7 +1348,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 224595a39f73a24dcdc7f65b1c8e398f61f1f258 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 13:53:25 +0200 Subject: [PATCH 04/38] Update AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index c38f74d9980..134f23fa5c6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -389,6 +389,7 @@ Serhii Mozghovyi Seth Junot Shantanu Jain Sharad Nair +Shaygan Hooshyari Shubham Adep Simon Blanchard Simon Gomizelj From 8666dfb6021a8a11f85e502709f0d8a00d5be55c Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 13:57:38 +0200 Subject: [PATCH 05/38] 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 00000000000..cc74b5ba9be --- /dev/null +++ b/changelog/11525.improvement.rst @@ -0,0 +1 @@ +Fixtures are now shown as a pytest fixture in tests output. From b72403072bc6b74063a36468d3b7d3a62b928744 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 14:15:28 +0200 Subject: [PATCH 06/38] 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 eeaaccb0b28..d3aa5a80886 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -472,7 +472,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 af0d8e4ffd4..118a4b1037e 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1237,6 +1237,9 @@ def __init__( ): 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 7be473d897a..6de9d5669ba 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -975,6 +975,23 @@ def __repr__(self): 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 4552e0631381c7ece4a2a6f0d13ca7985ff90c38 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 15:16:47 +0200 Subject: [PATCH 07/38] 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 7718b890cb7..b492b87853e 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 @@ -205,30 +204,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 118a4b1037e..e78bcf2cfcf 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -42,7 +42,6 @@ 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 @@ -1241,7 +1240,6 @@ def __init__( # 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 86868858956..9acd73e30bd 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -7,7 +7,6 @@ from functools import wraps from typing import TYPE_CHECKING -from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never from _pytest.compat import get_real_func from _pytest.compat import safe_getattr @@ -39,8 +38,8 @@ def __getattr__(self, attr): 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) @@ -65,10 +64,13 @@ def 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__ = _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 1efc23000220108aab52fc369257841675324cea Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 15:21:37 +0200 Subject: [PATCH 08/38] 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 cc74b5ba9be..e418718ba0c 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 b6f87a3309d1bfb39278a9a2a68318340ad8983a Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 15:31:24 +0200 Subject: [PATCH 09/38] refactor: remove _pytestfixturefunction attribute --- src/_pytest/assertion/rewrite.py | 2 +- src/_pytest/compat.py | 2 +- src/_pytest/fixtures.py | 23 +++++++++++------------ testing/code/test_source.py | 2 +- testing/test_compat.py | 2 +- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index d3aa5a80886..9f87cbd15e6 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -472,7 +472,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 b492b87853e..4b348395044 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -212,7 +212,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 e78bcf2cfcf..90c93fb6f2d 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -156,7 +156,7 @@ def getfixturemarker(obj: object) -> FixtureFunctionMarker | None: exceptions.""" return cast( Optional[FixtureFunctionMarker], - safe_getattr(obj, "_pytestfixturefunction", None), + safe_getattr(obj, "_fixture_function_marker", None), ) @@ -1230,7 +1230,7 @@ class FixtureFunctionDefinition: def __init__( self, - function: Callable[..., object], + function: Callable[..., Any], fixture_function_marker: FixtureFunctionMarker, instance: Optional[type] = None, ): @@ -1239,17 +1239,16 @@ def __init__( # 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: @@ -1261,10 +1260,10 @@ def __call__(self, *args: Any, **kwds: Any) -> Any: ) 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 diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 9ac6735721c..1bf88fed4d8 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -481,7 +481,7 @@ def 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()) + 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 9acd73e30bd..cfcdd94860a 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -70,7 +70,7 @@ def wrapped_func3(): 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 bc69c19084094aa496016a425d3608f6420e2d14 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 15:44:26 +0200 Subject: [PATCH 10/38] 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 90c93fb6f2d..d83004ef3a3 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1220,9 +1220,6 @@ def __call__(self, function: FixtureFunction) -> "FixtureFunctionDefinition": return fixture_definition - def __repr__(self): - return "fixture" - # TODO: write docstring class FixtureFunctionDefinition: @@ -1693,13 +1690,6 @@ def _register_fixture( ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = 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 da5e8e93f0033d91315355788ceced21d5c5b2b3 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 15:45:38 +0200 Subject: [PATCH 11/38] 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 cfcdd94860a..98570344d48 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -67,7 +67,7 @@ def func(): # 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 ec7f7b13d4b48c40d28ba8b43983b2cec6ca9a06 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 15:47:52 +0200 Subject: [PATCH 12/38] 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 d83004ef3a3..b2cf85c2ed1 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1221,10 +1221,7 @@ def __call__(self, function: FixtureFunction) -> "FixtureFunctionDefinition": 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 f61c23c5d0dd63b9e7fe4326eb1513c4f97c4f62 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 16:39:39 +0200 Subject: [PATCH 13/38] refactor: replace attribute check with type check --- src/_pytest/fixtures.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index b2cf85c2ed1..70f64ae7145 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -154,10 +154,9 @@ def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None: def getfixturemarker(obj: object) -> FixtureFunctionMarker | None: """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. @@ -1230,7 +1229,7 @@ def __init__( ): 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 @@ -1792,8 +1791,7 @@ def parsefactories( # Magic globals with __getattr__ might have got us a wrong # fixture attribute. continue - - if isinstance(obj_ub, FixtureFunctionDefinition): + if isinstance(obj, FixtureFunctionDefinition): if marker.name: name = marker.name func = obj_ub.get_real_func() From 144befbc9cd28d6fb16ebc75c8dc6174f83186b4 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 16:47:05 +0200 Subject: [PATCH 14/38] 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 e418718ba0c..0a4c41abbd1 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 e556218274ce882f22bf814888334bfacbdba741 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 17:00:09 +0200 Subject: [PATCH 15/38] 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 70f64ae7145..53829992b25 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1253,9 +1253,11 @@ def __call__(self, *args: Any, **kwds: Any) -> Any: ) 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 7cd6b9f224e..cfc3ccf1b57 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1,6 +1,7 @@ # mypy: allow-untyped-defs from __future__ import annotations +import inspect import os from pathlib import Path import sys @@ -3339,6 +3340,33 @@ def test_foo(B): 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 a48b2a0d00629103c33d540e940b2054bca9acde Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 17:05:10 +0200 Subject: [PATCH 16/38] 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 9f87cbd15e6..74e31265a22 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -31,6 +31,7 @@ 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 @@ -472,7 +473,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 53829992b25..e3b8ce57591 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1229,9 +1229,6 @@ def __init__( ): 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 c7d8339bbd59161a2b85d107f57aa6399936398f Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 17:09:15 +0200 Subject: [PATCH 17/38] 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 e3b8ce57591..40ef67726b1 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -154,7 +154,7 @@ def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None: def getfixturemarker(obj: object) -> FixtureFunctionMarker | None: """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 6ba2370fcdc29b03d3c7041a7e2e897d1802c791 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 17:15:23 +0200 Subject: [PATCH 18/38] docs: update docstring --- src/_pytest/fixtures.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 40ef67726b1..19555b99012 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -151,9 +151,8 @@ def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None: assert_never(scope) -def getfixturemarker(obj: object) -> FixtureFunctionMarker | None: - """Return fixturemarker or None if it doesn't exist or raised - exceptions.""" +def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: + """Return fixturemarker or None if it doesn't exist""" if isinstance(obj, FixtureFunctionDefinition): return obj._fixture_function_marker return None From 3bc5ad168ef378a99bd105ce1cd6b49dd8c1ff76 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 17:22:18 +0200 Subject: [PATCH 19/38] 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 74e31265a22..33785dfb9a6 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -31,7 +31,7 @@ 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 @@ -473,7 +473,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 04a0cde7df57202803e83f4e6731e1ecef26d0b6 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Thu, 20 Jun 2024 17:48:21 +0200 Subject: [PATCH 20/38] 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 9558a75f927..47fc70dce85 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -75,6 +75,7 @@ ("py:class", "_pytest._code.code.TerminalRepr"), ("py:class", "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 From 9822c946b9c331f12bfad32bfa8e98694159c306 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Jul 2024 18:47:14 +0000 Subject: [PATCH 21/38] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/fixtures.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 19555b99012..c7a026e2af3 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -151,7 +151,7 @@ def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None: assert_never(scope) -def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: +def getfixturemarker(obj: object) -> FixtureFunctionMarker | None: """Return fixturemarker or None if it doesn't exist""" if isinstance(obj, FixtureFunctionDefinition): return obj._fixture_function_marker @@ -1194,7 +1194,7 @@ class FixtureFunctionMarker: def __post_init__(self, _ispytest: bool) -> None: check_ispytest(_ispytest) - def __call__(self, function: FixtureFunction) -> "FixtureFunctionDefinition": + def __call__(self, function: FixtureFunction) -> FixtureFunctionDefinition: if inspect.isclass(function): raise ValueError("class fixtures not supported (maybe in the future)") @@ -1224,7 +1224,7 @@ def __init__( self, function: Callable[..., Any], fixture_function_marker: FixtureFunctionMarker, - instance: Optional[type] = None, + instance: type | None = None, ): self.name = fixture_function_marker.name or function.__name__ self.__name__ = self.name @@ -1264,10 +1264,8 @@ def fixture( scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., params: Iterable[object] | None = ..., autouse: bool = ..., - ids: Optional[ - Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] - ] = ..., - name: Optional[str] = ..., + ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., + name: str | None = ..., ) -> FixtureFunctionDefinition: ... @@ -1278,10 +1276,8 @@ def fixture( scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., params: Iterable[object] | None = ..., autouse: bool = ..., - ids: Optional[ - Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] - ] = ..., - name: Optional[str] = None, + ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., + name: str | None = None, ) -> FixtureFunctionDefinition: ... @@ -1291,11 +1287,9 @@ def fixture( scope: _ScopeName | Callable[[str, Config], _ScopeName] = "function", params: Iterable[object] | None = None, autouse: bool = False, - ids: Optional[ - Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] - ] = None, - name: Optional[str] = None, -) -> Union[FixtureFunctionMarker, FixtureFunctionDefinition]: + ids: Sequence[object | None] | Callable[[Any], object | None] | None = None, + name: str | None = None, +) -> FixtureFunctionMarker | FixtureFunctionDefinition: """Decorator to mark a fixture factory function. This decorator can be used, with or without parameters, to define a From 786aec5eb4fc1e38d24ae909330f79dbb7c51d2d Mon Sep 17 00:00:00 2001 From: Shaygan Hooshyari Date: Thu, 4 Jul 2024 14:16:44 -0700 Subject: [PATCH 22/38] Update changelog/11525.improvement.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) --- changelog/11525.improvement.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/11525.improvement.rst b/changelog/11525.improvement.rst index 0a4c41abbd1..10d6ca8c353 100644 --- a/changelog/11525.improvement.rst +++ b/changelog/11525.improvement.rst @@ -1,3 +1,3 @@ The fixtures are now represented as fixture in test output. --- by :user:`the-compiler` and :user:`glyphack`. +-- by :user:`the-compiler` and :user:`glyphack` From afe72f6ec921339e4767728c594889ada641f06e Mon Sep 17 00:00:00 2001 From: Shaygan Hooshyari Date: Thu, 4 Jul 2024 14:17:09 -0700 Subject: [PATCH 23/38] Update src/_pytest/fixtures.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) --- 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 c7a026e2af3..ad305c60448 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1233,7 +1233,7 @@ def __init__( self._instance = instance def __repr__(self) -> str: - return f"pytest_fixture({self._fixture_function})" + return f"" def __get__(self, instance, owner=None): return FixtureFunctionDefinition( From 3b66f724b3585ebb9ee65e299b1481d66609a6d3 Mon Sep 17 00:00:00 2001 From: Shaygan Hooshyari Date: Thu, 4 Jul 2024 14:17:26 -0700 Subject: [PATCH 24/38] Update src/_pytest/fixtures.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) --- src/_pytest/fixtures.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index ad305c60448..78de754c202 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1250,11 +1250,12 @@ def __call__(self, *args: Any, **kwds: Any) -> Any: fail(message, pytrace=False) def _get_wrapped_function(self) -> Callable[..., Any]: - if self._instance is not None: - return cast( - Callable[..., Any], self._fixture_function.__get__(self._instance) - ) - return self._fixture_function + if self._instance is None: + return self._fixture_function + + return cast( + Callable[..., Any], self._fixture_function.__get__(self._instance) + ) @overload From 149cc30cda6335f8ea26de5b66ec2814f9f78ded Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Jul 2024 21:17:45 +0000 Subject: [PATCH 25/38] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/fixtures.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 78de754c202..5d32d341823 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1253,9 +1253,7 @@ def _get_wrapped_function(self) -> Callable[..., Any]: if self._instance is None: return self._fixture_function - return cast( - Callable[..., Any], self._fixture_function.__get__(self._instance) - ) + return cast(Callable[..., Any], self._fixture_function.__get__(self._instance)) @overload From eb3943c56252202a9a04e40d0b2b49750a05acbc Mon Sep 17 00:00:00 2001 From: Shaygan Hooshyari Date: Thu, 4 Jul 2024 14:22:51 -0700 Subject: [PATCH 26/38] Update src/_pytest/compat.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) --- src/_pytest/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 4b348395044..3d5d09e3d99 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -206,7 +206,7 @@ def ascii_escaped(val: bytes | str) -> str: def get_real_func(obj): """Get the real function object of the (possibly) wrapped object by - functools.wraps or functools.partial or pytest.fixture""" + :func:`functools.wraps`, or :func:`functools.partial`, or :func:`pytest.fixture`.""" from _pytest.fixtures import FixtureFunctionDefinition start_obj = obj From 2891be5f5a97e3abfc8caf479568da1bfb155eaf Mon Sep 17 00:00:00 2001 From: Glyphack Date: Tue, 30 Jul 2024 20:19:14 +0200 Subject: [PATCH 27/38] fix: update fixture repr in text --- testing/test_assertrewrite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 6de9d5669ba..5b8a2ba1333 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -989,7 +989,7 @@ def test_something(): # missing "fixt" argument ) result = pytester.runpytest() result.stdout.fnmatch_lines( - ["*assert pytest_fixture() == 42*"] + ["*assert )> == 42*"] ) From 1734722694f867811c203b691b22eda7131eaf34 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Tue, 30 Jul 2024 20:56:52 +0200 Subject: [PATCH 28/38] fix: check TypeError raised --- testing/code/test_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 1bf88fed4d8..b8ed00a2bb8 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -479,7 +479,7 @@ def deco_fixture(): assert False # Since deco_fixture is now an instance of FixtureFunctionDef the getsource function will not work on it. - with pytest.raises(Exception): + with pytest.raises(TypeError, match=r"FixtureFunctionDefinition"): inspect.getsource(deco_fixture) src = inspect.getsource(deco_fixture._get_wrapped_function()) assert src == " @pytest.fixture\n def deco_fixture():\n assert False\n" From d749473249ebc918d88e964d2b1b126aff2abb38 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Wed, 23 Oct 2024 17:00:02 +0200 Subject: [PATCH 29/38] test: remove redundant test --- src/_pytest/fixtures.py | 12 +++++------- testing/code/test_source.py | 3 --- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 5d32d341823..709572d88b3 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1777,15 +1777,13 @@ def parsefactories( # The attribute can be an arbitrary descriptor, so the attribute # access below can raise. safe_getattr() ignores such exceptions. obj_ub = safe_getattr(holderobj_tp, name, None) - marker = getfixturemarker(obj_ub) - if not isinstance(marker, FixtureFunctionMarker): - # Magic globals with __getattr__ might have got us a wrong - # fixture attribute. - continue - if isinstance(obj, FixtureFunctionDefinition): + if isinstance(obj_ub, FixtureFunctionDefinition): + marker = getfixturemarker(obj_ub) + if marker is None: + raise Exception("marker was none") if marker.name: name = marker.name - func = obj_ub.get_real_func() + func = get_real_func(obj_ub) self._register_fixture( name=name, nodeid=nodeid, diff --git a/testing/code/test_source.py b/testing/code/test_source.py index b8ed00a2bb8..957ff1bb817 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -478,9 +478,6 @@ def deco_mark(): def deco_fixture(): assert False - # Since deco_fixture is now an instance of FixtureFunctionDef the getsource function will not work on it. - with pytest.raises(TypeError, match=r"FixtureFunctionDefinition"): - inspect.getsource(deco_fixture) 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 From fa2f2b57b237ca8808bb9b5027867f87b31585ad Mon Sep 17 00:00:00 2001 From: Glyphack Date: Wed, 23 Oct 2024 20:56:07 +0200 Subject: [PATCH 30/38] Fix tests --- src/_pytest/compat.py | 18 ++++++++++++++++++ src/_pytest/fixtures.py | 12 ++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 3d5d09e3d99..8454fdf3ede 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -112,6 +112,10 @@ def getfuncargnames( The name parameter should be the original name in which the function was collected. """ + # if name == "session_request": + # import pdb + # breakpoint() + # TODO(RonnyPfannschmidt): This function should be refactored when we # revisit fixtures. The fixture mechanism should ask the node for # the fixture names, and not try to obtain directly from the @@ -229,6 +233,20 @@ 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 709572d88b3..ae6d4b0c57d 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -44,6 +44,7 @@ from _pytest._io import TerminalWriter 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 @@ -1778,12 +1779,15 @@ def parsefactories( # access below can raise. safe_getattr() ignores such exceptions. obj_ub = safe_getattr(holderobj_tp, name, None) if isinstance(obj_ub, FixtureFunctionDefinition): - marker = getfixturemarker(obj_ub) - if marker is None: - raise Exception("marker was none") + marker = obj_ub._fixture_function_marker if marker.name: name = marker.name - func = get_real_func(obj_ub) + + # OK we know it is a fixture -- now safe to look up on the _instance_. + obj = getattr(holderobj_tp, name) + + func = get_real_method(obj, holderobj) + self._register_fixture( name=name, nodeid=nodeid, From 1b5b4c7112df33911597db371d304c358ed3b876 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Wed, 23 Oct 2024 21:14:09 +0200 Subject: [PATCH 31/38] Handle scenarios where fixture is renamed in decorator --- src/_pytest/fixtures.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index ae6d4b0c57d..b2adca9ad2d 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1778,13 +1778,17 @@ def parsefactories( # The attribute can be an arbitrary descriptor, so the attribute # access below can raise. safe_getattr() ignores such exceptions. obj_ub = safe_getattr(holderobj_tp, name, None) - if isinstance(obj_ub, FixtureFunctionDefinition): + if type(obj_ub) is FixtureFunctionDefinition: marker = obj_ub._fixture_function_marker if marker.name: name = marker.name # OK we know it is a fixture -- now safe to look up on the _instance_. - obj = getattr(holderobj_tp, name) + try: + obj = getattr(holderobj, name) + # if the fixture is named in the decorator we cannot find it in the module + except AttributeError: + obj = obj_ub func = get_real_method(obj, holderobj) From 6ee850a8da76ac62d34e7a4367d810035e63bca3 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Sun, 27 Oct 2024 10:58:37 +0100 Subject: [PATCH 32/38] Add follow ups as todo comments --- src/_pytest/compat.py | 4 ---- src/_pytest/fixtures.py | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 8454fdf3ede..8323e755158 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -112,10 +112,6 @@ def getfuncargnames( The name parameter should be the original name in which the function was collected. """ - # if name == "session_request": - # import pdb - # breakpoint() - # TODO(RonnyPfannschmidt): This function should be refactored when we # revisit fixtures. The fixture mechanism should ask the node for # the fixture names, and not try to obtain directly from the diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index b2adca9ad2d..0cca1ae3d56 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -152,6 +152,7 @@ def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None: assert_never(scope) +# TODO: Try to use FixtureFunctionDefinition instead of the marker def getfixturemarker(obj: object) -> FixtureFunctionMarker | None: """Return fixturemarker or None if it doesn't exist""" if isinstance(obj, FixtureFunctionDefinition): @@ -1220,6 +1221,7 @@ def __call__(self, function: FixtureFunction) -> FixtureFunctionDefinition: return fixture_definition +# TODO: paramspec/return type annotation tracking and storing class FixtureFunctionDefinition: def __init__( self, From 131e3061f368675b57dccb5d2fe48ec9386b469b Mon Sep 17 00:00:00 2001 From: Glyphack Date: Sun, 27 Oct 2024 12:10:05 +0100 Subject: [PATCH 33/38] Remove get_real_func loop --- src/_pytest/compat.py | 18 ++++-------------- testing/test_compat.py | 5 +---- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 8323e755158..a257a6752e5 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -209,21 +209,11 @@ def get_real_func(obj): :func:`functools.wraps`, or :func:`functools.partial`, or :func:`pytest.fixture`.""" from _pytest.fixtures import FixtureFunctionDefinition - start_obj = obj - for _ in range(100): - if isinstance(obj, FixtureFunctionDefinition): - obj = obj._get_wrapped_function() - break - new_obj = getattr(obj, "__wrapped__", None) - if new_obj is None: - break - obj = new_obj - else: - from _pytest._io.saferepr import saferepr + obj = inspect.unwrap(obj, stop=lambda x: type(x) is FixtureFunctionDefinition) + + if type(obj) == FixtureFunctionDefinition: + obj = obj._get_wrapped_function() - raise ValueError( - f"could not find real function of {saferepr(start_obj)}\nstopped at {saferepr(obj)}" - ) if isinstance(obj, functools.partial): obj = obj.func return obj diff --git a/testing/test_compat.py b/testing/test_compat.py index 98570344d48..3722bfcfb40 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -37,10 +37,7 @@ def __getattr__(self, attr): with pytest.raises( ValueError, - match=( - "could not find real function of \n" - "stopped at " - ), + match=("wrapper loop when unwrapping "), ): get_real_func(evil) From f062ff6d69935d497da3a54b8a8f30b04ad52857 Mon Sep 17 00:00:00 2001 From: Glyphack Date: Sat, 23 Nov 2024 16:19:20 +0100 Subject: [PATCH 34/38] Use _get_wrapped_function to unwrap fixtures --- src/_pytest/compat.py | 23 ++--------------------- src/_pytest/fixtures.py | 9 +++++---- testing/code/test_source.py | 4 +++- testing/test_compat.py | 6 +++++- 4 files changed, 15 insertions(+), 27 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index a257a6752e5..b3771d97c36 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -206,33 +206,14 @@ def ascii_escaped(val: bytes | str) -> str: def get_real_func(obj): """Get the real function object of the (possibly) wrapped object by - :func:`functools.wraps`, or :func:`functools.partial`, or :func:`pytest.fixture`.""" - from _pytest.fixtures import FixtureFunctionDefinition - - obj = inspect.unwrap(obj, stop=lambda x: type(x) is FixtureFunctionDefinition) - - if type(obj) == FixtureFunctionDefinition: - obj = obj._get_wrapped_function() + :func:`functools.wraps`, or :func:`functools.partial`.""" + obj = inspect.unwrap(obj) if isinstance(obj, functools.partial): obj = obj.func 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 0cca1ae3d56..9a27caad94f 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -44,7 +44,6 @@ from _pytest._io import TerminalWriter 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 @@ -1783,7 +1782,9 @@ def parsefactories( if type(obj_ub) is FixtureFunctionDefinition: marker = obj_ub._fixture_function_marker if marker.name: - name = marker.name + fixture_name = marker.name + else: + fixture_name = name # OK we know it is a fixture -- now safe to look up on the _instance_. try: @@ -1792,10 +1793,10 @@ def parsefactories( except AttributeError: obj = obj_ub - func = get_real_method(obj, holderobj) + func = obj._get_wrapped_function() self._register_fixture( - name=name, + name=fixture_name, nodeid=nodeid, func=func, scope=marker.scope, diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 957ff1bb817..b4a485fca0c 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -483,7 +483,9 @@ def deco_fixture(): # 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 + textwrap.indent(str(Source(deco_fixture._get_wrapped_function())), " ") + + "\n" + == src ) diff --git a/testing/test_compat.py b/testing/test_compat.py index 3722bfcfb40..94fa2a7885f 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -67,7 +67,11 @@ def wrapped_func3(): pass # pragma: no cover wrapped_func4 = decorator(wrapped_func3) - assert get_real_func(wrapped_func4) is wrapped_func3._get_wrapped_function() + assert ( + # get_real_func does not unwrap function that is wrapped by fixture hence we need to call _get_wrapped_function + get_real_func(wrapped_func4)._get_wrapped_function() + is wrapped_func3._get_wrapped_function() + ) def test_get_real_func_partial() -> None: From 0c01079c4a410d514db85912d68057b429181078 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 26 Nov 2024 11:02:41 -0300 Subject: [PATCH 35/38] Carry around ParamSpec and return value in FixtureFunctionDefinition --- src/_pytest/fixtures.py | 44 ++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 9a27caad94f..831fa56207b 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -35,6 +35,8 @@ from typing import Union import warnings +from typing_extensions import ParamSpec + import _pytest from _pytest import nodes from _pytest._code import getfslineno @@ -85,8 +87,12 @@ # The value of the fixture -- return/yield of the fixture function (type variable). FixtureValue = TypeVar("FixtureValue") + +# The parameters that a fixture function receives. +FixtureParams = ParamSpec("FixtureParams") + # The type of the fixture function (type variable). -FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object]) +# FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object]) # The type of a fixture function (type alias generic in fixture value). _FixtureFunc = Union[ Callable[..., FixtureValue], Callable[..., Generator[FixtureValue, None, None]] @@ -1195,7 +1201,9 @@ class FixtureFunctionMarker: def __post_init__(self, _ispytest: bool) -> None: check_ispytest(_ispytest) - def __call__(self, function: FixtureFunction) -> FixtureFunctionDefinition: + def __call__( + self, function: Callable[FixtureParams, FixtureValue] + ) -> FixtureFunctionDefinition[FixtureParams, FixtureValue]: if inspect.isclass(function): raise ValueError("class fixtures not supported (maybe in the future)") @@ -1220,14 +1228,13 @@ def __call__(self, function: FixtureFunction) -> FixtureFunctionDefinition: return fixture_definition -# TODO: paramspec/return type annotation tracking and storing -class FixtureFunctionDefinition: +class FixtureFunctionDefinition(Generic[FixtureParams, FixtureValue]): def __init__( self, - function: Callable[..., Any], + function: Callable[FixtureParams, FixtureValue], fixture_function_marker: FixtureFunctionMarker, - instance: type | None = None, - ): + instance: object | None = None, + ) -> None: self.name = fixture_function_marker.name or function.__name__ self.__name__ = self.name self._fixture_function_marker = fixture_function_marker @@ -1237,9 +1244,11 @@ def __init__( def __repr__(self) -> str: return f"" - def __get__(self, instance, owner=None): + def __get__( + self, obj: object, objtype: type | None = None + ) -> FixtureFunctionDefinition[FixtureParams, FixtureValue]: return FixtureFunctionDefinition( - self._fixture_function, self._fixture_function_marker, instance + self._fixture_function, self._fixture_function_marker, obj ) def __call__(self, *args: Any, **kwds: Any) -> Any: @@ -1251,23 +1260,26 @@ def __call__(self, *args: Any, **kwds: Any) -> Any: ) fail(message, pytrace=False) - def _get_wrapped_function(self) -> Callable[..., Any]: + def _get_wrapped_function(self) -> Callable[FixtureParams, FixtureValue]: if self._instance is None: return self._fixture_function - return cast(Callable[..., Any], self._fixture_function.__get__(self._instance)) + return cast( + Callable[FixtureParams, FixtureValue], + self._fixture_function.__get__(self._instance), + ) @overload def fixture( - fixture_function: Callable[..., object], + fixture_function: Callable[FixtureParams, FixtureValue], *, scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., params: Iterable[object] | None = ..., autouse: bool = ..., ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., name: str | None = ..., -) -> FixtureFunctionDefinition: ... +) -> FixtureFunctionDefinition[FixtureParams, FixtureValue]: ... @overload @@ -1279,18 +1291,18 @@ def fixture( autouse: bool = ..., ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., name: str | None = None, -) -> FixtureFunctionDefinition: ... +) -> FixtureFunctionMarker: ... def fixture( - fixture_function: FixtureFunction | None = None, + fixture_function: Callable[FixtureParams, FixtureValue] | None = None, *, scope: _ScopeName | Callable[[str, Config], _ScopeName] = "function", params: Iterable[object] | None = None, autouse: bool = False, ids: Sequence[object | None] | Callable[[Any], object | None] | None = None, name: str | None = None, -) -> FixtureFunctionMarker | FixtureFunctionDefinition: +) -> FixtureFunctionMarker | FixtureFunctionDefinition[FixtureParams, FixtureValue]: """Decorator to mark a fixture factory function. This decorator can be used, with or without parameters, to define a From e18d519d3164fc7592f39ee75f253b458efd1c54 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 26 Nov 2024 11:19:27 -0300 Subject: [PATCH 36/38] Make FixtureDef a Generic to carry the ParamSpec of the fixture function --- src/_pytest/fixtures.py | 63 ++++++++++++++++++++------------------ src/_pytest/hookspec.py | 4 +-- src/_pytest/python.py | 6 ++-- src/_pytest/setuponly.py | 7 +++-- src/_pytest/setupplan.py | 4 ++- testing/python/metafunc.py | 4 +-- 6 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 831fa56207b..03a69f5bc2f 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -93,9 +93,10 @@ # The type of the fixture function (type variable). # FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object]) -# The type of a fixture function (type alias generic in fixture value). +# The type of a fixture function (type alias generic in fixture params and value). _FixtureFunc = Union[ - Callable[..., FixtureValue], Callable[..., Generator[FixtureValue, None, None]] + Callable[FixtureParams, FixtureValue], + Callable[FixtureParams, Generator[FixtureValue, None, None]], ] # The type of FixtureDef.cached_result (type alias generic in fixture value). _FixtureCachedResult = Union[ @@ -128,7 +129,7 @@ def pytest_sessionstart(session: Session) -> None: def get_scope_package( node: nodes.Item, - fixturedef: FixtureDef[object], + fixturedef: FixtureDef[Any, object], ) -> nodes.Node | None: from _pytest.python import Package @@ -325,7 +326,7 @@ class FuncFixtureInfo: # matching the name which are applicable to this function. # There may be multiple overriding fixtures with the same name. The # sequence is ordered from furthest to closes to the function. - name2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] + name2fixturedefs: dict[str, Sequence[FixtureDef[Any, Any]]] def prune_dependency_tree(self) -> None: """Recompute names_closure from initialnames and name2fixturedefs. @@ -366,8 +367,8 @@ def __init__( self, pyfuncitem: Function, fixturename: str | None, - arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]], - fixture_defs: dict[str, FixtureDef[Any]], + arg2fixturedefs: dict[str, Sequence[FixtureDef[Any, Any]]], + fixture_defs: dict[str, FixtureDef[Any, Any]], *, _ispytest: bool = False, ) -> None: @@ -410,7 +411,7 @@ def scope(self) -> _ScopeName: @abc.abstractmethod def _check_scope( self, - requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], + requested_fixturedef: FixtureDef[Any, object] | PseudoFixtureDef[object], requested_scope: Scope, ) -> None: raise NotImplementedError() @@ -551,7 +552,7 @@ def _iter_chain(self) -> Iterator[SubRequest]: def _get_active_fixturedef( self, argname: str - ) -> FixtureDef[object] | PseudoFixtureDef[object]: + ) -> FixtureDef[Any, object] | PseudoFixtureDef[object]: if argname == "request": cached_result = (self, [0], None) return PseudoFixtureDef(cached_result, Scope.Function) @@ -623,7 +624,9 @@ def _get_active_fixturedef( self._fixture_defs[argname] = fixturedef return fixturedef - def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> None: + def _check_fixturedef_without_param( + self, fixturedef: FixtureDef[Any, object] + ) -> None: """Check that this request is allowed to execute this fixturedef without a param.""" funcitem = self._pyfuncitem @@ -656,7 +659,7 @@ def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> Non ) fail(msg, pytrace=False) - def _get_fixturestack(self) -> list[FixtureDef[Any]]: + def _get_fixturestack(self) -> list[FixtureDef[Any, Any]]: values = [request._fixturedef for request in self._iter_chain()] values.reverse() return values @@ -681,7 +684,7 @@ def _scope(self) -> Scope: def _check_scope( self, - requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], + requested_fixturedef: FixtureDef[Any, object] | PseudoFixtureDef[object], requested_scope: Scope, ) -> None: # TopRequest always has function scope so always valid. @@ -715,7 +718,7 @@ def __init__( scope: Scope, param: Any, param_index: int, - fixturedef: FixtureDef[object], + fixturedef: FixtureDef[Any, object], *, _ispytest: bool = False, ) -> None: @@ -728,7 +731,7 @@ def __init__( ) self._parent_request: Final[FixtureRequest] = request self._scope_field: Final = scope - self._fixturedef: Final[FixtureDef[object]] = fixturedef + self._fixturedef: Final[FixtureDef[Any, object]] = fixturedef if param is not NOTSET: self.param = param self.param_index: Final = param_index @@ -758,7 +761,7 @@ def node(self): def _check_scope( self, - requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], + requested_fixturedef: FixtureDef[Any, object] | PseudoFixtureDef[object], requested_scope: Scope, ) -> None: if isinstance(requested_fixturedef, PseudoFixtureDef): @@ -779,7 +782,7 @@ def _check_scope( pytrace=False, ) - def _format_fixturedef_line(self, fixturedef: FixtureDef[object]) -> str: + def _format_fixturedef_line(self, fixturedef: FixtureDef[Any, object]) -> str: factory = fixturedef.func path, lineno = getfslineno(factory) if isinstance(path, Path): @@ -893,7 +896,9 @@ def toterminal(self, tw: TerminalWriter) -> None: def call_fixture_func( - fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs + fixturefunc: _FixtureFunc[FixtureParams, FixtureValue], + request: FixtureRequest, + kwargs: FixtureParams.kwargs, ) -> FixtureValue: if inspect.isgeneratorfunction(fixturefunc): fixturefunc = cast( @@ -954,7 +959,7 @@ def _eval_scope_callable( @final -class FixtureDef(Generic[FixtureValue]): +class FixtureDef(Generic[FixtureParams, FixtureValue]): """A container for a fixture definition. Note: At this time, only explicitly documented fields and methods are @@ -966,7 +971,7 @@ def __init__( config: Config, baseid: str | None, argname: str, - func: _FixtureFunc[FixtureValue], + func: _FixtureFunc[FixtureParams, FixtureValue], scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] | None, params: Sequence[object] | None, ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, @@ -1121,8 +1126,8 @@ def __repr__(self) -> str: def resolve_fixture_function( - fixturedef: FixtureDef[FixtureValue], request: FixtureRequest -) -> _FixtureFunc[FixtureValue]: + fixturedef: FixtureDef[FixtureParams, FixtureValue], request: FixtureRequest +) -> _FixtureFunc[FixtureParams, FixtureValue]: """Get the actual callable that can be called to obtain the fixture value.""" fixturefunc = fixturedef.func @@ -1145,7 +1150,7 @@ def resolve_fixture_function( def pytest_fixture_setup( - fixturedef: FixtureDef[FixtureValue], request: SubRequest + fixturedef: FixtureDef[FixtureParams, FixtureValue], request: SubRequest ) -> FixtureValue: """Execution of fixture setup.""" kwargs = {} @@ -1509,7 +1514,7 @@ def __init__(self, session: Session) -> None: # suite/plugins defined with this name. Populated by parsefactories(). # TODO: The order of the FixtureDefs list of each arg is significant, # explain. - self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any]]]] = {} + self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any, Any]]]] = {} self._holderobjseen: Final[set[object]] = set() # A mapping from a nodeid to a list of autouse fixtures it defines. self._nodeid_autousenames: Final[dict[str, list[str]]] = { @@ -1594,7 +1599,7 @@ def getfixtureclosure( parentnode: nodes.Node, initialnames: tuple[str, ...], ignore_args: AbstractSet[str], - ) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any]]]]: + ) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any, Any]]]]: # Collect the closure of all fixtures, starting with the given # fixturenames as the initial set. As we have to visit all # factory definitions anyway, we also return an arg2fixturedefs @@ -1604,7 +1609,7 @@ def getfixtureclosure( fixturenames_closure = list(initialnames) - arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] = {} + arg2fixturedefs: dict[str, Sequence[FixtureDef[Any, Any]]] = {} lastlen = -1 while lastlen != len(fixturenames_closure): lastlen = len(fixturenames_closure) @@ -1684,7 +1689,7 @@ def _register_fixture( self, *, name: str, - func: _FixtureFunc[object], + func: _FixtureFunc[Any, object], nodeid: str | None, scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] = "function", params: Sequence[object] | None = None, @@ -1819,7 +1824,7 @@ def parsefactories( def getfixturedefs( self, argname: str, node: nodes.Node - ) -> Sequence[FixtureDef[Any]] | None: + ) -> Sequence[FixtureDef[Any, Any]] | None: """Get FixtureDefs for a fixture name which are applicable to a given node. @@ -1838,8 +1843,8 @@ def getfixturedefs( return tuple(self._matchfactories(fixturedefs, node)) def _matchfactories( - self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node - ) -> Iterator[FixtureDef[Any]]: + self, fixturedefs: Iterable[FixtureDef[Any, Any]], node: nodes.Node + ) -> Iterator[FixtureDef[Any, Any]]: parentnodeids = {n.nodeid for n in node.iter_parents()} for fixturedef in fixturedefs: if fixturedef.baseid in parentnodeids: @@ -1876,7 +1881,7 @@ def get_best_relpath(func) -> str: loc = getlocation(func, invocation_dir) return bestrelpath(invocation_dir, Path(loc)) - def write_fixture(fixture_def: FixtureDef[object]) -> None: + def write_fixture(fixture_def: FixtureDef[Any, object]) -> None: argname = fixture_def.argname if verbose <= 0 and argname.startswith("_"): return diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 0a41b0aca47..48f964d2a7a 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -866,7 +866,7 @@ def pytest_report_from_serializable( @hookspec(firstresult=True) def pytest_fixture_setup( - fixturedef: FixtureDef[Any], request: SubRequest + fixturedef: FixtureDef[Any, Any], request: SubRequest ) -> object | None: """Perform fixture setup execution. @@ -894,7 +894,7 @@ def pytest_fixture_setup( def pytest_fixture_post_finalizer( - fixturedef: FixtureDef[Any], request: SubRequest + fixturedef: FixtureDef[Any, Any], request: SubRequest ) -> None: """Called after fixture teardown, but before the cache is cleared, so the fixture result ``fixturedef.cached_result`` is still available (not diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 1456b5212d4..37c3b51774b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1078,7 +1078,7 @@ def get_direct_param_fixture_func(request: FixtureRequest) -> Any: # Used for storing pseudo fixturedefs for direct parametrization. -name2pseudofixturedef_key = StashKey[Dict[str, FixtureDef[Any]]]() +name2pseudofixturedef_key = StashKey[Dict[str, FixtureDef[Any, Any]]]() @final @@ -1264,7 +1264,7 @@ def parametrize( if node is None: name2pseudofixturedef = None else: - default: dict[str, FixtureDef[Any]] = {} + default: dict[str, FixtureDef[Any, Any]] = {} name2pseudofixturedef = node.stash.setdefault( name2pseudofixturedef_key, default ) @@ -1451,7 +1451,7 @@ def _recompute_direct_params_indices(self) -> None: def _find_parametrized_scope( argnames: Sequence[str], - arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]], + arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[Any, object]]], indirect: bool | Sequence[str], ) -> Scope: """Find the most appropriate scope for a parametrized call based on its arguments. diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index de297f408d3..bc934b14f33 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -1,5 +1,6 @@ from __future__ import annotations +from typing import Any from typing import Generator from _pytest._io.saferepr import saferepr @@ -30,7 +31,7 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl(wrapper=True) def pytest_fixture_setup( - fixturedef: FixtureDef[object], request: SubRequest + fixturedef: FixtureDef[Any, object], request: SubRequest ) -> Generator[None, object, object]: try: return (yield) @@ -51,7 +52,7 @@ def pytest_fixture_setup( def pytest_fixture_post_finalizer( - fixturedef: FixtureDef[object], request: SubRequest + fixturedef: FixtureDef[Any, object], request: SubRequest ) -> None: if fixturedef.cached_result is not None: config = request.config @@ -62,7 +63,7 @@ def pytest_fixture_post_finalizer( def _show_fixture_action( - fixturedef: FixtureDef[object], config: Config, msg: str + fixturedef: FixtureDef[Any, object], config: Config, msg: str ) -> None: capman = config.pluginmanager.getplugin("capturemanager") if capman: diff --git a/src/_pytest/setupplan.py b/src/_pytest/setupplan.py index 4e124cce243..0a6f2f668e5 100644 --- a/src/_pytest/setupplan.py +++ b/src/_pytest/setupplan.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config.argparsing import Parser @@ -21,7 +23,7 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl(tryfirst=True) def pytest_fixture_setup( - fixturedef: FixtureDef[object], request: SubRequest + fixturedef: FixtureDef[Any, object], request: SubRequest ) -> object | None: # Will return a dummy fixture if the setuponly option is provided. if request.config.option.setupplan: diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 0a4ebf2c9af..cb51b4ca7c0 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -33,7 +33,7 @@ def Metafunc(self, func, config=None) -> python.Metafunc: # on the funcarg level, so we don't need a full blown # initialization. class FuncFixtureInfoMock: - name2fixturedefs: dict[str, list[fixtures.FixtureDef[object]]] = {} + name2fixturedefs: dict[str, list[fixtures.FixtureDef[Any, object]]] = {} def __init__(self, names): self.names_closure = names @@ -154,7 +154,7 @@ class DummyFixtureDef: _scope: Scope fixtures_defs = cast( - Dict[str, Sequence[fixtures.FixtureDef[object]]], + Dict[str, Sequence[fixtures.FixtureDef[Any, object]]], dict( session_fix=[DummyFixtureDef(Scope.Session)], package_fix=[DummyFixtureDef(Scope.Package)], From 47c82faaa9fa23e8deda65d62f45761384fbf0bd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 26 Nov 2024 11:21:56 -0300 Subject: [PATCH 37/38] Fix import --- src/_pytest/fixtures.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 03a69f5bc2f..d802647b1f2 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -35,8 +35,6 @@ from typing import Union import warnings -from typing_extensions import ParamSpec - import _pytest from _pytest import nodes from _pytest._code import getfslineno @@ -78,6 +76,11 @@ if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup +if sys.version_info < (3, 10): + from typing_extensions import ParamSpec +else: + from typing import ParamSpec + if TYPE_CHECKING: from _pytest.python import CallSpec2 From 8aa9998ef8c126bcd18e4839743a98aa3a329acb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 26 Nov 2024 11:25:34 -0300 Subject: [PATCH 38/38] Remove old FixtureFunction alias --- src/_pytest/fixtures.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d802647b1f2..9096ac3f7c3 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -94,8 +94,6 @@ # The parameters that a fixture function receives. FixtureParams = ParamSpec("FixtureParams") -# The type of the fixture function (type variable). -# FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object]) # The type of a fixture function (type alias generic in fixture params and value). _FixtureFunc = Union[ Callable[FixtureParams, FixtureValue],