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

Cross-module dataclass inheritance breaks get_type_hints #89687

Open
aidanbclark mannequin opened this issue Oct 19, 2021 · 13 comments
Open

Cross-module dataclass inheritance breaks get_type_hints #89687

aidanbclark mannequin opened this issue Oct 19, 2021 · 13 comments
Assignees
Labels
3.9 only security fixes 3.10 only security fixes 3.11 only security fixes stdlib Python modules in the Lib dir topic-typing type-bug An unexpected behavior, bug, or error

Comments

@aidanbclark
Copy link
Mannequin

aidanbclark mannequin commented Oct 19, 2021

BPO 45524
Nosy @gvanrossum, @ericvsmith, @JelleZijlstra, @sobolevn, @Fidget-Spinner, @AlexWaygood
PRs
  • bpo-45524: fix get_type_hints with dataclasses __init__ generation #29158
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = 'https://github.com/ericvsmith'
    closed_at = None
    created_at = <Date 2021-10-19.14:37:47.448>
    labels = ['type-bug', 'library', '3.9', '3.10', '3.11']
    title = 'Cross-module dataclass inheritance breaks get_type_hints'
    updated_at = <Date 2022-01-22.08:37:13.515>
    user = 'https://bugs.python.org/aidanbclark'

    bugs.python.org fields:

    activity = <Date 2022-01-22.08:37:13.515>
    actor = 'AlexWaygood'
    assignee = 'eric.smith'
    closed = False
    closed_date = None
    closer = None
    components = ['Library (Lib)']
    creation = <Date 2021-10-19.14:37:47.448>
    creator = 'aidan.b.clark'
    dependencies = []
    files = []
    hgrepos = []
    issue_num = 45524
    keywords = ['patch']
    message_count = 13.0
    messages = ['404307', '404312', '404313', '404314', '404321', '404740', '404752', '404754', '404812', '404822', '404823', '404907', '404918']
    nosy_count = 8.0
    nosy_names = ['gvanrossum', 'eric.smith', 'JelleZijlstra', 'slebedev', 'sobolevn', 'kj', 'AlexWaygood', 'aidan.b.clark']
    pr_nums = ['29158']
    priority = 'normal'
    resolution = None
    stage = 'patch review'
    status = 'open'
    superseder = None
    type = 'behavior'
    url = 'https://bugs.python.org/issue45524'
    versions = ['Python 3.9', 'Python 3.10', 'Python 3.11']

    @aidanbclark
    Copy link
    Mannequin Author

    aidanbclark mannequin commented Oct 19, 2021

    [I believe this is fundamentally a dataclass version of https://bugs.python.org/issue41249]

    When using from __future__ import annotations, calling get_type_hints on the constructor of a dataclass B which inherits from a dataclass A defined in another module will error if dataclass A has type hints which are not imported in the module where dataclass B is defined.

    This is best shown by example, if you have foo.py:

    from __future__ import annotations
    
    import collections
    import dataclasses
    
    @dataclasses.dataclass
    class A:
      x: collections.OrderedDict
    

    and then in bar.py:

    from __future__ import annotations
    
    import foo
    import dataclasses
    import typing
    
    @dataclasses.dataclass
    class B(foo.A):
      pass
    
    typing.get_type_hints(B)
    

    the final line will raise "NameError: name 'collections' is not defined".

    This code will not error if you do either of the following:

    • add import collections to bar.py.
    • remove the future annotations import from both files.

    I am not confident enough on the internals of dataclass to suggest a fix, but potentially a similar approach to that which solved the TypedDict equivalent https://bugs.python.org/issue41249 would work?

    @aidanbclark aidanbclark mannequin added 3.9 only security fixes stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error labels Oct 19, 2021
    @AlexWaygood
    Copy link
    Member

    I can't reproduce this on Python 3.8.3, 3.9.6, 3.10.0 or 3.11.0a1+. Which versions of Python have you tried this on? (I'm able to reproduce the typing.TypedDict bug on Python 3.8, since the fix was only backported to 3.9, but not this.)

    @slebedev
    Copy link
    Mannequin

    slebedev mannequin commented Oct 19, 2021

    I think the example has a minor typo.

    The crash is reproducible on 3.9 if the last line in bar.py is

        typing.get_type_hints(B.__init__)

    instead of

        typing.get_type_hints(B)

    @AlexWaygood
    Copy link
    Member

    Thanks @sergei. With that alteration, I have reproduced this on Python 3.9, 3.10 and 3.11.

    @AlexWaygood AlexWaygood added 3.10 only security fixes 3.11 only security fixes labels Oct 19, 2021
    @aidanbclark
    Copy link
    Mannequin Author

    aidanbclark mannequin commented Oct 19, 2021

    Ah yes, I completely apologize about that typo, the __init__ is definitely needed (serves me right for trying to clean up a repo before posting it).

    One other comment to make, perhaps obvious; manually passing a namespace to get_type_hints' globalns which contains collections does indeed solve the problem (in case anyone else sees this and is blocked). Of course determining the right values to pass is in general difficult for a user (and shouldn't be necessary).

    @sobolevn
    Copy link
    Member

    I had some time to debug this. It happens because we treat classes and functions differently.

    The difference between get_type_hints(B) and get_type_hints(B.__init__) is that we use different global contexts there:

    • For B we use __mro__ entries and extract globals and locals from there:

      cpython/Lib/typing.py

      Lines 1792 to 1796 in 86dfb55

      for base in reversed(obj.__mro__):
      if globalns is None:
      base_globals = getattr(sys.modules.get(base.__module__, None), '__dict__', {})
      else:
      base_globals = globalns
    • For B.__init__ we use simplier logic

      cpython/Lib/typing.py

      Lines 1822 to 1828 in 86dfb55

      nsobj = obj
      # Find globalns for the unwrapped object.
      while hasattr(nsobj, '__wrapped__'):
      nsobj = nsobj.__wrapped__
      globalns = getattr(nsobj, '__globals__', {})
      if localns is None:
      localns = globalns
      It does not know anything about __mro__ and super contexts

    Funny thing, this problem goes away if we remove @dataclass decorator and convert our examples into regular classes:

    # a.py
    from __future__ import annotations
    
    import collections
    
    
    class A:
      x: collections.OrderedDict
      def __init__(self, x: collections.OrderedDict) -> None:
          ...
    

    and

    # b.py
    from __future__ import annotations
    
    import a
    import typing
    
    class B(a.A):
      pass
    
    print(typing.get_type_hints(B))  
    # {'x': <class 'collections.OrderedDict'>}
    print(typing.get_type_hints(B.__init__))
    # {'x': <class 'collections.OrderedDict'>, 'return': <class 'NoneType'>}
    

    I am going to try to solve this with something really simple (if no one else is working on it).

    @AlexWaygood
    Copy link
    Member

    @nikita, I had a go at writing some more rigorous tests regarding this issue. I found the same thing you did -- the issue seems:

    • Isolated to dataclasses specifically (doesn't occur with TypedDicts, standard classes or NamedTuples)
    • Isolated to the __init__ method of dataclasses
    • Only occurs when you *subclass* a dataclass defined in another module.

    My tests are in these two files on my cpython fork:

    (I'm not proposing adding two new files to the cpython test suite -- just put the tests in new files so that I could isolate the new tests from the rest of the test suite and understand the problem better.)

    @AlexWaygood
    Copy link
    Member

    If you try running my test script with the from __future__ import annotations line at the top commented out, however, the error is the same. (from __future__ import annotations is obviously still there in the module that's being imported by the test script.)

    @slebedev
    Copy link
    Mannequin

    slebedev mannequin commented Oct 22, 2021

    Is it worth also addressing the case where a @dataclass/typing.TypeDict class is defined within a function?

    from __future__ import annotations
    
    import typing
    from dataclasses import dataclass
    
    
    def make_A():
      import collections
    
      @dataclass
      class A:
        x: collections.defaultdict
    
      return A
    
    
    A = make_A()
    
    @dataclass
    class B(A):
      y: int
    
    # NameError: name 'collections' is not defined
    print(typing.get_type_hints(B.__init__))
    

    @AlexWaygood
    Copy link
    Member

    @sergei, I believe that's a much larger issue, and one that's quite difficult to solve. It's one of the principal reasons why from __future__ import annotations behaviour wasn't made the default in Python 3.10, as was originally the plan. (This behaviour doesn't play nicely with libraries such as pydantic that analyse annotations at runtime.) I think it's probably beyond the scope of this BPO issue :)

    https://mail.python.org/archives/list/python-dev@python.org/thread/CLVXXPQ2T2LQ5MP2Y53VVQFCXYWQJHKZ/

    @sobolevn
    Copy link
    Member

    I completelly agree that the exable above is out of scope. Because it requires locals() to be modified. While this issue is focused on globals().

    @slebedev
    Copy link
    Mannequin

    slebedev mannequin commented Oct 23, 2021

    Is it worth filing a separate issue for locals()?

    In my experience local classes are less common than cross-module inheritance, so I suspect that the chances of someone accidentally hitting lack of locals() forwarding are quite low. However, given how confusing the error message is, it might be worth having an issue for that. Wdyt?

    @sobolevn
    Copy link
    Member

    In my opinion, it is never a bad thing to create a new issue :)

    сб, 23 окт. 2021 г. в 22:46, Sergei Lebedev <report@bugs.python.org>:

    Sergei Lebedev <slebedev@google.com> added the comment:

    Is it worth filing a separate issue for locals()?

    In my experience local classes are less common than cross-module
    inheritance, so I suspect that the chances of someone accidentally hitting
    lack of locals() forwarding are quite low. However, given how confusing the
    error message is, it might be worth having an issue for that. Wdyt?

    ----------


    Python tracker <report@bugs.python.org>
    <https://bugs.python.org/issue45524\>


    @ericvsmith ericvsmith self-assigned this Nov 30, 2021
    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    3.9 only security fixes 3.10 only security fixes 3.11 only security fixes stdlib Python modules in the Lib dir topic-typing type-bug An unexpected behavior, bug, or error
    Projects
    None yet
    Development

    No branches or pull requests

    3 participants