-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
(fixtures): Replace fixture represenation with a class #12473
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
41c7c2c
cee9044
004f29f
224595a
8666dfb
b724030
4552e06
1efc230
b6f87a3
bc69c19
da5e8e9
ec7f7b1
f61c23c
144befb
e556218
a48b2a0
c7d8339
6ba2370
3bc5ad1
04a0cde
9822c94
786aec5
afe72f6
3b66f72
149cc30
eb3943c
2891be5
1734722
d749473
fa2f2b5
1b5b4c7
6ee850a
131e306
f062ff6
7f1b8d3
fb4ce59
3f22fe5
98ea2f5
72269d8
d625874
2f1a17e
d909768
56899f6
ad3f0fa
845d50e
a6d0998
0bb4f14
68a66b5
a160a6a
b1de16a
3ba7108
475deb1
723a8f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Fixtures are now clearly represented in the output as a "fixture object", not as a normal function as before, making it easy for beginners to catch mistakes such as referencing a fixture declared in the same module but not requested in the test function. | ||
|
||
-- by :user:`the-compiler` and :user:`glyphack` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,10 +40,8 @@ | |
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 | ||
|
@@ -152,13 +150,12 @@ | |
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 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): | ||
Glyphack marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return obj._fixture_function_marker | ||
return None | ||
|
||
|
||
# Algorithm for sorting on a per-parametrized resource setup basis. | ||
|
@@ -1181,31 +1178,6 @@ | |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The previous code used There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes unfortunately the new result is this object instead of a wrapped function. I was not aware of this thanks for noticing it. Since the methods that are copied are not a lot shall I just assign them just like we do with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the best thing would be to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did this the only problem was that when using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was actually thinking that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please see this commit that fixes the issue: 475deb1 I agree with doing this. Initially I was lazily binding methods, which led me to use |
||
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: | ||
|
@@ -1220,19 +1192,21 @@ | |
def __post_init__(self, _ispytest: bool) -> None: | ||
check_ispytest(_ispytest) | ||
|
||
def __call__(self, function: FixtureFunction) -> FixtureFunction: | ||
def __call__(self, function: FixtureFunction) -> FixtureFunctionDefinition: | ||
nicoddemus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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}" | ||
) | ||
|
||
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=function, fixture_function_marker=self, _ispytest=True | ||
) | ||
|
||
name = self.name or function.__name__ | ||
if name == "request": | ||
|
@@ -1242,21 +1216,68 @@ | |
pytrace=False, | ||
) | ||
|
||
# Type ignored because https://github.com/python/mypy/issues/2087. | ||
function._pytestfixturefunction = self # type: ignore[attr-defined] | ||
return function | ||
return fixture_definition | ||
|
||
|
||
# TODO: paramspec/return type annotation tracking and storing | ||
class FixtureFunctionDefinition: | ||
nicoddemus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def __init__( | ||
bluetech marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self, | ||
Glyphack marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*, | ||
function: Callable[..., Any], | ||
fixture_function_marker: FixtureFunctionMarker, | ||
instance: object | None = None, | ||
_ispytest: bool = False, | ||
) -> None: | ||
check_ispytest(_ispytest) | ||
self.name = fixture_function_marker.name or function.__name__ | ||
# In order to show the function that this fixture contains in messages. | ||
# Set the __name__ to be same as the function __name__ or the given fixture name. | ||
self.__name__ = self.name | ||
bluetech marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self._fixture_function_marker = fixture_function_marker | ||
if instance is not None: | ||
self._fixture_function = cast( | ||
Callable[..., Any], function.__get__(instance) | ||
) | ||
else: | ||
self._fixture_function = function | ||
functools.update_wrapper(self, function) | ||
|
||
def __repr__(self) -> str: | ||
return f"<pytest_fixture({self._fixture_function})>" | ||
|
||
def __get__(self, instance, owner=None): | ||
bluetech marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Behave like a method if the function it was applied to was a method.""" | ||
return FixtureFunctionDefinition( | ||
function=self._fixture_function, | ||
fixture_function_marker=self._fixture_function_marker, | ||
instance=instance, | ||
_ispytest=True, | ||
) | ||
|
||
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]: | ||
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: ... | ||
) -> FixtureFunctionDefinition: ... | ||
|
||
|
||
@overload | ||
|
@@ -1279,7 +1300,7 @@ | |
autouse: bool = False, | ||
ids: Sequence[object | None] | Callable[[Any], object | None] | None = None, | ||
name: str | None = None, | ||
) -> FixtureFunctionMarker | FixtureFunction: | ||
) -> FixtureFunctionMarker | FixtureFunctionDefinition: | ||
"""Decorator to mark a fixture factory function. | ||
|
||
This decorator can be used, with or without parameters, to define a | ||
|
@@ -1774,33 +1795,31 @@ | |
# 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 | ||
|
||
# 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) | ||
|
||
self._register_fixture( | ||
name=name, | ||
nodeid=nodeid, | ||
func=func, | ||
scope=marker.scope, | ||
params=marker.params, | ||
ids=marker.ids, | ||
autouse=marker.autouse, | ||
) | ||
if type(obj_ub) is FixtureFunctionDefinition: | ||
marker = obj_ub._fixture_function_marker | ||
if 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: | ||
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 = obj._get_wrapped_function() | ||
|
||
self._register_fixture( | ||
name=fixture_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 | ||
|
Uh oh!
There was an error while loading. Please reload this page.