-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
(fixtures): Replace fixture represenation with a class #12473
base: main
Are you sure you want to change the base?
Conversation
bf90fca
to
d25a8d9
Compare
3d2ec33
to
84cc5b7
Compare
Hey @The-Compiler @RonnyPfannschmidt I rebased the PR could you please take a look? |
testing/code/test_source.py
Outdated
@@ -478,12 +478,13 @@ def deco_mark(): | |||
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): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you assert against a more specific exception? As I understand, this is not testing that something went wrong but it's not clear what exactly. Also, it's usually a good idea to use the match=
regex with this helper.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes will apply. This tries to document the behavior more than testing anything. Before introducing the FixtureFunctionDefinition it was possible to run inspect.getsource on fixtures but now it's not. This is just describing the behavior. I'm open to removing it as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed it to a more specific error. Do you think we can remove the test? This just tests that you cannot use inspect on fixture functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove this test. I removed it.
f0324a5
to
9d2a916
Compare
Hey @Glyphack, could you resolve the merge conflicts / rebase this? |
Yes will do today. |
641a3a1
to
bc0a0e5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Iove where this is going
It opens up nice follow ups
src/_pytest/compat.py
Outdated
new_obj = getattr(obj, "__pytest_wrapped__", None) | ||
if isinstance(new_obj, _PytestWrapper): | ||
obj = new_obj.obj | ||
for _ in range(100): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we can drop the loop as nesting is no longer available
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're correct. I removed the loop. We still need to keep the unwrapping behavior. Because we expect the function to also unwrap the decorated functions for example this test: https://github.com/Glyphack/pytest/blob/3d7416c2dd5dcf2d60a8b30636dcc90978c64215/testing/test_compat.py#L63
src/_pytest/compat.py
Outdated
def get_real_func(obj): | ||
"""Get the real function object of the (possibly) wrapped object by | ||
functools.wraps or functools.partial.""" | ||
:func:`functools.wraps`, or :func:`functools.partial`, or :func:`pytest.fixture`.""" | ||
from _pytest.fixtures import FixtureFunctionDefinition |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we can drop fixture definition from that , we should pass the definitions attribute
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you suggesting to pass the FixtureFunctionDefinition object to this function? If I understand correctly this function is expected to take any object and return the real object according to this test testing the behavior:
https://github.com/Glyphack/pytest/blob/3d7416c2dd5dcf2d60a8b30636dcc90978c64215/testing/test_compat.py#L63
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As fixture definitions are no longer wrapping functions into other functions
The suggestion is to no longer consider them wrappers and passing the function directly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RonnyPfannschmidt Sorry I didn't fully understand your comment.
The current unwrapping behavior here is only for two cases:
- The pytest fixture is applied on a function so I can display this case with Fixture(f)
- The pytest fixture is decorated so we have deco(Fixture(f))
The first case is handled by obj = obj._get_wrapped_function()
line and second case is handled by obj = inspect.unwrap(obj, stop=lambda x: type(x) is FixtureFunctionDefinition)
.
If we drop any of them we cannot handle one of those two cases.
But I feel I am missing your point but unwrapping to me are these two lines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decorators cannot apply to a fixture definition
So that should trigger a error
It's wrong even now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wrong about that but the behavior is still needed. For example this test fails if we don't do unwrapping in this function:
https://github.com/Glyphack/pytest/blob/a6318c944f907bc350922c3961365e9ecd8d3e81/testing/python/integration.py#L11
The reason is that getfslineno
is using this function to unwrap a decorator:
https://github.com/Glyphack/pytest/blob/a6318c944f907bc350922c3961365e9ecd8d3e81/src/_pytest/_code/code.py#L1341
I think the solution is to make a separate function for the cases when we want to perform the unwrapping? What is your suggestion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I meant is that we no longer ought to pass fixture definition to anything that unwrapped
Definition is no longer a function
Perhaps we should drop the call altogether
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks I realized the reason my code required this check to be here was that I was getting objects using the fixture name instead of their actual name and mistakenly I calling this on non fixture objects. After fixing that I could remove the get_real_method and simplify this function.
return fixture_definition | ||
|
||
|
||
class FixtureFunctionDefinition: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to log a followup task for paramspec/return type annotation tracking and storing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added. I just don't fully know what does this mean I added your comment but feel free to edit it with more context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should create an issue and reference it here, taking the opportunity of writing a rationale and what needs to be done in the issue.
@RonnyPfannschmidt can you do that, given you asked this originally and @Glyphack doesn't know exactly what you mean? I understand you mean for this to be a Generic which carries around the type information.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I gave this a try in #12999, so we don't need to create an issue after all in the end, we can continue on that PR once this one gets merged.
Thank you both for the review. I left some TODO comments I will address them in a follow up PR. |
This is an internal class users don't need to know about it.
for more information, see https://pre-commit.ci
Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]>
Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]>
Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]>
for more information, see https://pre-commit.ci
Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]>
50a001e
to
f062ff6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall great work!
I think this almost ready to merge, and we can improve on the typing later on #12999.
@@ -0,0 +1,3 @@ | |||
The fixtures are now represented as fixture in test output. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sentence by itself, without more context, is confusing.
Perhaps:
The fixtures are now represented as fixture in test output. | |
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Might be too verbose still).
return fixture_definition | ||
|
||
|
||
class FixtureFunctionDefinition: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should create an issue and reference it here, taking the opportunity of writing a rationale and what needs to be done in the issue.
@RonnyPfannschmidt can you do that, given you asked this originally and @Glyphack doesn't know exactly what you mean? I understand you mean for this to be a Generic which carries around the type information.
function: Callable[..., Any], | ||
fixture_function_marker: FixtureFunctionMarker, | ||
instance: type | None = None, | ||
): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
): | |
) -> None: |
self, | ||
function: Callable[..., Any], | ||
fixture_function_marker: FixtureFunctionMarker, | ||
instance: type | None = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instance
seems like it should be an object
, not a type?
instance: type | None = None, | |
instance: object | None = None, |
@@ -1271,7 +1279,7 @@ def fixture( | |||
autouse: bool = ..., | |||
ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., | |||
name: str | None = None, | |||
) -> FixtureFunctionMarker: ... | |||
) -> FixtureFunctionDefinition: ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did this type annotation changed? Seems like it should still return a marker if we are using the @fixture(...)
syntax?
@@ -1223,19 +1195,19 @@ class FixtureFunctionMarker: | |||
def __post_init__(self, _ispytest: bool) -> None: | |||
check_ispytest(_ispytest) | |||
|
|||
def __call__(self, function: FixtureFunction) -> FixtureFunction: | |||
def __call__(self, function: FixtureFunction) -> FixtureFunctionDefinition: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should get rid of FixtureFunction
at this point...
Done that in #12999.
return fixture_definition | ||
|
||
|
||
class FixtureFunctionDefinition: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I gave this a try in #12999, so we don't need to create an issue after all in the end, we can continue on that PR once this one gets merged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this change, thanks!
I left some comments, please take a look.
instance: type | None = None, | ||
): | ||
self.name = fixture_function_marker.name or function.__name__ | ||
self.__name__ = self.name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment on this line, explaining what it does? IIUC, it's to match the __name__
attribute of function objects, but it's not obvious.
def __repr__(self) -> str: | ||
return f"<pytest_fixture({self._fixture_function})>" | ||
|
||
def __get__(self, instance, owner=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment explaining what does? IIUC, it makes the FixtureFunctionDefinition bind like a method, but it's not obvious so would be nice have a comment.
@@ -1352,7 +1360,7 @@ def fixture( | |||
def yield_fixture( | |||
fixture_function=None, | |||
*args, | |||
scope="function", | |||
scope: _ScopeName = "function", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yield_fixture
is a legacy thing and is intentionally not typed, so I would omit this change.
# TODO: paramspec/return type annotation tracking and storing | ||
class FixtureFunctionDefinition: | ||
def __init__( | ||
self, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These days I prefer to force keyword arguments in such cases:
self, | |
self, | |
*, |
|
||
# TODO: paramspec/return type annotation tracking and storing | ||
class FixtureFunctionDefinition: | ||
def __init__( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this type is not public, it's seems like something that plugin might start to use directly. For this, we have an "opt in" mechanism which makes it clear the ctor is internal and can change between pytest versions without warning. The mechanism is to add a _ispytest: bool = False,
parameter, always call with _ispytest=True
inside of pytest, and issue a warning when it's False
. There are many examples in the code you can copy from.
@pytest.fixture | ||
def fixture_function_def_test_method(self): | ||
return self.i | ||
|
||
|
||
@pytest.fixture | ||
def fixture_function_def_test_func(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would write tests that register fixtures as pytester tests, to avoid "polluting" the actual pytest-testsuite pytest session.
@@ -472,7 +473,7 @@ def _format_assertmsg(obj: object) -> str: | |||
|
|||
def _should_repr_global_name(obj: object) -> bool: | |||
if callable(obj): | |||
return False | |||
return isinstance(obj, FixtureFunctionDefinition) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment on this line?
"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 comment
The reason will be displayed to describe this comment to others. Learn more.
The previous code used functools.wraps
, while the new code does the wrapping "manually". But functools.wraps
/functools.update_wrapper
does some stuff like copy __doc__
and such. Are we losing that?
Closes #11525
During the sprint we discussed fixing the above issue and thought maybe it's a better idea to add a better represenation to fixtures. To do this without patching the object more, this PR refactors fixtures to have a class with attributes.
The main rational behind that is:
Example
Previously we had:
where fixt is a pytest fixture function that is not replaced by it's value(directly used)
Now we print: