-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
improvement: show return type annotations in fixtures #13680
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
base: main
Are you sure you want to change the base?
Conversation
src/_pytest/fixtures.py
Outdated
if annotation is not sig.empty and annotation != inspect._empty: | ||
return inspect.formatannotation(annotation).replace("'", "") |
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 seems odd to me, for various reasons:
inspect._empty
is private so it shouldn't be usedinspect.formatannotation
, while public, is undocumented so probably shouldn't be used either- Why remove
'
from it, which is somethinginspect
itself doesn't seem to do?
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, that inspect._empty
condition is actually redundant. I investigated the logic further and there were lots of inconsistent behaviors in the test (that's why I previously did replace("'", "")
) and corner cases. However, the new solution (though a bit hacky) is the most general one and hopefully covering everything.
changelog/13676.improvement.rst
Outdated
@@ -0,0 +1 @@ | |||
Added return type annotations in ``fixtures`` and ``fixtures-per-test``. |
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 return type annotations in ``fixtures`` and ``fixtures-per-test``. | |
Added return type annotations in ``--fixtures`` and ``--fixtures-per-test``. |
src/_pytest/fixtures.py
Outdated
if annotation.__module__ == "typing": | ||
return str(annotation).replace("typing.", "") | ||
return annotation.__name__ | ||
except (ValueError, TypeError): |
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 would ValueError
and TypeError
happen here? The only thing that could happen I think is AttributeError
, which already happens with e.g. -> None:
as annotation.
That being said, None
should definitively handled correctly by the code and tested properly 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.
TypeError happens when the argument is not a callable (e.g. a number). I doubt this case happens. ValueError happens when the passed argument is a callable but a signature can't be obtained (e.g. range
). Also unlikely imo.
For -> None:
on my local test didn't raise any error.
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.
Both of those cause neither of those exceptions, but cause an AttributeError
for .__module__
.
testing/python/fixtures.py
Outdated
result.assert_outcomes(passed=1) | ||
|
||
|
||
def test_get_return_annotation() -> 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.
Maybe turn this into a test class and split the individual asserts into separate test functions? That way, if one of them fails, the rest of the tests will still run.
testing/python/fixtures.py
Outdated
|
||
assert get_return_annotation(no_annotation) == "" | ||
|
||
def none_return() -> 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.
Ah, this test happens to work (despite the bug above) because this module uses from __future__ import annotations
, so the annotation already is 'None'
rather than None
.
Not sure how to best get around this. Maybe those tests should be in a separate module which deliberately doesn't use that?
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.
On my local test (3.13) with or without from __future__ import annotations
it worked the same. I might be very wrong but I think it behaves differently in different Python versions. Still, I added a exclusive check for none. Do you think moving it to another module is worth it? Seems too much for a humble helper function!
PS: The reason I didn't use `isinstance(annotation, types.NoneType) was that pylance was complaining.
@The-Compiler Could you please review again? |
Sorry for the radio silence, I never got around to taking a deeper look at this, seems like getting a type annotation as a string is surprisingly difficult with a lot of corner cases... I still think having a file without the Trying with Given the amount of corner cases, I wonder if (despite what I said earlier) we should go back at letting #13550 added a helper |
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
Are you suggesting to use |
At least for Python < 3.14, for two reasons:
|
In addition to the current code, I've tried |
I suspected this is going to be trickier than it looked on the surface 😅. I really hope we don't need to resort to AST shenanigans, after all, other tools do sometimes print type annotations as well I think? Can you elaborate on "all fail miserably on the test cases"? I'd hope that |
I mean the test cases fail. For example doing:
will give |
I think that's fine, after all that's what import inspect
from collections.abc import Callable
def func(x: Callable[[], None]) -> None:
pass
print(inspect.signature(func)) And with the approach outlined above, starting on Python 3.14 (or before with |
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
The best that I could come up with was treating signature as string (as you suggested) and use regex to remove clutterings (if present) and it passed the tests locally. But again the tests are failing in CI and codecov is complaining unfairly. Any tips? |
That's not what I meant with treating it as a string at all 😅 Parsing the string output definitely isn't necessary here, as you're just picking out what was rendered using What I meant was that with Python 3.14, you can pass So basically I see three scenarios the code needs to handle:
As for the coverage, looks like pre-commit tried to be helpful and introduced the This seems to happen via ruff: Line 102 in a55c959
So if you add a |
Understood, but |
That's because it formats the Maybe that could actually be useful information in real-world scenarios (try e.g. with It looks like If you disagree, I suppose we're back to copying the |
for more information, see https://pre-commit.ci
I've tried it but it doesn't work as intended. Do you object using regex to remove the extra verbose parts?
|
closes #13676