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

bpo-45524: fix get_type_hints with dataclasses __init__ generation #29158

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

sobolevn
Copy link
Member

@sobolevn sobolevn commented Oct 22, 2021

This is a rather brave idea to manipulate globals, but it looks like it works.
It can backfire is some corner cases, though. But, I am not able to think about any. Ideas? 🙂

https://bugs.python.org/issue45524

CC @AlexWaygood @aidan.b.clark

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

Looks like a good fix -- the test script I linked to in the BPO issue passes fine (as do the tests you've added, obviously). Nice job!

Lib/dataclasses.py Outdated Show resolved Hide resolved
def test_dataclass_from_another_module(self):
from test import dataclass_textanno
from dataclasses import dataclass

@dataclass
class Default(dataclass_textanno.Bar):
pass

@dataclass(init=False)
class WithInitFalse(dataclass_textanno.Bar):
pass

@dataclass(init=False)
class CustomInit(dataclass_textanno.Bar):
def __init__(self, foo: dataclass_textanno.Foo) -> None:
pass

classes = [
Default,
WithInitFalse,
CustomInit,
dataclass_textanno.WithFutureInit,
]
for klass in classes:
with self.subTest(klass=klass):
self.assertEqual(
get_type_hints(klass),
{'foo': dataclass_textanno.Foo},
)
self.assertEqual(get_type_hints(klass.__new__), {})
self.assertEqual(
get_type_hints(klass.__init__),
{'foo': dataclass_textanno.Foo, 'return': type(None)},
)

Copy link
Member

Choose a reason for hiding this comment

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

Bikeshedding point, but: would it be better to put these tests in test_dataclasses? For the similar issue with TypedDict, the new tests were put next to the other TypedDict tests. Moreover, given that this is a very specific issue with the way that the dataclasses module generates __init__ functions, it seems like more of a dataclasses bug than a get_type_hints bug.

Copy link
Member Author

@sobolevn sobolevn Oct 22, 2021

Choose a reason for hiding this comment

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

I'm fine with any decision from maintainers 👍
Both cases make sense.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed!

Lib/test/test_typing.py Outdated Show resolved Hide resolved
@sobolevn
Copy link
Member Author

sobolevn commented Oct 22, 2021

I found a corner case: when I define a proxy module, say dataclass_textanno2.py, with this content:

from __future__ import annotations

import dataclasses
from test import dataclass_textanno  # We need to be sure that `Foo` is not in scope


class Custom:
    pass


@dataclasses.dataclass
class Child(dataclass_textanno.Bar):
    custom: Custom


@dataclasses.dataclass(init=False)
class WithFutureInit(Child):
    def __init__(self, foo: dataclass_textanno.Foo, custom: Custom) -> None:
        pass

Then, this test does not pass:

def test_dataclass_from_proxy_module(self):
        from test import dataclass_textanno, dataclass_textanno2
        from dataclasses import dataclass

        @dataclass
        class Default(dataclass_textanno2.Child):
            pass

       self.assertEqual(
                    get_type_hints(Defualt.__init__),
                    {'foo': dataclass_textanno.Foo, 'custom': dataclass_textanno2.Custom,  'return': type(None)},
                )

Output:

======================================================================
ERROR: test_dataclass_from_proxy_module (test.test_typing.GetTypeHintTests) (klass=<class 'test.test_typing.GetTypeHintTests.test_dataclass_from_proxy_module.<locals>.Default'>)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/sobolev/Desktop/cpython/Lib/test/test_typing.py", line 3376, in test_dataclass_from_proxy_module
    get_type_hints(klass.__init__),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/sobolev/Desktop/cpython/Lib/typing.py", line 1852, in get_type_hints
    value = _eval_type(value, globalns, localns)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/sobolev/Desktop/cpython/Lib/typing.py", line 334, in _eval_type
    return t._evaluate(globalns, localns, recursive_guard)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/sobolev/Desktop/cpython/Lib/typing.py", line 700, in _evaluate
    eval(self.__forward_code__, globalns, localns),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<string>", line 1, in <module>
NameError: name 'Custom' is not defined

----------------------------------------------------------------------
Ran 1 test in 0.044s

FAILED (errors=1)
test test_typing failed
test_typing failed (1 error)

== Tests result: FAILURE ==

1 test failed:
    test_typing

@sobolevn
Copy link
Member Author

Now it is even more wild. I collect globals from all other mro items especially for __init__ annotations.
This is literally one of the craziest things I've ever done 😆

Lib/test/test_typing.py Outdated Show resolved Hide resolved
Lib/test/test_typing.py Outdated Show resolved Hide resolved
@sobolevn
Copy link
Member Author

One more case: same names obviously get overriden by the current logic. I will try something else.

@AlexWaygood
Copy link
Member

One more case: same names obviously get overriden by the current logic. I will try something else.

Not sure I understand. What's a failing test case?

@sobolevn
Copy link
Member Author

sobolevn commented Oct 29, 2021

@AlexWaygood done! Here's the failing test case: https://github.com/python/cpython/pull/29158/files#diff-f0436842533448f8f02a19081d7c9cc8372b6685756caf32c83dfecebbee68e7R3189-R3219

And here's my new solution: https://github.com/python/cpython/pull/29158/files#diff-44ce2dc1c4922b2f5cf7631d8f86cc569a4c25eb003aaecdc2bc22eb9163d5f5R455-R468

I now manually resolve str types into ForwardRef if they are used for __init__.
This looks like the most logical and easy way to solve our problem.

@AlexWaygood
Copy link
Member

@AlexWaygood done! Here's the failing test case: https://github.com/python/cpython/pull/29158/files#diff-f0436842533448f8f02a19081d7c9cc8372b6685756caf32c83dfecebbee68e7R3189-R3219

And here's my new solution: https://github.com/python/cpython/pull/29158/files#diff-44ce2dc1c4922b2f5cf7631d8f86cc569a4c25eb003aaecdc2bc22eb9163d5f5R455-R468

I now manually resolve str types into ForwardRef if they are used for __init__. This looks like the most logical and easy way to solve our problem.

Ahh, I understand -- thanks!

Lib/dataclasses.py Outdated Show resolved Hide resolved
Lib/test/dataclass_textanno2.py Outdated Show resolved Hide resolved
Lib/test/dataclass_textanno2.py Outdated Show resolved Hide resolved
@sobolevn
Copy link
Member Author

Thanks! Done 👍

@github-actions
Copy link

This PR is stale because it has been open for 30 days with no activity.

@github-actions github-actions bot added the stale Stale PR or inactive for long period of time. label Nov 29, 2021
@sobolevn
Copy link
Member Author

Rebased 🙂

Friendly ping to @ericvsmith and @Fidget-Spinner

@github-actions github-actions bot removed the stale Stale PR or inactive for long period of time. label Nov 30, 2021
@sobolevn sobolevn closed this Aug 28, 2022
@sobolevn sobolevn reopened this Aug 28, 2022
@ericvsmith
Copy link
Member

I'm putting this off until the SC makes a decision on PEP 649.

@ericvsmith ericvsmith self-assigned this Oct 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants