From 8b906cb66a4f7de2c3b27d89ae002feb0a95cdc0 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 18 Feb 2023 14:31:23 -0800 Subject: [PATCH 1/3] gh-98886: Fix issues with fields with special underscore names This commit prefixes `__dataclass` to several things in the locals dict: - Names like _dflt_ (which cause trouble, see first test) - Names like _type_ (not known to be able to cause trouble) - _return_type (not known to able to cause trouble) - _HAS_DEFAULT_FACTORY (which causes trouble, see second test) In addition, this removes `MISSING` from the locals dict. As far as I can tell, this wasn't needed even in the initial implementation of dataclasses.py (and tests on that version passed with it removed). This is basically a continuation of #96151, where fixing this was welcomed in https://github.com/python/cpython/pull/98143#issuecomment-1280306360 --- Lib/dataclasses.py | 19 +++++++++---------- Lib/test/test_dataclasses.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 5c0257eba186d1..8e8ccbb97d0387 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -433,8 +433,8 @@ def _create_fn(name, args, body, *, globals=None, locals=None, locals = {} return_annotation = '' if return_type is not MISSING: - locals['_return_type'] = return_type - return_annotation = '->_return_type' + locals['__dataclass_return_type'] = return_type + return_annotation = '->__dataclass_return_type' args = ','.join(args) body = '\n'.join(f' {b}' for b in body) @@ -468,14 +468,14 @@ def _field_init(f, frozen, globals, self_name, slots): # Return the text of the line in the body of __init__ that will # initialize this field. - default_name = f'_dflt_{f.name}' + default_name = f'__dataclass_dflt_{f.name}' if f.default_factory is not MISSING: if f.init: # This field has a default factory. If a parameter is # given, use it. If not, call the factory. globals[default_name] = f.default_factory value = (f'{default_name}() ' - f'if {f.name} is _HAS_DEFAULT_FACTORY ' + f'if {f.name} is __dataclass_HAS_DEFAULT_FACTORY ' f'else {f.name}') else: # This is a field that's not in the __init__ params, but @@ -536,11 +536,11 @@ def _init_param(f): elif f.default is not MISSING: # There's a default, this will be the name that's used to look # it up. - default = f'=_dflt_{f.name}' + default = f'=__dataclass_dflt_{f.name}' elif f.default_factory is not MISSING: # There's a factory function. Set a marker. - default = '=_HAS_DEFAULT_FACTORY' - return f'{f.name}:_type_{f.name}{default}' + default = '=__dataclass_HAS_DEFAULT_FACTORY' + return f'{f.name}:__dataclass_type_{f.name}{default}' def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init, @@ -563,10 +563,9 @@ def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init, raise TypeError(f'non-default argument {f.name!r} ' 'follows default argument') - locals = {f'_type_{f.name}': f.type for f in fields} + locals = {f'__dataclass_type_{f.name}': f.type for f in fields} locals.update({ - 'MISSING': MISSING, - '_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY, + '__dataclass_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY, '__dataclass_builtins_object__': object, }) diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 81a36aa241acf7..e773a62034aa7b 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -283,6 +283,23 @@ class C: c = C(5) self.assertEqual(c.BUILTINS, 5) + def test_field_with_special_single_underscore_names(self): + # gh-98886 + + @dataclass + class X: + x: int = field(default_factory=lambda: 111) + _dflt_x: int = field(default_factory=lambda: 222) + + X() + + @dataclass + class Y: + y: int = field(default_factory=lambda: 111) + _HAS_DEFAULT_FACTORY: int = 222 + + assert Y(y=222).y == 222 + def test_field_named_like_builtin(self): # Attribute names can shadow built-in names # since code generation is used. From f6ad0f447e87b8bca5b5684f60d322b76230be0e Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 18 Feb 2023 23:03:52 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2023-02-18-23-03-50.gh-issue-98886.LkKGWv.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2023-02-18-23-03-50.gh-issue-98886.LkKGWv.rst diff --git a/Misc/NEWS.d/next/Library/2023-02-18-23-03-50.gh-issue-98886.LkKGWv.rst b/Misc/NEWS.d/next/Library/2023-02-18-23-03-50.gh-issue-98886.LkKGWv.rst new file mode 100644 index 00000000000000..64e4d6eed2f62d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-02-18-23-03-50.gh-issue-98886.LkKGWv.rst @@ -0,0 +1 @@ +Fix issues when defining dataclasses that have fields with specific underscore names that aren't clearly reserved by :mod:`dataclasses`. From c1d22c72226c07518de5842311f79d687ac5de8e Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 8 Mar 2023 15:30:43 -0800 Subject: [PATCH 3/3] use dunder names --- Lib/dataclasses.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 8e8ccbb97d0387..1cf321b1d811b1 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -433,8 +433,8 @@ def _create_fn(name, args, body, *, globals=None, locals=None, locals = {} return_annotation = '' if return_type is not MISSING: - locals['__dataclass_return_type'] = return_type - return_annotation = '->__dataclass_return_type' + locals['__dataclass_return_type__'] = return_type + return_annotation = '->__dataclass_return_type__' args = ','.join(args) body = '\n'.join(f' {b}' for b in body) @@ -468,14 +468,14 @@ def _field_init(f, frozen, globals, self_name, slots): # Return the text of the line in the body of __init__ that will # initialize this field. - default_name = f'__dataclass_dflt_{f.name}' + default_name = f'__dataclass_dflt_{f.name}__' if f.default_factory is not MISSING: if f.init: # This field has a default factory. If a parameter is # given, use it. If not, call the factory. globals[default_name] = f.default_factory value = (f'{default_name}() ' - f'if {f.name} is __dataclass_HAS_DEFAULT_FACTORY ' + f'if {f.name} is __dataclass_HAS_DEFAULT_FACTORY__ ' f'else {f.name}') else: # This is a field that's not in the __init__ params, but @@ -536,11 +536,11 @@ def _init_param(f): elif f.default is not MISSING: # There's a default, this will be the name that's used to look # it up. - default = f'=__dataclass_dflt_{f.name}' + default = f'=__dataclass_dflt_{f.name}__' elif f.default_factory is not MISSING: # There's a factory function. Set a marker. - default = '=__dataclass_HAS_DEFAULT_FACTORY' - return f'{f.name}:__dataclass_type_{f.name}{default}' + default = '=__dataclass_HAS_DEFAULT_FACTORY__' + return f'{f.name}:__dataclass_type_{f.name}__{default}' def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init, @@ -563,9 +563,9 @@ def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init, raise TypeError(f'non-default argument {f.name!r} ' 'follows default argument') - locals = {f'__dataclass_type_{f.name}': f.type for f in fields} + locals = {f'__dataclass_type_{f.name}__': f.type for f in fields} locals.update({ - '__dataclass_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY, + '__dataclass_HAS_DEFAULT_FACTORY__': _HAS_DEFAULT_FACTORY, '__dataclass_builtins_object__': object, })