Skip to content

Commit 39006fb

Browse files
committed
bpo-34776: Fix dataclasses to support __future__ "annotations" mode
1 parent 488cfb7 commit 39006fb

File tree

4 files changed

+43
-8
lines changed

4 files changed

+43
-8
lines changed

Lib/dataclasses.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -354,13 +354,17 @@ def _create_fn(name, args, body, *, globals=None, locals=None,
354354
locals['_return_type'] = return_type
355355
return_annotation = '->_return_type'
356356
args = ','.join(args)
357-
body = '\n'.join(f' {b}' for b in body)
357+
body = '\n'.join(f' {b}' for b in body)
358358

359359
# Compute the text of the entire function.
360-
txt = f'def {name}({args}){return_annotation}:\n{body}'
360+
txt = f' def {name}({args}){return_annotation}:\n{body}'
361+
362+
local_vars = ', '.join(locals.keys())
363+
txt = f"def __create_fn__({local_vars}):\n{txt}\n return {name}"
361364

362365
exec(txt, globals, locals)
363-
return locals[name]
366+
create_fn = locals.pop('__create_fn__')
367+
return create_fn(**locals)
364368

365369

366370
def _field_assign(frozen, name, value, self_name):
@@ -448,7 +452,7 @@ def _init_param(f):
448452
return f'{f.name}:_type_{f.name}{default}'
449453

450454

451-
def _init_fn(fields, frozen, has_post_init, self_name):
455+
def _init_fn(fields, frozen, has_post_init, self_name, modname):
452456
# fields contains both real fields and InitVar pseudo-fields.
453457

454458
# Make sure we don't have fields without defaults following fields
@@ -466,12 +470,18 @@ def _init_fn(fields, frozen, has_post_init, self_name):
466470
raise TypeError(f'non-default argument {f.name!r} '
467471
'follows default argument')
468472

469-
globals = {'MISSING': MISSING,
470-
'_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY}
473+
globals = sys.modules[modname].__dict__
474+
475+
locals = {f'_type_{f.name}': f.type for f in fields}
476+
locals.update({
477+
'MISSING': MISSING,
478+
'_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY,
479+
'__builtins__': builtins,
480+
})
471481

472482
body_lines = []
473483
for f in fields:
474-
line = _field_init(f, frozen, globals, self_name)
484+
line = _field_init(f, frozen, locals, self_name)
475485
# line is None means that this field doesn't require
476486
# initialization (it's a pseudo-field). Just skip it.
477487
if line:
@@ -487,7 +497,6 @@ def _init_fn(fields, frozen, has_post_init, self_name):
487497
if not body_lines:
488498
body_lines = ['pass']
489499

490-
locals = {f'_type_{f.name}': f.type for f in fields}
491500
return _create_fn('__init__',
492501
[self_name] + [_init_param(f) for f in fields if f.init],
493502
body_lines,
@@ -877,6 +886,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
877886
# if possible.
878887
'__dataclass_self__' if 'self' in fields
879888
else 'self',
889+
cls.__module__
880890
))
881891

882892
# Get the fields as a list, and include only real fields. This is

Lib/test/dataclass_textanno.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from __future__ import annotations
2+
3+
import dataclasses
4+
5+
6+
class Foo:
7+
pass
8+
9+
10+
@dataclasses.dataclass
11+
class Bar:
12+
foo: Foo

Lib/test/test_dataclasses.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import unittest
1111
from unittest.mock import Mock
1212
from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional
13+
from typing import get_type_hints
1314
from collections import deque, OrderedDict, namedtuple
1415
from functools import total_ordering
1516

@@ -2882,6 +2883,17 @@ def test_classvar_module_level_import(self):
28822883
# won't exist on the instance.
28832884
self.assertNotIn('not_iv4', c.__dict__)
28842885

2886+
def test_text_annotations(self):
2887+
from test import dataclass_textanno
2888+
2889+
self.assertEqual(
2890+
get_type_hints(dataclass_textanno.Bar),
2891+
{'foo': dataclass_textanno.Foo})
2892+
self.assertEqual(
2893+
get_type_hints(dataclass_textanno.Bar.__init__),
2894+
{'foo': dataclass_textanno.Foo,
2895+
'return': type(None)})
2896+
28852897

28862898
class TestMakeDataclass(unittest.TestCase):
28872899
def test_simple(self):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix dataclasses to support __future__ "annotations" mode

0 commit comments

Comments
 (0)