[Python-checkins] (no subject)
Łukasz Langa
webhook-mailer at python.org
Mon Dec 9 11:07:55 EST 2019
To: python-checkins at python.org
Subject: bpo-34776: Fix dataclasses to support __future__ "annotations" mode
(GH-9518) (#17531)
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
MIME-Version: 1.0
https://github.com/python/cpython/commit/0d57db27f2c563b6433a220b646b50bdeff8=
a1f2
commit: 0d57db27f2c563b6433a220b646b50bdeff8a1f2
branch: 3.8
author: Miss Islington (bot) <31488909+miss-islington at users.noreply.github.co=
m>
committer: =C5=81ukasz Langa <lukasz at langa.pl>
date: 2019-12-09T17:07:51+01:00
summary:
bpo-34776: Fix dataclasses to support __future__ "annotations" mode (GH-9518)=
(#17531)
(cherry picked from commit d219cc4180e7589807ebbef7421879f095e72a98)
Co-authored-by: Yury Selivanov <yury at magic.io>
files:
A Lib/test/dataclass_textanno.py
A Misc/NEWS.d/next/Library/2018-09-23-14-24-37.bpo-34776.1SrQe3.rst
M Lib/dataclasses.py
M Lib/test/test_dataclasses.py
diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index 391f32e11d52a..74f79294e81bd 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -379,23 +379,24 @@ def _create_fn(name, args, body, *, globals=3DNone, loc=
als=3DNone,
# worries about external callers.
if locals is None:
locals =3D {}
- # __builtins__ may be the "builtins" module or
- # the value of its "__dict__",
- # so make sure "__builtins__" is the module.
- if globals is not None and '__builtins__' not in globals:
- globals['__builtins__'] =3D builtins
+ if 'BUILTINS' not in locals:
+ locals['BUILTINS'] =3D builtins
return_annotation =3D ''
if return_type is not MISSING:
locals['_return_type'] =3D return_type
return_annotation =3D '->_return_type'
args =3D ','.join(args)
- body =3D '\n'.join(f' {b}' for b in body)
+ body =3D '\n'.join(f' {b}' for b in body)
=20
# Compute the text of the entire function.
- txt =3D f'def {name}({args}){return_annotation}:\n{body}'
+ txt =3D f' def {name}({args}){return_annotation}:\n{body}'
=20
- exec(txt, globals, locals)
- return locals[name]
+ local_vars =3D ', '.join(locals.keys())
+ txt =3D f"def __create_fn__({local_vars}):\n{txt}\n return {name}"
+
+ ns =3D {}
+ exec(txt, globals, ns)
+ return ns['__create_fn__'](**locals)
=20
=20
def _field_assign(frozen, name, value, self_name):
@@ -406,7 +407,7 @@ def _field_assign(frozen, name, value, self_name):
# self_name is what "self" is called in this function: don't
# hard-code "self", since that might be a field name.
if frozen:
- return f'__builtins__.object.__setattr__({self_name},{name!r},{value=
})'
+ return f'BUILTINS.object.__setattr__({self_name},{name!r},{value})'
return f'{self_name}.{name}=3D{value}'
=20
=20
@@ -483,7 +484,7 @@ def _init_param(f):
return f'{f.name}:_type_{f.name}{default}'
=20
=20
-def _init_fn(fields, frozen, has_post_init, self_name):
+def _init_fn(fields, frozen, has_post_init, self_name, globals):
# fields contains both real fields and InitVar pseudo-fields.
=20
# Make sure we don't have fields without defaults following fields
@@ -501,12 +502,15 @@ def _init_fn(fields, frozen, has_post_init, self_name):
raise TypeError(f'non-default argument {f.name!r} '
'follows default argument')
=20
- globals =3D {'MISSING': MISSING,
- '_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY}
+ locals =3D {f'_type_{f.name}': f.type for f in fields}
+ locals.update({
+ 'MISSING': MISSING,
+ '_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY,
+ })
=20
body_lines =3D []
for f in fields:
- line =3D _field_init(f, frozen, globals, self_name)
+ line =3D _field_init(f, frozen, locals, self_name)
# line is None means that this field doesn't require
# initialization (it's a pseudo-field). Just skip it.
if line:
@@ -522,7 +526,6 @@ def _init_fn(fields, frozen, has_post_init, self_name):
if not body_lines:
body_lines =3D ['pass']
=20
- locals =3D {f'_type_{f.name}': f.type for f in fields}
return _create_fn('__init__',
[self_name] + [_init_param(f) for f in fields if f.ini=
t],
body_lines,
@@ -531,20 +534,19 @@ def _init_fn(fields, frozen, has_post_init, self_name):
return_type=3DNone)
=20
=20
-def _repr_fn(fields):
+def _repr_fn(fields, globals):
fn =3D _create_fn('__repr__',
('self',),
['return self.__class__.__qualname__ + f"(' +
', '.join([f"{f.name}=3D{{self.{f.name}!r}}"
for f in fields]) +
- ')"'])
+ ')"'],
+ globals=3Dglobals)
return _recursive_repr(fn)
=20
=20
-def _frozen_get_del_attr(cls, fields):
- # XXX: globals is modified on the first call to _create_fn, then
- # the modified version is used in the second call. Is this okay?
- globals =3D {'cls': cls,
+def _frozen_get_del_attr(cls, fields, globals):
+ locals =3D {'cls': cls,
'FrozenInstanceError': FrozenInstanceError}
if fields:
fields_str =3D '(' + ','.join(repr(f.name) for f in fields) + ',)'
@@ -556,17 +558,19 @@ def _frozen_get_del_attr(cls, fields):
(f'if type(self) is cls or name in {fields_str}:',
' raise FrozenInstanceError(f"cannot assign to field=
{name!r}")',
f'super(cls, self).__setattr__(name, value)'),
+ locals=3Dlocals,
globals=3Dglobals),
_create_fn('__delattr__',
('self', 'name'),
(f'if type(self) is cls or name in {fields_str}:',
' raise FrozenInstanceError(f"cannot delete field {n=
ame!r}")',
f'super(cls, self).__delattr__(name)'),
+ locals=3Dlocals,
globals=3Dglobals),
)
=20
=20
-def _cmp_fn(name, op, self_tuple, other_tuple):
+def _cmp_fn(name, op, self_tuple, other_tuple, globals):
# Create a comparison function. If the fields in the object are
# named 'x' and 'y', then self_tuple is the string
# '(self.x,self.y)' and other_tuple is the string
@@ -576,14 +580,16 @@ def _cmp_fn(name, op, self_tuple, other_tuple):
('self', 'other'),
[ 'if other.__class__ is self.__class__:',
f' return {self_tuple}{op}{other_tuple}',
- 'return NotImplemented'])
+ 'return NotImplemented'],
+ globals=3Dglobals)
=20
=20
-def _hash_fn(fields):
+def _hash_fn(fields, globals):
self_tuple =3D _tuple_str('self', fields)
return _create_fn('__hash__',
('self',),
- [f'return hash({self_tuple})'])
+ [f'return hash({self_tuple})'],
+ globals=3Dglobals)
=20
=20
def _is_classvar(a_type, typing):
@@ -756,14 +762,14 @@ def _set_new_attribute(cls, name, value):
# take. The common case is to do nothing, so instead of providing a
# function that is a no-op, use None to signify that.
=20
-def _hash_set_none(cls, fields):
+def _hash_set_none(cls, fields, globals):
return None
=20
-def _hash_add(cls, fields):
+def _hash_add(cls, fields, globals):
flds =3D [f for f in fields if (f.compare if f.hash is None else f.hash)]
- return _hash_fn(flds)
+ return _hash_fn(flds, globals)
=20
-def _hash_exception(cls, fields):
+def _hash_exception(cls, fields, globals):
# Raise an exception.
raise TypeError(f'Cannot overwrite attribute __hash__ '
f'in class {cls.__name__}')
@@ -805,6 +811,16 @@ def _process_class(cls, init, repr, eq, order, unsafe_ha=
sh, frozen):
# is defined by the base class, which is found first.
fields =3D {}
=20
+ if cls.__module__ in sys.modules:
+ globals =3D sys.modules[cls.__module__].__dict__
+ else:
+ # Theoretically this can happen if someone writes
+ # a custom string to cls.__module__. In which case
+ # such dataclass won't be fully introspectable
+ # (w.r.t. typing.get_type_hints) but will still function
+ # correctly.
+ globals =3D {}
+
setattr(cls, _PARAMS, _DataclassParams(init, repr, eq, order,
unsafe_hash, frozen))
=20
@@ -914,6 +930,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_has=
h, frozen):
# if possible.
'__dataclass_self__' if 'self' in fields
else 'self',
+ globals,
))
=20
# Get the fields as a list, and include only real fields. This is
@@ -922,7 +939,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_has=
h, frozen):
=20
if repr:
flds =3D [f for f in field_list if f.repr]
- _set_new_attribute(cls, '__repr__', _repr_fn(flds))
+ _set_new_attribute(cls, '__repr__', _repr_fn(flds, globals))
=20
if eq:
# Create _eq__ method. There's no need for a __ne__ method,
@@ -932,7 +949,8 @@ def _process_class(cls, init, repr, eq, order, unsafe_has=
h, frozen):
other_tuple =3D _tuple_str('other', flds)
_set_new_attribute(cls, '__eq__',
_cmp_fn('__eq__', '=3D=3D',
- self_tuple, other_tuple))
+ self_tuple, other_tuple,
+ globals=3Dglobals))
=20
if order:
# Create and set the ordering methods.
@@ -945,13 +963,14 @@ def _process_class(cls, init, repr, eq, order, unsafe_h=
ash, frozen):
('__ge__', '>=3D'),
]:
if _set_new_attribute(cls, name,
- _cmp_fn(name, op, self_tuple, other_tuple)=
):
+ _cmp_fn(name, op, self_tuple, other_tuple,
+ globals=3Dglobals)):
raise TypeError(f'Cannot overwrite attribute {name} '
f'in class {cls.__name__}. Consider using '
'functools.total_ordering')
=20
if frozen:
- for fn in _frozen_get_del_attr(cls, field_list):
+ for fn in _frozen_get_del_attr(cls, field_list, globals):
if _set_new_attribute(cls, fn.__name__, fn):
raise TypeError(f'Cannot overwrite attribute {fn.__name__} '
f'in class {cls.__name__}')
@@ -964,7 +983,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_has=
h, frozen):
if hash_action:
# No need to call _set_new_attribute here, since by the time
# we're here the overwriting is unconditional.
- cls.__hash__ =3D hash_action(cls, field_list)
+ cls.__hash__ =3D hash_action(cls, field_list, globals)
=20
if not getattr(cls, '__doc__'):
# Create a class doc-string.
diff --git a/Lib/test/dataclass_textanno.py b/Lib/test/dataclass_textanno.py
new file mode 100644
index 0000000000000..3eb6c943d4c43
--- /dev/null
+++ b/Lib/test/dataclass_textanno.py
@@ -0,0 +1,12 @@
+from __future__ import annotations
+
+import dataclasses
+
+
+class Foo:
+ pass
+
+
+ at dataclasses.dataclass
+class Bar:
+ foo: Foo
diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py
index b018133cf2302..2f3c531fc208a 100755
--- a/Lib/test/test_dataclasses.py
+++ b/Lib/test/test_dataclasses.py
@@ -10,6 +10,7 @@
import unittest
from unittest.mock import Mock
from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar=
, Optional
+from typing import get_type_hints
from collections import deque, OrderedDict, namedtuple
from functools import total_ordering
=20
@@ -2926,6 +2927,17 @@ def test_classvar_module_level_import(self):
# won't exist on the instance.
self.assertNotIn('not_iv4', c.__dict__)
=20
+ def test_text_annotations(self):
+ from test import dataclass_textanno
+
+ self.assertEqual(
+ get_type_hints(dataclass_textanno.Bar),
+ {'foo': dataclass_textanno.Foo})
+ self.assertEqual(
+ get_type_hints(dataclass_textanno.Bar.__init__),
+ {'foo': dataclass_textanno.Foo,
+ 'return': type(None)})
+
=20
class TestMakeDataclass(unittest.TestCase):
def test_simple(self):
diff --git a/Misc/NEWS.d/next/Library/2018-09-23-14-24-37.bpo-34776.1SrQe3.rs=
t b/Misc/NEWS.d/next/Library/2018-09-23-14-24-37.bpo-34776.1SrQe3.rst
new file mode 100644
index 0000000000000..815a4876e0b4a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2018-09-23-14-24-37.bpo-34776.1SrQe3.rst
@@ -0,0 +1 @@
+Fix dataclasses to support forward references in type annotations
More information about the Python-checkins
mailing list