fix: update code to use new fixture definitions

This commit is contained in:
Glyphack 2024-06-20 13:46:25 +02:00
parent 46d2ccbfd4
commit 31c8fbed1e
5 changed files with 50 additions and 81 deletions

View File

@ -210,6 +210,7 @@ def ascii_escaped(val: bytes | str) -> str:
return ret.translate(_non_printable_ascii_translate_table)
# TODO: remove and replace with FixtureFunctionDefinition
@dataclasses.dataclass
class _PytestWrapper:
"""Dummy wrapper around a function object for internal use only.
@ -249,20 +250,6 @@ def get_real_func(obj):
return obj
def get_real_method(obj, holder):
"""Attempt to obtain the real function object that might be wrapping
``obj``, while at the same time returning a bound method to ``holder`` if
the original object was a bound method."""
try:
is_method = hasattr(obj, "__func__")
obj = get_real_func(obj)
except Exception: # pragma: no cover
return obj
if is_method and hasattr(obj, "__get__") and callable(obj.__get__):
obj = obj.__get__(holder)
return obj
def getimfunc(func):
try:
return func.__func__

View File

@ -45,8 +45,6 @@ from _pytest._io import TerminalWriter
from _pytest.compat import _PytestWrapper
from _pytest.compat import assert_never
from _pytest.compat import get_real_func
# from _pytest.compat import get_real_method
from _pytest.compat import getfuncargnames
from _pytest.compat import getimfunc
from _pytest.compat import getlocation
@ -1146,31 +1144,6 @@ def pytest_fixture_setup(
return result
def wrap_function_to_error_out_if_called_directly(
function: FixtureFunction,
fixture_marker: "FixtureFunctionMarker",
) -> FixtureFunction:
"""Wrap the given fixture function so we can raise an error about it being called directly,
instead of used as an argument in a test function."""
name = fixture_marker.name or function.__name__
message = (
f'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
"but are created automatically when test functions request them as parameters.\n"
"See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n"
"https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code."
)
@functools.wraps(function)
def result(*args, **kwargs):
fail(message, pytrace=False)
# Keep reference to the original function in our own custom attribute so we don't unwrap
# further than this point and lose useful wrappings like @mock.patch (#3774).
result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined]
return cast(FixtureFunction, result)
@final
@dataclasses.dataclass(frozen=True)
class FixtureFunctionMarker:
@ -1191,7 +1164,7 @@ class FixtureFunctionMarker:
if inspect.isclass(function):
raise ValueError("class fixtures not supported (maybe in the future)")
if getattr(function, "_pytestfixturefunction", False):
if isinstance(function, FixtureFunctionDefinition):
raise ValueError(
f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}"
)
@ -1201,8 +1174,6 @@ class FixtureFunctionMarker:
fixture_definition = FixtureFunctionDefinition(function, self)
# function = wrap_function_to_error_out_if_called_directly(function, self)
name = self.name or function.__name__
if name == "request":
location = getlocation(function)
@ -1211,16 +1182,16 @@ class FixtureFunctionMarker:
pytrace=False,
)
# Type ignored because https://github.com/python/mypy/issues/2087.
# function._pytestfixturefunction = self # type: ignore[attr-defined]
# return function
return fixture_definition
def __repr__(self):
return "fixture"
# TODO: write docstring
class FixtureFunctionDefinition:
"""Since deco_fixture is now an instance of FixtureFunctionDef the getsource function will not work on it."""
def __init__(
self,
function: Callable[..., object],
@ -1228,19 +1199,15 @@ class FixtureFunctionDefinition:
instance: Optional[type] = None,
):
self.name = fixture_function_marker.name or function.__name__
self.__name__ = self.name
self._pytestfixturefunction = fixture_function_marker
self.__pytest_wrapped__ = _PytestWrapper(function)
self.fixture_function = function
self.fixture_function_marker = fixture_function_marker
self.scope = fixture_function_marker.scope
self.params = fixture_function_marker.params
self.autouse = fixture_function_marker.autouse
self.ids = fixture_function_marker.ids
self.fixture_function = function
self.instance = instance
def __repr__(self) -> str:
return f"fixture {self.fixture_function}"
return f"pytest_fixture({self.fixture_function})"
def __get__(self, instance, owner=None):
return FixtureFunctionDefinition(
@ -1248,7 +1215,13 @@ class FixtureFunctionDefinition:
)
def __call__(self, *args: Any, **kwds: Any) -> Any:
return self.get_real_func(*args, **kwds)
message = (
f'Fixture "{self.name}" called directly. Fixtures are not meant to be called directly,\n'
"but are created automatically when test functions request them as parameters.\n"
"See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n"
"https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly"
)
fail(message, pytrace=False)
def get_real_func(self):
if self.instance is not None:
@ -1792,26 +1765,19 @@ class FixtureManager:
# Magic globals with __getattr__ might have got us a wrong
# fixture attribute.
continue
if marker.name:
name = marker.name
# During fixture definition we wrap the original fixture function
# to issue a warning if called directly, so here we unwrap it in
# order to not emit the warning when pytest itself calls the
# fixture function.
# func = get_real_method(obj, holderobj)
func = obj.get_real_func()
self._register_fixture(
name=name,
nodeid=nodeid,
func=func,
scope=marker.scope,
params=marker.params,
ids=marker.ids,
autouse=marker.autouse,
)
if isinstance(obj, FixtureFunctionDefinition):
if marker.name:
name = marker.name
func = obj.get_real_func()
self._register_fixture(
name=name,
nodeid=nodeid,
func=func,
scope=marker.scope,
params=marker.params,
ids=marker.ids,
autouse=marker.autouse,
)
def getfixturedefs(
self, argname: str, node: nodes.Node

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_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
)

View File

@ -4465,6 +4465,21 @@ def test_fixture_double_decorator(pytester: Pytester) -> None:
)
def test_fixture_class(pytester: Pytester) -> None:
"""Check if an error is raised when using @pytest.fixture on a class."""
pytester.makepyfile(
"""
import pytest
@pytest.fixture
class A:
pass
"""
)
result = pytester.runpytest()
result.assert_outcomes(errors=1)
def test_fixture_param_shadowing(pytester: Pytester) -> None:
"""Parametrized arguments would be shadowed if a fixture with the same name also exists (#5036)"""
pytester.makepyfile(

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: