From a6f95941a3d686707fb38e0f37758e666f25e180 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 13 Apr 2023 08:31:03 -0700 Subject: [PATCH] gh-103479: [Enum] require __new__ to be considered a data type (GH-103495) a mixin must either have a __new__ method, or be a dataclass, to be interpreted as a data-type --- Doc/howto/enum.rst | 8 +++++--- Lib/enum.py | 3 ++- Lib/test/test_enum.py | 13 +++++++------ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Doc/howto/enum.rst b/Doc/howto/enum.rst index 9390faded2fb8d..56391a026cf889 100644 --- a/Doc/howto/enum.rst +++ b/Doc/howto/enum.rst @@ -865,17 +865,19 @@ Some rules: 4. When another data type is mixed in, the :attr:`value` attribute is *not the same* as the enum member itself, although it is equivalent and will compare equal. -5. %-style formatting: ``%s`` and ``%r`` call the :class:`Enum` class's +5. A ``data type`` is a mixin that defines :meth:`__new__`, or a + :class:`~dataclasses.dataclass` +6. %-style formatting: ``%s`` and ``%r`` call the :class:`Enum` class's :meth:`__str__` and :meth:`__repr__` respectively; other codes (such as ``%i`` or ``%h`` for IntEnum) treat the enum member as its mixed-in type. -6. :ref:`Formatted string literals `, :meth:`str.format`, +7. :ref:`Formatted string literals `, :meth:`str.format`, and :func:`format` will use the enum's :meth:`__str__` method. .. note:: Because :class:`IntEnum`, :class:`IntFlag`, and :class:`StrEnum` are designed to be drop-in replacements for existing constants, their - :meth:`__str__` method has been reset to their data types + :meth:`__str__` method has been reset to their data types' :meth:`__str__` method. When to use :meth:`__new__` vs. :meth:`__init__` diff --git a/Lib/enum.py b/Lib/enum.py index 432d7456b4b9f1..e9f224a303d3e5 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -967,6 +967,7 @@ def _find_data_repr_(mcls, class_name, bases): @classmethod def _find_data_type_(mcls, class_name, bases): + # a datatype has a __new__ method, or a __dataclass_fields__ attribute data_types = set() base_chain = set() for chain in bases: @@ -979,7 +980,7 @@ def _find_data_type_(mcls, class_name, bases): if base._member_type_ is not object: data_types.add(base._member_type_) break - elif '__new__' in base.__dict__ or '__init__' in base.__dict__: + elif '__new__' in base.__dict__ or '__dataclass_fields__' in base.__dict__: data_types.add(candidate or base) break else: diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 89294e95df2a83..e9dfcf8586a823 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -2737,10 +2737,10 @@ def __repr__(self): return 'ha hah!' class Entries(Foo, Enum): ENTRY1 = 1 + self.assertEqual(repr(Entries.ENTRY1), '') + self.assertTrue(Entries.ENTRY1.value == Foo(1), Entries.ENTRY1.value) self.assertTrue(isinstance(Entries.ENTRY1, Foo)) self.assertTrue(Entries._member_type_ is Foo, Entries._member_type_) - self.assertTrue(Entries.ENTRY1.value == Foo(1), Entries.ENTRY1.value) - self.assertEqual(repr(Entries.ENTRY1), '') # # check auto-generated dataclass __repr__ is not used # @@ -2787,8 +2787,7 @@ class Creature(CreatureDataMixin, Enum): DOG = ('medium', 4) self.assertRegex(repr(Creature.DOG), "") - def test_repr_with_init_data_type_mixin(self): - # non-data_type is a mixin that doesn't define __new__ + def test_repr_with_init_mixin(self): class Foo: def __init__(self, a): self.a = a @@ -2797,9 +2796,9 @@ def __repr__(self): class Entries(Foo, Enum): ENTRY1 = 1 # - self.assertEqual(repr(Entries.ENTRY1), '') + self.assertEqual(repr(Entries.ENTRY1), 'Foo(a=1)') - def test_repr_and_str_with_non_data_type_mixin(self): + def test_repr_and_str_with_no_init_mixin(self): # non-data_type is a mixin that doesn't define __new__ class Foo: def __repr__(self): @@ -2911,6 +2910,8 @@ def __new__(cls, c): def test_init_exception(self): class Base: + def __new__(cls, *args): + return object.__new__(cls) def __init__(self, x): raise ValueError("I don't like", x) with self.assertRaises(TypeError):