From ab60fdda7fbc6d341c70de66c9724a1ddf8c9b9e Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 13 Apr 2023 08:31:03 -0700 Subject: [PATCH 1/3] [3.11] 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. (cherry picked from commit a6f95941a3d686707fb38e0f37758e666f25e180) Co-authored-by: Ethan Furman --- Doc/howto/enum.rst | 8 +++++--- Lib/enum.py | 3 ++- Lib/test/test_enum.py | 11 +++++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Doc/howto/enum.rst b/Doc/howto/enum.rst index 32310692fe56ed..dbd8d4fa9231ee 100644 --- a/Doc/howto/enum.rst +++ b/Doc/howto/enum.rst @@ -837,17 +837,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 84ae339d0d4020..0ef6559b3c6bd6 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -981,6 +981,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: @@ -993,7 +994,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__: if isinstance(base, EnumType): continue data_types.add(candidate or base) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 51dd66bea0c42c..2d615edee580d2 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -2667,13 +2667,14 @@ class Foo: a: int 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), '') - 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 @@ -2682,9 +2683,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): @@ -2790,6 +2791,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): From 102b7f8ac6defe7c16d6f438922ced74cf3a7bd1 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 13 Apr 2023 09:58:19 -0700 Subject: [PATCH 2/3] remove mention of dataclass --- Doc/howto/enum.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Doc/howto/enum.rst b/Doc/howto/enum.rst index dbd8d4fa9231ee..55f0dfb4c48c38 100644 --- a/Doc/howto/enum.rst +++ b/Doc/howto/enum.rst @@ -837,8 +837,7 @@ 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. A ``data type`` is a mixin that defines :meth:`__new__`, or a - :class:`~dataclasses.dataclass` +5. A ``data type`` is a mixin that defines :meth:`__new__`. 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. From faff581517e19529986b44577203fe55f6f9be10 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 13 Apr 2023 10:58:11 -0700 Subject: [PATCH 3/3] restore mention of dataclass --- Lib/enum.py | 4 ++-- Lib/test/test_enum.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 0ef6559b3c6bd6..5b175820972f4c 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -981,7 +981,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 + # a datatype has a __new__ method data_types = set() base_chain = set() for chain in bases: @@ -994,7 +994,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__: + elif '__new__' in base.__dict__ or '__dataclass_fields__' in base.__dict__: if isinstance(base, EnumType): continue data_types.add(candidate or base) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 2d615edee580d2..397bb849d00aba 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -2667,8 +2667,6 @@ class Foo: a: int 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) @@ -2679,7 +2677,7 @@ class Foo: def __init__(self, a): self.a = a def __repr__(self): - return f'Foo(a={self.a!r})' + return 'Foo(a=%r)' % self._value_ class Entries(Foo, Enum): ENTRY1 = 1 #