diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 7a24f8a9e5ccee..75861ee908593b 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1009,6 +1009,10 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, # Otherwise it's a field of some type. cls_fields.append(_get_field(cls, name, type, kw_only)) + # Test whether '__init__' is to be auto-generated or if + # it is provided explicitly by the user. + has_init_method = init or '__init__' in cls.__dict__ + for f in cls_fields: fields[f.name] = f @@ -1018,6 +1022,15 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, # sees a real default value, not a Field. if isinstance(getattr(cls, f.name, None), Field): if f.default is MISSING: + # https://github.com/python/cpython/issues/89529 + if f.default_factory is not MISSING and not has_init_method: + raise ValueError( + f'specifying default_factory for {f.name!r}' + f' requires the @dataclass decorator to be' + f' called with init=True or to implement' + f' an __init__ method' + ) + # If there's no default, delete the class attribute. # This happens if we specify field(repr=False), for # example (that is, we specified a field object, but diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 2e6c49e29ce828..48e95f3cb8df1d 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -9,6 +9,7 @@ import pickle import inspect import builtins +import re import types import weakref import traceback @@ -18,6 +19,7 @@ from typing import get_type_hints from collections import deque, OrderedDict, namedtuple, defaultdict from copy import deepcopy +from itertools import product from functools import total_ordering, wraps import typing # Needed for the string "typing.ClassVar[int]" to work as an annotation. @@ -1413,6 +1415,71 @@ class C: C().x self.assertEqual(factory.call_count, 2) + def test_default_factory_and_init_method_interaction(self): + # See https://github.com/python/cpython/issues/89529. + + @dataclass + class BaseWithInit: + x: list + + @dataclass(slots=True) + class BaseWithSlots: + x: list + + @dataclass(init=False) + class BaseWithOutInit: + x: list + + @dataclass(init=False, slots=True) + class BaseWithOutInitWithSlots: + x: list + + err = re.escape( + "specifying default_factory for 'x' requires the " + "@dataclass decorator to be called with init=True " + "or to implement an __init__ method" + ) + + for base_class, slots, field_init in product( + (object, BaseWithInit, BaseWithSlots, + BaseWithOutInit, BaseWithOutInitWithSlots), + (True, False), + (True, False), + ): + with self.subTest('generated __init__', base_class=base_class, + init=True, slots=slots, field_init=field_init): + @dataclass(init=True, slots=slots) + class C(base_class): + x: list = field(init=field_init, default_factory=list) + self.assertListEqual(C().x, []) + + with self.subTest('user-defined __init__', base_class=base_class, + init=True, slots=slots, field_init=field_init): + @dataclass(init=True, slots=slots) + class C(base_class): + x: list = field(init=field_init, default_factory=list) + def __init__(self, *a, **kw): + # deliberately use something else + self.x = 'hello' + self.assertEqual(C().x, 'hello') + + with self.subTest('no generated __init__', base_class=base_class, + init=False, slots=slots, field_init=field_init): + with self.assertRaisesRegex(ValueError, err): + @dataclass(init=False, slots=slots) + class C(base_class): + x: list = field(init=field_init, default_factory=list) + + with self.subTest('user-defined __init__', base_class=base_class, + init=False, slots=slots, field_init=field_init): + @dataclass(init=False, slots=slots) + class C(base_class): + x: list = field(init=field_init, default_factory=list) + def __init__(self, *a, **kw): + # deliberately use something else + self.x = 'world' + self.assertEqual(C().x, 'world') + def test_default_factory_not_called_if_value_given(self): # We need a factory that we can test if it's been called. factory = Mock() diff --git a/Misc/NEWS.d/next/Library/2024-08-16-15-35-57.gh-issue-89529.ayfZ1n.rst b/Misc/NEWS.d/next/Library/2024-08-16-15-35-57.gh-issue-89529.ayfZ1n.rst new file mode 100644 index 00000000000000..e34859d1cf26f3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-08-16-15-35-57.gh-issue-89529.ayfZ1n.rst @@ -0,0 +1,2 @@ +Disallow ``default_factory`` for dataclass fields if the dataclass does not +have an ``__init__`` method. Patch by Bénédikt Tran. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy