Merge 260b5a7614
into f426c0b35a
This commit is contained in:
commit
42f7713bff
1
AUTHORS
1
AUTHORS
|
@ -376,6 +376,7 @@ Serhii Mozghovyi
|
|||
Seth Junot
|
||||
Shantanu Jain
|
||||
Sharad Nair
|
||||
Shaygan Hooshyari
|
||||
Shubham Adep
|
||||
Simon Blanchard
|
||||
Simon Gomizelj
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
The fixtures are now represented as fixture in test output.
|
||||
|
||||
-- by :user:`the-compiler` and :user:`glyphack`.
|
|
@ -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
|
||||
|
|
|
@ -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__")
|
||||
|
|
|
@ -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__
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue