Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Merged
merged 10 commits into from
Mar 7, 2022
Merged

bpo-41370: Evaluate strings as forward refs in PEP 585 generics #30900

merged 10 commits into from
Mar 7, 2022

Conversation

NiklasRosenstein
Copy link
Contributor

@NiklasRosenstein NiklasRosenstein commented Jan 25, 2022

This Pull Requests suggests a change to typing._eval_type() that considers strings in PEP 585 generics (ie. instances of types.GenericAlias) as forward references. This is necessary because ForwardRef is not and likely will not be implemented in C, and thus strings used as forward references in PEP 585 are not currently evaluated by typing.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 alias x, the comparison x is typing.get_type_hints(func)['X'] will evaluate to False, assuming func has a field/argument annotation X: 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.

======================================================================
FAIL: test_get_type_hints_annotated (__main__.GetTypeHintTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/niklas/git/cpython/Lib/test/test_typing.py", line 3298, in test_get_type_hints_annotated
    self.assertIs(
    ^^^^^^^^^^^^^^
AssertionError: tuple[typing.Annotated[~T, (1, 0)], ...] is not tuple[typing.Annotated[~T, (1, 0)], ...]

Open todos/questions

  • Fix test_get_type_hints_annotated in test_typing.py
  • Do we actually need to treat types.GenericAlias separately or is it safe to assume that a string argument is always a forward reference?
  • Backport to 3.10 as a bugfix?

Co-Authored-By: Guido van Rossum [email protected]

https://bugs.python.org/issue41370

@the-knights-who-say-ni

This comment has been minimized.

Copy link
Member

@JelleZijlstra JelleZijlstra left a 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.

Copy link
Member

@gvanrossum gvanrossum left a 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?

@JelleZijlstra
Copy link
Member

  • 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?

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.

  • And can we sell this as a bugfix for 3.10, or will this be a new feature in 3.11?

I think this can be considered a bugfix. get_type_hints() is supposed to evaluate forward references but it didn't in all cases.

  • How will it interact with from future import annotations?

That's another interesting test case. It would basically give us two levels of stringification, like x: 'list["N"]'. We should test that get_type_hints() resolves both levels of strings.

@JelleZijlstra
Copy link
Member

Would also be good to test a recursive forward ref:

X = list["X"]
def f(x: X): ...
get_type_hints(f)  # hopefully no RecursionError

We guard against this sort of thing already, but we should validate it works in this case too.

@AlexWaygood
Copy link
Member

You can add a NEWS entry using https://blurb-it.herokuapp.com/ as an alternative to the CLI :)

@NiklasRosenstein
Copy link
Contributor Author

[@gvanrossum]

This is the code that I posted to the issue.

Yes, sorry for not mentioning it. Same code, different location.

[@JelleZijlstra]

I've added two more unit tests for the cases Guido and you mentioned.

[@AlexWaygood]

Thanks, added it.

@@ -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
Copy link
Member

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? 🤔

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 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.

Copy link
Member

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).

@NiklasRosenstein NiklasRosenstein marked this pull request as ready for review February 22, 2022 23:11
@NiklasRosenstein
Copy link
Contributor Author

Actually I'm not sure if we can find a good way to make the Annotated equality unit test work (test_get_type_hints_annotated) with this new behaviour. Maybe this is a side effect of this change that has to be accepted, but I'm happy to hear suggestions.

@JelleZijlstra
Copy link
Member

Actually I'm not sure if we can find a good way to make the Annotated equality unit test work (test_get_type_hints_annotated) with this new behaviour

We can just change those tests from assertIs to assertEquals. It doesn't seem important that the same object is returned.

Copy link
Member

@gvanrossum gvanrossum left a 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
Copy link
Member

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).

@gvanrossum
Copy link
Member

There's no C code here, and 3.11a6 seems to be still delayed, so I'm just landing this. Sorry for the confusion.

@gvanrossum gvanrossum merged commit b465b60 into python:main Mar 7, 2022
@gvanrossum
Copy link
Member

gvanrossum commented Mar 7, 2022

Update: This didn't make 3.11a6. The release process was farther along than I realized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants