[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