From 82802f6addd45fba6100fa01639387944b7d2e02 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 7 Mar 2024 13:30:26 -0800 Subject: [PATCH 1/2] gh-116040: [Enum] fix by-value calls when second value is falsey; e.g. Cardinal(1, 0) (GH-116072) (cherry picked from commit 13ffd4bd9f529b6a5fe33741fbd57f14b4b80137) --- Lib/enum.py | 15 ++- Lib/test/test_enum.py | 93 +++++++++++++++++++ ...-02-28-13-10-17.gh-issue-116040.wDidHd.rst | 1 + 3 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-02-28-13-10-17.gh-issue-116040.wDidHd.rst diff --git a/Lib/enum.py b/Lib/enum.py index 6d3bbdc31625f7..110fbda4ddf5ad 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -166,6 +166,13 @@ def _dedent(text): lines[j] = l[i:] return '\n'.join(lines) +class _not_given: + def __repr__(self): + return('') + def __bool__(self): + return False +_not_given = _not_given() + class _auto_null: def __repr__(self): return '_auto_null' @@ -718,7 +725,7 @@ def __bool__(cls): """ return True - def __call__(cls, value, names=None, *values, module=None, qualname=None, type=None, start=1, boundary=None): + def __call__(cls, value, names=_not_given, *values, module=None, qualname=None, type=None, start=1, boundary=None): """ Either returns an existing member, or creates a new enum class. @@ -747,18 +754,18 @@ def __call__(cls, value, names=None, *values, module=None, qualname=None, type=N """ if cls._member_map_: # simple value lookup if members exist - if names: + if names is not _not_given: value = (value, names) + values return cls.__new__(cls, value) # otherwise, functional API: we're creating a new Enum type - if names is None and type is None: + if names is _not_given and type is None: # no body? no data-type? possibly wrong usage raise TypeError( f"{cls} has no members; specify `names=()` if you meant to create a new, empty, enum" ) return cls._create_( class_name=value, - names=names, + names=names or None, module=module, qualname=qualname, type=type, diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index c58dc36fe84134..8c97208b4e05df 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -3312,6 +3312,99 @@ def __new__(cls, value): member._value_ = Base(value) return member + def test_extra_member_creation(self): + class IDEnumMeta(EnumMeta): + def __new__(metacls, cls, bases, classdict, **kwds): + # add new entries to classdict + for name in classdict.member_names: + classdict[f'{name}_DESC'] = f'-{classdict[name]}' + return super().__new__(metacls, cls, bases, classdict, **kwds) + class IDEnum(StrEnum, metaclass=IDEnumMeta): + pass + class MyEnum(IDEnum): + ID = 'id' + NAME = 'name' + self.assertEqual(list(MyEnum), [MyEnum.ID, MyEnum.NAME, MyEnum.ID_DESC, MyEnum.NAME_DESC]) + + def test_add_alias(self): + class mixin: + @property + def ORG(self): + return 'huh' + class Color(mixin, Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + Color.RED._add_alias_('ROJO') + self.assertIs(Color.RED, Color['ROJO']) + self.assertIs(Color.RED, Color.ROJO) + Color.BLUE._add_alias_('ORG') + self.assertIs(Color.BLUE, Color['ORG']) + self.assertIs(Color.BLUE, Color.ORG) + self.assertEqual(Color.RED.ORG, 'huh') + self.assertEqual(Color.GREEN.ORG, 'huh') + self.assertEqual(Color.BLUE.ORG, 'huh') + self.assertEqual(Color.ORG.ORG, 'huh') + + def test_add_value_alias_after_creation(self): + class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + Color.RED._add_value_alias_(5) + self.assertIs(Color.RED, Color(5)) + + def test_add_value_alias_during_creation(self): + class Types(Enum): + Unknown = 0, + Source = 1, 'src' + NetList = 2, 'nl' + def __new__(cls, int_value, *value_aliases): + member = object.__new__(cls) + member._value_ = int_value + for alias in value_aliases: + member._add_value_alias_(alias) + return member + self.assertIs(Types(0), Types.Unknown) + self.assertIs(Types(1), Types.Source) + self.assertIs(Types('src'), Types.Source) + self.assertIs(Types(2), Types.NetList) + self.assertIs(Types('nl'), Types.NetList) + + def test_second_tuple_item_is_falsey(self): + class Cardinal(Enum): + RIGHT = (1, 0) + UP = (0, 1) + LEFT = (-1, 0) + DOWN = (0, -1) + self.assertIs(Cardinal(1, 0), Cardinal.RIGHT) + self.assertIs(Cardinal(-1, 0), Cardinal.LEFT) + + def test_no_members(self): + with self.assertRaisesRegex( + TypeError, + 'has no members', + ): + Enum(7) + with self.assertRaisesRegex( + TypeError, + 'has no members', + ): + Flag(7) + + def test_empty_names(self): + for nothing, e_type in ( + ('', None), + ('', int), + ([], None), + ([], int), + ({}, None), + ({}, int), + ): + empty_enum = Enum('empty_enum', nothing, type=e_type) + self.assertEqual(len(empty_enum), 0) + self.assertRaises(TypeError, 'has no members', empty_enum, 0) + class TestOrder(unittest.TestCase): "test usage of the `_order_` attribute" diff --git a/Misc/NEWS.d/next/Library/2024-02-28-13-10-17.gh-issue-116040.wDidHd.rst b/Misc/NEWS.d/next/Library/2024-02-28-13-10-17.gh-issue-116040.wDidHd.rst new file mode 100644 index 00000000000000..907b58b3a5c206 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-02-28-13-10-17.gh-issue-116040.wDidHd.rst @@ -0,0 +1 @@ +[Enum] fix by-value calls when second value is falsey; e.g. Cardinal(1, 0) From 12cba0573754691977621a035726c8544deac8ad Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 7 Mar 2024 14:41:37 -0800 Subject: [PATCH 2/2] remove 3.13 tests --- Lib/test/test_enum.py | 59 ------------------------------------------- 1 file changed, 59 deletions(-) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 8c97208b4e05df..d5e9444bdcb07b 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -3312,65 +3312,6 @@ def __new__(cls, value): member._value_ = Base(value) return member - def test_extra_member_creation(self): - class IDEnumMeta(EnumMeta): - def __new__(metacls, cls, bases, classdict, **kwds): - # add new entries to classdict - for name in classdict.member_names: - classdict[f'{name}_DESC'] = f'-{classdict[name]}' - return super().__new__(metacls, cls, bases, classdict, **kwds) - class IDEnum(StrEnum, metaclass=IDEnumMeta): - pass - class MyEnum(IDEnum): - ID = 'id' - NAME = 'name' - self.assertEqual(list(MyEnum), [MyEnum.ID, MyEnum.NAME, MyEnum.ID_DESC, MyEnum.NAME_DESC]) - - def test_add_alias(self): - class mixin: - @property - def ORG(self): - return 'huh' - class Color(mixin, Enum): - RED = 1 - GREEN = 2 - BLUE = 3 - Color.RED._add_alias_('ROJO') - self.assertIs(Color.RED, Color['ROJO']) - self.assertIs(Color.RED, Color.ROJO) - Color.BLUE._add_alias_('ORG') - self.assertIs(Color.BLUE, Color['ORG']) - self.assertIs(Color.BLUE, Color.ORG) - self.assertEqual(Color.RED.ORG, 'huh') - self.assertEqual(Color.GREEN.ORG, 'huh') - self.assertEqual(Color.BLUE.ORG, 'huh') - self.assertEqual(Color.ORG.ORG, 'huh') - - def test_add_value_alias_after_creation(self): - class Color(Enum): - RED = 1 - GREEN = 2 - BLUE = 3 - Color.RED._add_value_alias_(5) - self.assertIs(Color.RED, Color(5)) - - def test_add_value_alias_during_creation(self): - class Types(Enum): - Unknown = 0, - Source = 1, 'src' - NetList = 2, 'nl' - def __new__(cls, int_value, *value_aliases): - member = object.__new__(cls) - member._value_ = int_value - for alias in value_aliases: - member._add_value_alias_(alias) - return member - self.assertIs(Types(0), Types.Unknown) - self.assertIs(Types(1), Types.Source) - self.assertIs(Types('src'), Types.Source) - self.assertIs(Types(2), Types.NetList) - self.assertIs(Types('nl'), Types.NetList) - def test_second_tuple_item_is_falsey(self): class Cardinal(Enum): RIGHT = (1, 0)