Skip to content
Prev Previous commit
Next Next commit
Manually resolve string types into ForwardRef in __init__ of a da…
…taclass
  • Loading branch information
sobolevn committed Nov 29, 2021
commit 97a34960a875eb28b2745ea5c945c432a327102f
36 changes: 20 additions & 16 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import abc
import _thread
from types import FunctionType, GenericAlias
from typing import ForwardRef
Comment thread
sobolevn marked this conversation as resolved.
Outdated


__all__ = ['dataclass',
Expand Down Expand Up @@ -447,10 +448,24 @@ def _field_assign(frozen, name, value, self_name):
return f'{self_name}.{name}={value}'


def _field_init(f, frozen, globals, self_name, slots):
def _field_init(f, frozen, globals, self_name, slots, module):
# Return the text of the line in the body of __init__ that will
# initialize this field.

if f.init and isinstance(f.type, str):
# We need to resolve this string type into a real `ForwardRef` object,
# because otherwise we might end up with unsolvable annotations.
# For example:
# def __init__(self, d: collections.OrderedDict) -> None:
# We won't be able to resolve `collections.OrderedDict`
# with wrong `module` param, when placed in a different module. #45524
try:
f.type = ForwardRef(f.type, module=module, is_class=True)
except SyntaxError:
# We don't want to fail class creation
# when `ForwardRef` cannot be constructed.
pass

default_name = f'_dflt_{f.name}'
if f.default_factory is not MISSING:
if f.init:
Expand Down Expand Up @@ -527,7 +542,7 @@ def _init_param(f):


def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init,
self_name, globals, slots):
self_name, globals, slots, module):
# fields contains both real fields and InitVar pseudo-fields.

# Make sure we don't have fields without defaults following fields
Expand All @@ -554,7 +569,7 @@ def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init,

body_lines = []
for f in fields:
line = _field_init(f, frozen, locals, self_name, slots)
line = _field_init(f, frozen, locals, self_name, slots, module)
# line is None means that this field doesn't require
# initialization (it's a pseudo-field). Just skip it.
if line:
Expand Down Expand Up @@ -906,7 +921,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
# we're iterating over them, see if any are frozen.
any_frozen_base = False
has_dataclass_bases = False
init_globals = dict(globals)

for b in cls.__mro__[-1:0:-1]:
# Only process classes that have been processed by our
Expand All @@ -918,17 +932,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
fields[f.name] = f
if getattr(b, _PARAMS).frozen:
any_frozen_base = True
if has_dataclass_bases:
# If dataclass has other dataclass as a base type,
# it might have existing `__init__` method.
# We need to change its `globals`, because otherwise
# we might end up with unsolvable annotations. For example:
# `def __init__(self, d: collections.OrderedDict) -> None:`
# We won't be able to resolve `collections.OrderedDict`
# with wrong `globals`, when placed in a different module. #45524
super_init = getattr(b, '__init__', None)
if super_init is not None:
init_globals.update(getattr(super_init, '__globals__', {}))

# Annotations that are defined in this class (not in base
# classes). If __annotations__ isn't present, then this class
Expand Down Expand Up @@ -1045,8 +1048,9 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
# if possible.
'__dataclass_self__' if 'self' in fields
else 'self',
globals,
slots,
init_globals,
cls.__module__,
))

# Get the fields as a list, and include only real fields. This is
Expand Down
9 changes: 9 additions & 0 deletions Lib/test/dataclass_textanno2.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ class Child(dataclass_textanno.Bar):
custom: Custom


class Foo: # matching name with `dataclass_testanno.Foo`
pass


@dataclasses.dataclass
class WithMatchinNameOverride(dataclass_textanno.Bar):
Comment thread
sobolevn marked this conversation as resolved.
Outdated
foo: Foo # we override existing `foo: Foo` with
Comment thread
sobolevn marked this conversation as resolved.
Outdated


@dataclasses.dataclass(init=False)
class WithFutureInit(Child):
def __init__(self, foo: dataclass_textanno.Foo, custom: Custom) -> None:
Expand Down
31 changes: 31 additions & 0 deletions Lib/test/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -3221,6 +3221,37 @@ class FutureInitChild(dataclass_textanno2.WithFutureInit):
},
)

def test_dataclass_proxy_modules_matching_name_override(self):
# see bpo-45524
from test import dataclass_textanno2
from dataclasses import dataclass

@dataclass
class Default(dataclass_textanno2.WithMatchinNameOverride):
pass

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



class TestMakeDataclass(unittest.TestCase):
def test_simple(self):
Expand Down