Skip to content
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

Merged
merged 53 commits into from
Dec 7, 2024

Conversation

Glyphack
Copy link
Contributor

@Glyphack Glyphack commented Jun 18, 2024

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:

  • Now we have a type for fixtures makes it easier to check for fixture types and no need to perform cast on objects. Removed monkey patching.
  • They are no longer functions that carry a class within them.
  • When you decorate a function with a fixture it's not possible to call it anymore. So it makes more sense for it to not be a function or method.

Example

Previously we had:

===================================== short test summary info ======================================
FAILED tmp31nzhe4b.py::test_something - assert fixt == 42
======================================== 1 failed in 0.03s =========================================

where fixt is a pytest fixture function that is not replaced by it's value(directly used)

Now we print:

===================================== short test summary info ======================================
FAILED tmp31nzhe4b.py::test_something - assert pytest_fixture(<function fixt at *>) == 42 
======================================== 1 failed in 0.03s =========================================

@Glyphack Glyphack changed the title (assertion): Use repr for pytest fixtures and set repr for pytest fixtures (fixtures): Replace fixture represenation with a class Jun 20, 2024
@psf-chronographer psf-chronographer bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Jun 20, 2024
@Glyphack Glyphack force-pushed the reprs-assert-callable branch 3 times, most recently from bf90fca to d25a8d9 Compare June 20, 2024 13:51
testing/test_compat.py Outdated Show resolved Hide resolved
@Glyphack Glyphack marked this pull request as ready for review June 20, 2024 15:19
@Glyphack Glyphack force-pushed the reprs-assert-callable branch 2 times, most recently from 3d2ec33 to 84cc5b7 Compare July 4, 2024 18:48
@Glyphack
Copy link
Contributor Author

Glyphack commented Jul 4, 2024

Hey @The-Compiler @RonnyPfannschmidt I rebased the PR could you please take a look?

changelog/11525.improvement.rst Outdated Show resolved Hide resolved
src/_pytest/compat.py Outdated Show resolved Hide resolved
src/_pytest/compat.py Outdated Show resolved Hide resolved
src/_pytest/fixtures.py Outdated Show resolved Hide resolved
src/_pytest/fixtures.py Outdated Show resolved Hide resolved
@@ -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):
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

@Glyphack Glyphack Oct 23, 2024

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.

@Glyphack Glyphack force-pushed the reprs-assert-callable branch 3 times, most recently from f0324a5 to 9d2a916 Compare July 30, 2024 18:37
@webknjaz
Copy link
Member

Hey @Glyphack, could you resolve the merge conflicts / rebase this?

@Glyphack
Copy link
Contributor Author

Hey @Glyphack, could you resolve the merge conflicts / rebase this?

Yes will do today.

Copy link
Member

@RonnyPfannschmidt RonnyPfannschmidt left a 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 Show resolved Hide resolved
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
Copy link
Member

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

Copy link
Contributor Author

@Glyphack Glyphack Oct 27, 2024

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

Copy link
Member

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

Copy link
Contributor Author

@Glyphack Glyphack Nov 15, 2024

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:

  1. The pytest fixture is applied on a function so I can display this case with Fixture(f)
  2. 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.

Copy link
Member

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

Copy link
Contributor Author

@Glyphack Glyphack Nov 17, 2024

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?

Copy link
Member

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

Copy link
Contributor Author

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.

src/_pytest/fixtures.py Show resolved Hide resolved
src/_pytest/fixtures.py Show resolved Hide resolved
@Glyphack
Copy link
Contributor Author

Glyphack commented Oct 27, 2024

Thank you both for the review. I left some TODO comments I will address them in a follow up PR.

@RonnyPfannschmidt RonnyPfannschmidt added this to the 8.4 milestone Nov 12, 2024
@Glyphack
Copy link
Contributor Author

Glyphack commented Dec 5, 2024

Thank you so much for the reviews. Please take another look and let me know if there are more improvements. I was busy last week I will be quicker in resolving them this time.
@nicoddemus I like to help with the PR you created. I'll take a look there once this is done.

Copy link
Member

@bluetech bluetech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update! Please see my comments.

@nicoddemus I like to help with the PR you created. I'll take a look there once this is done.

I think @nicoddemus's intention is for you to incorporate his suggestions in this PR, this way we can review & merge everything together.

"https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code."
)

@functools.wraps(function)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the best thing would be to use functools.update_wrapper in FixtureFunctionDefinition.__init__, unless there is something preventing it.

@@ -1352,7 +1360,7 @@ def fixture(
def yield_fixture(
fixture_function=None,
*args,
scope="function",
scope: _ScopeName = "function",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is still relevant

self.__name__ = self.name
self._fixture_function_marker = fixture_function_marker
self._fixture_function = function
self._instance = instance
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it wouldn't be simpler to keep self._fixture_function bound to instance instead of keeping _instance and binding it lazily? Is there a reason why it needs to be lazy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I applied this and it's simpler. I don't think there's any reason to do it when we get the fixture.

@@ -1271,7 +1279,7 @@ def fixture(
autouse: bool = ...,
ids: Sequence[object | None] | Callable[[Any], object | None] | None = ...,
name: str | None = None,
) -> FixtureFunctionMarker: ...
) -> FixtureFunctionDefinition: ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is still relevant

@nicoddemus
Copy link
Member

I think @nicoddemus's intention is for you to incorporate his suggestions in this PR, this way we can review & merge everything together.

@bluetech not really, I meant to apply those later (I only created a Draft PR to not lose the local commits I made).

@bluetech
Copy link
Member

bluetech commented Dec 6, 2024

@bluetech not really, I meant to apply those later (I only created a Draft PR to not lose the local commits I made).

Ah sorry for the assumption. The reason I thought it would be better to combine is that merging as-is would introduce a typing regression (fixture functions would lose their signature). But it's only temporary so not a problem.

Copy link
Member

@bluetech bluetech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Glyphack!

@nicoddemus nicoddemus merged commit ecde993 into pytest-dev:main Dec 7, 2024
28 checks passed
nicoddemus added a commit to nicoddemus/pytest that referenced this pull request Dec 7, 2024
* Carry around parameters and return value in `FixtureFunctionDefinition`.
* Add `FixtureParams` to `FixtureDef`.

Follow up to pytest-dev#12473.
nicoddemus added a commit to nicoddemus/pytest that referenced this pull request Dec 7, 2024
* Carry around parameters and return value in `FixtureFunctionDefinition`.
* Add `FixtureParams` to `FixtureDef`.

Follow up to pytest-dev#12473.
@youtux
Copy link

youtux commented Jan 11, 2025

FYI, this breaks pytest-factoryboy, but that's something I have to deal with.
Just wanted to let you know, in case maybe other users could have the same issue.

That being said, __pytest_wrapped__ was an internal API, so 100% agree that there is no guaranteed behavior on these.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bot:chronographer:provided (automation) changelog entry is part of PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Show repr for callables used in an assert directly
6 participants