This commit is contained in:
Shaygan Hooshyari 2024-06-20 15:49:24 +00:00 committed by GitHub
commit 42f7713bff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 155 additions and 120 deletions

View File

@ -376,6 +376,7 @@ Serhii Mozghovyi
Seth Junot Seth Junot
Shantanu Jain Shantanu Jain
Sharad Nair Sharad Nair
Shaygan Hooshyari
Shubham Adep Shubham Adep
Simon Blanchard Simon Blanchard
Simon Gomizelj Simon Gomizelj

View File

@ -0,0 +1,3 @@
The fixtures are now represented as fixture in test output.
-- by :user:`the-compiler` and :user:`glyphack`.

View File

@ -190,6 +190,7 @@ nitpick_ignore = [
("py:class", "TerminalReporter"), ("py:class", "TerminalReporter"),
("py:class", "_pytest._code.code.TerminalRepr"), ("py:class", "_pytest._code.code.TerminalRepr"),
("py:class", "_pytest.fixtures.FixtureFunctionMarker"), ("py:class", "_pytest.fixtures.FixtureFunctionMarker"),
("py:class", "_pytest.fixtures.FixtureFunctionDefinition"),
("py:class", "_pytest.logging.LogCaptureHandler"), ("py:class", "_pytest.logging.LogCaptureHandler"),
("py:class", "_pytest.mark.structures.ParameterSet"), ("py:class", "_pytest.mark.structures.ParameterSet"),
# Intentionally undocumented/private # Intentionally undocumented/private

View File

@ -35,6 +35,7 @@ from _pytest._io.saferepr import saferepr
from _pytest._version import version from _pytest._version import version
from _pytest.assertion import util from _pytest.assertion import util
from _pytest.config import Config from _pytest.config import Config
from _pytest.fixtures import FixtureFunctionDefinition
from _pytest.main import Session from _pytest.main import Session
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import fnmatch_ex
@ -462,7 +463,7 @@ def _format_assertmsg(obj: object) -> str:
def _should_repr_global_name(obj: object) -> bool: def _should_repr_global_name(obj: object) -> bool:
if callable(obj): if callable(obj):
return False return isinstance(obj, FixtureFunctionDefinition)
try: try:
return not hasattr(obj, "__name__") return not hasattr(obj, "__name__")

View File

@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
import dataclasses
import enum import enum
import functools import functools
import inspect import inspect
@ -210,29 +209,15 @@ def ascii_escaped(val: bytes | str) -> str:
return ret.translate(_non_printable_ascii_translate_table) return ret.translate(_non_printable_ascii_translate_table)
@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): def get_real_func(obj):
"""Get the real function object of the (possibly) wrapped object by """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 start_obj = obj
for i in range(100): for _ in range(100):
# __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function if isinstance(obj, FixtureFunctionDefinition):
# to trigger a warning if it gets called directly instead of by pytest: we don't obj = obj._get_wrapped_function()
# 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
break break
new_obj = getattr(obj, "__wrapped__", None) new_obj = getattr(obj, "__wrapped__", None)
if new_obj is None: if new_obj is None:
@ -249,20 +234,6 @@ def get_real_func(obj):
return 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): def getimfunc(func):
try: try:
return func.__func__ return func.__func__

View File

@ -42,10 +42,8 @@ from _pytest._code import Source
from _pytest._code.code import FormattedExcinfo from _pytest._code.code import FormattedExcinfo
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import _PytestWrapper
from _pytest.compat import assert_never from _pytest.compat import assert_never
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.compat import get_real_method
from _pytest.compat import getfuncargnames from _pytest.compat import getfuncargnames
from _pytest.compat import getimfunc from _pytest.compat import getimfunc
from _pytest.compat import getlocation from _pytest.compat import getlocation
@ -153,12 +151,10 @@ def get_scope_node(node: nodes.Node, scope: Scope) -> Optional[nodes.Node]:
def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
"""Return fixturemarker or None if it doesn't exist or raised """Return fixturemarker or None if it doesn't exist"""
exceptions.""" if isinstance(obj, FixtureFunctionDefinition):
return cast( return obj._fixture_function_marker
Optional[FixtureFunctionMarker], return None
safe_getattr(obj, "_pytestfixturefunction", None),
)
# Algorithm for sorting on a per-parametrized resource setup basis. # Algorithm for sorting on a per-parametrized resource setup basis.
@ -1145,31 +1141,6 @@ def pytest_fixture_setup(
return result 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 @final
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class FixtureFunctionMarker: class FixtureFunctionMarker:
@ -1186,11 +1157,11 @@ class FixtureFunctionMarker:
def __post_init__(self, _ispytest: bool) -> None: def __post_init__(self, _ispytest: bool) -> None:
check_ispytest(_ispytest) check_ispytest(_ispytest)
def __call__(self, function: FixtureFunction) -> FixtureFunction: def __call__(self, function: FixtureFunction) -> "FixtureFunctionDefinition":
if inspect.isclass(function): if inspect.isclass(function):
raise ValueError("class fixtures not supported (maybe in the future)") raise ValueError("class fixtures not supported (maybe in the future)")
if getattr(function, "_pytestfixturefunction", False): if isinstance(function, FixtureFunctionDefinition):
raise ValueError( raise ValueError(
f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}" f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}"
) )
@ -1198,7 +1169,7 @@ class FixtureFunctionMarker:
if hasattr(function, "pytestmark"): if hasattr(function, "pytestmark"):
warnings.warn(MARKED_FIXTURE, stacklevel=2) warnings.warn(MARKED_FIXTURE, stacklevel=2)
function = wrap_function_to_error_out_if_called_directly(function, self) fixture_definition = FixtureFunctionDefinition(function, self)
name = self.name or function.__name__ name = self.name or function.__name__
if name == "request": if name == "request":
@ -1208,14 +1179,50 @@ class FixtureFunctionMarker:
pytrace=False, pytrace=False,
) )
# Type ignored because https://github.com/python/mypy/issues/2087. return fixture_definition
function._pytestfixturefunction = self # type: ignore[attr-defined]
return function
class FixtureFunctionDefinition:
def __init__(
self,
function: Callable[..., Any],
fixture_function_marker: FixtureFunctionMarker,
instance: Optional[type] = None,
):
self.name = fixture_function_marker.name or function.__name__
self.__name__ = self.name
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})"
def __get__(self, instance, owner=None):
return FixtureFunctionDefinition(
self._fixture_function, self._fixture_function_marker, instance
)
def __call__(self, *args: Any, **kwds: Any) -> Any:
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_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
@overload @overload
def fixture( def fixture(
fixture_function: FixtureFunction, fixture_function: Callable[..., object],
*, *,
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ..., scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
params: Optional[Iterable[object]] = ..., params: Optional[Iterable[object]] = ...,
@ -1224,7 +1231,7 @@ def fixture(
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = ..., ] = ...,
name: Optional[str] = ..., name: Optional[str] = ...,
) -> FixtureFunction: ... ) -> FixtureFunctionDefinition: ...
@overload @overload
@ -1238,7 +1245,7 @@ def fixture(
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = ..., ] = ...,
name: Optional[str] = None, name: Optional[str] = None,
) -> FixtureFunctionMarker: ... ) -> FixtureFunctionDefinition: ...
def fixture( def fixture(
@ -1251,7 +1258,7 @@ def fixture(
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = None, ] = None,
name: Optional[str] = None, name: Optional[str] = None,
) -> Union[FixtureFunctionMarker, FixtureFunction]: ) -> Union[FixtureFunctionMarker, FixtureFunctionDefinition]:
"""Decorator to mark a fixture factory function. """Decorator to mark a fixture factory function.
This decorator can be used, with or without parameters, to define a This decorator can be used, with or without parameters, to define a
@ -1321,7 +1328,7 @@ def fixture(
def yield_fixture( def yield_fixture(
fixture_function=None, fixture_function=None,
*args, *args,
scope="function", scope: _ScopeName = "function",
params=None, params=None,
autouse=False, autouse=False,
ids=None, ids=None,
@ -1734,24 +1741,12 @@ class FixtureManager:
self._holderobjseen.add(holderobj) self._holderobjseen.add(holderobj)
for name in dir(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.
obj = safe_getattr(holderobj, name, None) obj = safe_getattr(holderobj, name, None)
marker = getfixturemarker(obj) if type(obj) is FixtureFunctionDefinition:
if not isinstance(marker, FixtureFunctionMarker): marker = obj._fixture_function_marker
# Magic globals with __getattr__ might have got us a wrong
# fixture attribute.
continue
if marker.name: if marker.name:
name = marker.name name = marker.name
func = obj._get_wrapped_function()
# 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)
self._register_fixture( self._register_fixture(
name=name, name=name,
nodeid=nodeid, nodeid=nodeid,

View File

@ -478,12 +478,13 @@ def test_source_with_decorator() -> None:
def deco_fixture(): def deco_fixture():
assert False 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_wrapped_function())
assert src == " @pytest.fixture\n def deco_fixture():\n assert False\n" assert src == " @pytest.fixture\n def deco_fixture():\n assert False\n"
# currently Source does not unwrap decorators, testing the # Make sure the decorator is not a wrapped function
# existing behavior here for explicitness, but perhaps we should revisit/change this assert not str(Source(deco_fixture)).startswith("@functools.wraps(function)")
# in the future
assert str(Source(deco_fixture)).startswith("@functools.wraps(function)")
assert ( assert (
textwrap.indent(str(Source(get_real_func(deco_fixture))), " ") + "\n" == src textwrap.indent(str(Source(get_real_func(deco_fixture))), " ") + "\n" == src
) )

View File

@ -1,4 +1,5 @@
# mypy: allow-untyped-defs # mypy: allow-untyped-defs
import inspect
import os import os
from pathlib import Path from pathlib import Path
import sys import sys
@ -3295,6 +3296,33 @@ class TestFixtureMarker:
assert output1 == output2 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: class TestRequestScopeAccess:
pytestmark = pytest.mark.parametrize( pytestmark = pytest.mark.parametrize(
("scope", "ok", "error"), ("scope", "ok", "error"),
@ -4465,6 +4493,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: def test_fixture_param_shadowing(pytester: Pytester) -> None:
"""Parametrized arguments would be shadowed if a fixture with the same name also exists (#5036)""" """Parametrized arguments would be shadowed if a fixture with the same name also exists (#5036)"""
pytester.makepyfile( pytester.makepyfile(

View File

@ -744,6 +744,23 @@ class TestAssertionRewrite:
assert "UnicodeDecodeError" not in msg assert "UnicodeDecodeError" not in msg
assert "UnicodeEncodeError" 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(<function fixt at *>) == 42*"]
)
class TestRewriteOnImport: class TestRewriteOnImport:
def test_pycache_is_a_file(self, pytester: Pytester) -> None: def test_pycache_is_a_file(self, pytester: Pytester) -> None:

View File

@ -1310,7 +1310,7 @@ def test_collect_handles_raising_on_dunder_class(pytester: Pytester) -> None:
""" """
) )
result = pytester.runpytest() result = pytester.runpytest()
result.stdout.fnmatch_lines(["*1 passed in*"]) result.assert_outcomes(passed=1)
assert result.ret == 0 assert result.ret == 0
@ -1374,7 +1374,7 @@ def test_collect_pyargs_with_testpaths(
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.chdir(root) mp.chdir(root)
result = pytester.runpytest_subprocess() 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: def test_initial_conftests_with_testpaths(pytester: Pytester) -> None:

View File

@ -7,7 +7,6 @@ import sys
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
from _pytest.compat import _PytestWrapper
from _pytest.compat import assert_never from _pytest.compat import assert_never
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.compat import is_generator from _pytest.compat import is_generator
@ -52,8 +51,8 @@ def test_real_func_loop_limit() -> None:
with pytest.raises( with pytest.raises(
ValueError, ValueError,
match=( match=(
"could not find real function of <Evil left=800>\n" "could not find real function of <Evil left=900>\n"
"stopped at <Evil left=800>" "stopped at <Evil left=900>"
), ),
): ):
get_real_func(evil) get_real_func(evil)
@ -78,10 +77,13 @@ def test_get_real_func() -> None:
wrapped_func2 = decorator(decorator(wrapped_func)) wrapped_func2 = decorator(decorator(wrapped_func))
assert get_real_func(wrapped_func2) is func assert get_real_func(wrapped_func2) is func
# special case for __pytest_wrapped__ attribute: used to obtain the function up until the point # obtain the function up until the point a function was wrapped by pytest itself
# a function was wrapped by pytest itself @pytest.fixture
wrapped_func2.__pytest_wrapped__ = _PytestWrapper(wrapped_func) def wrapped_func3():
assert get_real_func(wrapped_func2) is wrapped_func pass # pragma: no cover
wrapped_func4 = decorator(wrapped_func3)
assert get_real_func(wrapped_func4) is wrapped_func3._get_wrapped_function()
def test_get_real_func_partial() -> None: def test_get_real_func_partial() -> None: