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
Shantanu Jain
Sharad Nair
Shaygan Hooshyari
Shubham Adep
Simon Blanchard
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", "_pytest._code.code.TerminalRepr"),
("py:class", "_pytest.fixtures.FixtureFunctionMarker"),
("py:class", "_pytest.fixtures.FixtureFunctionDefinition"),
("py:class", "_pytest.logging.LogCaptureHandler"),
("py:class", "_pytest.mark.structures.ParameterSet"),
# Intentionally undocumented/private

View File

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

View File

@ -3,7 +3,6 @@
from __future__ import annotations
import dataclasses
import enum
import functools
import inspect
@ -210,29 +209,15 @@ def ascii_escaped(val: bytes | str) -> str:
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):
"""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_wrapped_function()
break
new_obj = getattr(obj, "__wrapped__", None)
if new_obj is None:
@ -249,20 +234,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__

View File

@ -42,10 +42,8 @@ from _pytest._code import Source
from _pytest._code.code import FormattedExcinfo
from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
from _pytest.compat import _PytestWrapper
from _pytest.compat import assert_never
from _pytest.compat import get_real_func
from _pytest.compat import get_real_method
from _pytest.compat import getfuncargnames
from _pytest.compat import getimfunc
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"]:
"""Return fixturemarker or None if it doesn't exist or raised
exceptions."""
return cast(
Optional[FixtureFunctionMarker],
safe_getattr(obj, "_pytestfixturefunction", None),
)
"""Return fixturemarker or None if it doesn't exist"""
if isinstance(obj, FixtureFunctionDefinition):
return obj._fixture_function_marker
return None
# Algorithm for sorting on a per-parametrized resource setup basis.
@ -1145,31 +1141,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:
@ -1186,11 +1157,11 @@ 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)")
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}"
)
@ -1198,7 +1169,7 @@ class FixtureFunctionMarker:
if hasattr(function, "pytestmark"):
warnings.warn(MARKED_FIXTURE, stacklevel=2)
function = wrap_function_to_error_out_if_called_directly(function, self)
fixture_definition = FixtureFunctionDefinition(function, self)
name = self.name or function.__name__
if name == "request":
@ -1208,14 +1179,50 @@ class FixtureFunctionMarker:
pytrace=False,
)
# Type ignored because https://github.com/python/mypy/issues/2087.
function._pytestfixturefunction = self # type: ignore[attr-defined]
return function
return fixture_definition
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
def fixture(
fixture_function: FixtureFunction,
fixture_function: Callable[..., object],
*,
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
params: Optional[Iterable[object]] = ...,
@ -1224,7 +1231,7 @@ def fixture(
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = ...,
name: Optional[str] = ...,
) -> FixtureFunction: ...
) -> FixtureFunctionDefinition: ...
@overload
@ -1238,7 +1245,7 @@ def fixture(
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = ...,
name: Optional[str] = None,
) -> FixtureFunctionMarker: ...
) -> FixtureFunctionDefinition: ...
def fixture(
@ -1251,7 +1258,7 @@ def fixture(
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = None,
name: Optional[str] = None,
) -> Union[FixtureFunctionMarker, FixtureFunction]:
) -> Union[FixtureFunctionMarker, FixtureFunctionDefinition]:
"""Decorator to mark a fixture factory function.
This decorator can be used, with or without parameters, to define a
@ -1321,7 +1328,7 @@ def fixture(
def yield_fixture(
fixture_function=None,
*args,
scope="function",
scope: _ScopeName = "function",
params=None,
autouse=False,
ids=None,
@ -1734,33 +1741,21 @@ class FixtureManager:
self._holderobjseen.add(holderobj)
for name in dir(holderobj):
# The attribute can be an arbitrary descriptor, so the attribute
# access below can raise. safe_getatt() ignores such exceptions.
obj = safe_getattr(holderobj, name, None)
marker = getfixturemarker(obj)
if not isinstance(marker, FixtureFunctionMarker):
# Magic globals with __getattr__ might have got us a wrong
# fixture attribute.
continue
if 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)
self._register_fixture(
name=name,
nodeid=nodeid,
func=func,
scope=marker.scope,
params=marker.params,
ids=marker.ids,
autouse=marker.autouse,
)
if type(obj) is FixtureFunctionDefinition:
marker = obj._fixture_function_marker
if marker.name:
name = marker.name
func = obj._get_wrapped_function()
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

View File

@ -478,12 +478,13 @@ def test_source_with_decorator() -> None:
def deco_fixture():
assert False
src = inspect.getsource(deco_fixture)
# Since deco_fixture is now an instance of FixtureFunctionDef the getsource function will not work on it.
with pytest.raises(Exception):
inspect.getsource(deco_fixture)
src = inspect.getsource(deco_fixture._get_wrapped_function())
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
)

View File

@ -1,4 +1,5 @@
# mypy: allow-untyped-defs
import inspect
import os
from pathlib import Path
import sys
@ -3295,6 +3296,33 @@ class TestFixtureMarker:
assert output1 == output2
class FixtureFunctionDefTestClass:
def __init__(self) -> None:
self.i = 10
@pytest.fixture
def fixture_function_def_test_method(self):
return self.i
@pytest.fixture
def fixture_function_def_test_func():
return 9
def test_get_wrapped_func_returns_method():
obj = FixtureFunctionDefTestClass()
wrapped_function_result = (
obj.fixture_function_def_test_method._get_wrapped_function()
)
assert inspect.ismethod(wrapped_function_result)
assert wrapped_function_result() == 10
def test_get_wrapped_func_returns_function():
assert fixture_function_def_test_func._get_wrapped_function()() == 9
class TestRequestScopeAccess:
pytestmark = pytest.mark.parametrize(
("scope", "ok", "error"),
@ -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:
"""Parametrized arguments would be shadowed if a fixture with the same name also exists (#5036)"""
pytester.makepyfile(

View File

@ -744,6 +744,23 @@ class TestAssertionRewrite:
assert "UnicodeDecodeError" not in msg
assert "UnicodeEncodeError" not in msg
def test_assert_fixture(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""\
import pytest
@pytest.fixture
def fixt():
return 42
def test_something(): # missing "fixt" argument
assert fixt == 42
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(
["*assert pytest_fixture(<function fixt at *>) == 42*"]
)
class TestRewriteOnImport:
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.stdout.fnmatch_lines(["*1 passed in*"])
result.assert_outcomes(passed=1)
assert result.ret == 0
@ -1374,7 +1374,7 @@ def test_collect_pyargs_with_testpaths(
with monkeypatch.context() as mp:
mp.chdir(root)
result = pytester.runpytest_subprocess()
result.stdout.fnmatch_lines(["*1 passed in*"])
result.assert_outcomes(passed=1)
def test_initial_conftests_with_testpaths(pytester: Pytester) -> None:

View File

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