[Python-checkins] (no subject)

Łukasz Langa webhook-mailer at python.org
Mon Dec 9 11:07:59 EST 2019


To: python-checkins at python.org
Subject: bpo-34776: Fix dataclasses to support __future__ "annotations" mode
 (GH-9518) (#17532)
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
MIME-Version: 1.0
https://github.com/python/cpython/commit/66d7a5d58a88bce312bc4668f2cc54c9488c=
5bd8
commit: 66d7a5d58a88bce312bc4668f2cc54c9488c5bd8
branch: 3.7
author: Miss Islington (bot) <31488909+miss-islington at users.noreply.github.co=
m>
committer: =C5=81ukasz Langa <lukasz at langa.pl>
date: 2019年12月09日T17:07:54+01:00
summary:
bpo-34776: Fix dataclasses to support __future__ "annotations" mode (GH-9518)=
 (#17532)
(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 33e26460c74ea..146468ba6c06c 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -368,23 +368,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):
@@ -395,7 +396,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
@@ -472,7 +473,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
@@ -490,12 +491,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:
@@ -511,7 +515,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,
@@ -520,20 +523,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) + ',)'
@@ -545,17 +547,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
@@ -565,14 +569,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):
@@ -744,14 +750,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__}')
@@ -793,6 +799,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
@@ -902,6 +918,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
@@ -910,7 +927,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,
@@ -920,7 +937,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.
@@ -933,13 +951,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__}')
@@ -952,7 +971,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 99086e5f6d254..284484547c814 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
@@ -2918,6 +2919,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

AltStyle によって変換されたページ (->オリジナル) /