-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
bpo-41370: Evaluate strings as forward refs in PEP 585 generics #30900
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
Conversation
…`ForwardRef` before evaluation (https://bugs.python.org/issue41370)
This comment has been minimized.
This comment has been minimized.
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.
Looks pretty good! I have one small suggestion. This also needs a NEWS entry, which you can add with blurb.
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 is the code that I posted to the issue. But I also posted a few questions there, and those should at least be answered by tests:
-
But would that be enough? The algorithm would have to recursively dive into args to see if there's a string hidden deep inside, e.g. list[tuple[int, list["N"]]].
-
And what if the user writes something hybrid, like List[list["N"]]? What other cases would we need to cover?
-
And can we sell this as a bugfix for 3.10, or will this be a new feature in 3.11?
-
How will it interact with from future import annotations?
The tests already cover the sort of case you list here (there's a dict[int, list[List[list["T"]]] I think). We could add more variants though.
I think this can be considered a bugfix. get_type_hints() is supposed to evaluate forward references but it didn't in all cases.
That's another interesting test case. It would basically give us two levels of stringification, like |
Would also be good to test a recursive forward ref:
We guard against this sort of thing already, but we should validate it works in this case too. |
You can add a NEWS entry using https://blurb-it.herokuapp.com/ as an alternative to the CLI :) |
Yes, sorry for not mentioning it. Same code, different location. I've added two more unit tests for the cases Guido and you mentioned. Thanks, added it. |
Co-authored-by: Jelle Zijlstra <[email protected]>
Misc/NEWS.d/next/Library/2022-01-27-11-54-16.bpo-41370.gYxCPE.rst
Outdated
Show resolved
Hide resolved
Co-authored-by: Alex Waygood <[email protected]>
@@ -331,6 +331,12 @@ def _eval_type(t, globalns, localns, recursive_guard=frozenset()): | |||
if isinstance(t, ForwardRef): | |||
return t._evaluate(globalns, localns, recursive_guard) | |||
if isinstance(t, (_GenericAlias, GenericAlias, types.UnionType)): | |||
if isinstance(t, GenericAlias): | |||
args = tuple( | |||
ForwardRef(arg) if isinstance(arg, str) else arg |
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.
Do we need to pass globals
and locals
to ForwardRef
here? 🤔
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 may not be knowledgable enough about the typing code base, but I don't see a globals/locals argument for ForwardRef
. Do you mean the module? I couldn't find out how to use/what to pass to that argument exactly.
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.
It's too late for that anyway. The ForwardRef class has an optional module= argument which may be used to resolve references, but the best we could do at this point is passing globalns.get("__name__")
, which would just end up with the same globalns as we are using anyway.
This suggests there could still be scenarios where this will fail, esp. when a type alias defined in one module is used in another. But that would only be solvable by recording the module at the time the alias is being defined, and we've already said we wouldn't go that far (since it would require a ForwardRef implementation in C).
Actually I'm not sure if we can find a good way to make the |
We can just change those tests from |
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.
LGTM.
I'll wait landing this until 3.11a6 is out, so as not to disturb the release process (which is already stressed by a last-minute blocker).
@@ -331,6 +331,12 @@ def _eval_type(t, globalns, localns, recursive_guard=frozenset()): | |||
if isinstance(t, ForwardRef): | |||
return t._evaluate(globalns, localns, recursive_guard) | |||
if isinstance(t, (_GenericAlias, GenericAlias, types.UnionType)): | |||
if isinstance(t, GenericAlias): | |||
args = tuple( | |||
ForwardRef(arg) if isinstance(arg, str) else arg |
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.
It's too late for that anyway. The ForwardRef class has an optional module= argument which may be used to resolve references, but the best we could do at this point is passing globalns.get("__name__")
, which would just end up with the same globalns as we are using anyway.
This suggests there could still be scenarios where this will fail, esp. when a type alias defined in one module is used in another. But that would only be solvable by recording the module at the time the alias is being defined, and we've already said we wouldn't go that far (since it would require a ForwardRef implementation in C).
There's no C code here, and 3.11a6 seems to be still delayed, so I'm just landing this. Sorry for the confusion. |
Update: This didn't make 3.11a6. The release process was farther along than I realized. |
This Pull Requests suggests a change to
typing._eval_type()
that considers strings in PEP 585 generics (ie. instances oftypes.GenericAlias
) as forward references. This is necessary becauseForwardRef
is not and likely will not be implemented in C, and thus strings used as forward references in PEP 585 are not currently evaluated bytyping.get_type_hints()
.https://bugs.python.org/issue41370
A test case that uses
assertIs()
currently fails because in the current state_eval_type()
creates a copy of the same generic alias with transformed arguments, thus for any PEP 585 generic aliasx
, the comparisonx is typing.get_type_hints(func)['X']
will evaluate toFalse
, assumingfunc
has a field/argument annotationX: x
. I think that this can be worked around, and happy to propose an updated implementation.I created this PR as a proof of concept until a there is consent that in spirit this change to
get_type_hints()
is something that should happen. So for now I'll keep it as it is.Open todos/questions
test_get_type_hints_annotated
intest_typing.py
types.GenericAlias
separately or is it safe to assume that a string argument is always a forward reference?Co-Authored-By: Guido van Rossum [email protected]
https://bugs.python.org/issue41370