Skip to content

bpo-46333: Honor module parameter in ForwardRef #30536

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 13 commits into from
Feb 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2611,6 +2611,10 @@ def test_forward_equality(self):
fr = typing.ForwardRef('int')
self.assertEqual(fr, typing.ForwardRef('int'))
self.assertNotEqual(List['int'], List[int])
self.assertNotEqual(fr, typing.ForwardRef('int', module=__name__))
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I wonder if this test is run directly, will __name__ be `main``? Wouldn't that make the test pass incorrectly? (Throwing this question out there because I'm not too sure myself). Maybe we should be safe and give it a distinct name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test is to ensure that ForwardRef with module set is different from ForwardRef with module not set (That was previsously not the case). The actual value of module (or __name__) is not relevant. Under the hood the comparison is None vs str

Copy link
Member

@Fidget-Spinner Fidget-Spinner Jan 12, 2022

Choose a reason for hiding this comment

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

My bad, thanks for pointing that out. I mistakenly assumed it would grab a default value from the current scope to populate module in things like ClassVar['int'] due to the _type_check/_type_convert function (I'm now shocked that it doesn't and only does that for TypedDict).

Copy link
Contributor Author

@aha79 aha79 Jan 12, 2022

Choose a reason for hiding this comment

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

Yes, unfortunately the current module is not captured automatically.
The current scope/module is typicallly obtained by get_type_hints because a class knows its module (and so all ForwardRefs in member annotations can be correctly resolved. But each forward Ref does originally not know its module).
However for type aliases this does not work, and there one needs the explicit module.

For example

Json = Union[ List['Json'], Dict[str,'Json'], int, float, bool, None ]

cannot be exported (and expect to work) unless one sets the module for each forward ref.

frm = typing.ForwardRef('int', module=__name__)
self.assertEqual(frm, typing.ForwardRef('int', module=__name__))
self.assertNotEqual(frm, typing.ForwardRef('int', module='__other_name__'))

def test_forward_equality_gth(self):
c1 = typing.ForwardRef('C')
Expand Down Expand Up @@ -2647,6 +2651,14 @@ def foo(a: c1_gth, b: c2_gth):
self.assertEqual(hash(c1_gth), hash(c2_gth))
self.assertEqual(hash(c1), hash(c1_gth))

c3 = typing.ForwardRef('int', module=__name__)
c4 = typing.ForwardRef('int', module='__other_name__')

self.assertNotEqual(hash(c3), hash(c1))
self.assertNotEqual(hash(c3), hash(c1_gth))
self.assertNotEqual(hash(c3), hash(c4))
self.assertEqual(hash(c3), hash(typing.ForwardRef('int', module=__name__)))

def test_forward_equality_namespace(self):
class A:
pass
Expand Down
5 changes: 3 additions & 2 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,10 +713,11 @@ def __eq__(self, other):
if self.__forward_evaluated__ and other.__forward_evaluated__:
return (self.__forward_arg__ == other.__forward_arg__ and
self.__forward_value__ == other.__forward_value__)
return self.__forward_arg__ == other.__forward_arg__
return (self.__forward_arg__ == other.__forward_arg__ and
self.__forward_module__ == other.__forward_module__)

def __hash__(self):
return hash(self.__forward_arg__)
return hash((self.__forward_arg__, self.__forward_module__))

def __or__(self, other):
return Union[self, other]
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,7 @@ Anders Hammarquist
Mark Hammond
Harald Hanche-Olsen
Manus Hand
Andreas Hangauer
Milton L. Hankins
Carl Bordum Hansen
Stephen Hansen
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The :meth:`__eq__` and :meth:`__hash__` methods of
:class:`typing.ForwardRef` now honor the ``module`` parameter of
:class:`typing.ForwardRef`. Forward references from different
modules are now differentiated.