From b947792f9a7f9563d262d802c484658ef029d816 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 6 Jun 2023 11:52:05 +0300 Subject: [PATCH 1/2] gh-105332: Fix unpickling `enum.Flag` with specific values --- Lib/enum.py | 7 +++---- Lib/test/test_enum.py | 18 ++++++++++++++++++ ...3-06-06-11-50-33.gh-issue-105332.tmpgRA.rst | 1 + 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-06-06-11-50-33.gh-issue-105332.tmpgRA.rst diff --git a/Lib/enum.py b/Lib/enum.py index b50fe50d8258d3..beb280c659e926 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1308,6 +1308,8 @@ class Flag(Enum, boundary=STRICT): def __reduce_ex__(self, proto): cls = self.__class__ + if self._value_ in cls._value2member_map_: + return cls, (self._value_,) unknown = self._value_ & ~cls._flag_mask_ member_value = self._value_ & cls._flag_mask_ if unknown and member_value: @@ -1318,10 +1320,7 @@ def __reduce_ex__(self, proto): return _or_, (cls(rest), cls._value2member_map_.get(val)) else: break - if self._name_ is None: - return cls, (self._value_,) - else: - return getattr, (cls, self._name_) + return cls, (self._value_,) _numeric_repr_ = repr diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index fe701025b70f48..702498cb719c2b 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -66,6 +66,7 @@ class FlagStooges(Flag): LARRY = 1 CURLY = 2 MOE = 4 + BIG = 389 except Exception as exc: FlagStooges = exc @@ -74,17 +75,20 @@ class FlagStoogesWithZero(Flag): LARRY = 1 CURLY = 2 MOE = 4 + BIG = 389 class IntFlagStooges(IntFlag): LARRY = 1 CURLY = 2 MOE = 4 + BIG = 389 class IntFlagStoogesWithZero(IntFlag): NOFLAG = 0 LARRY = 1 CURLY = 2 MOE = 4 + BIG = 389 # for pickle test and subclass tests class Name(StrEnum): @@ -3252,11 +3256,17 @@ def test_pickle(self): test_pickle_dump_load(self.assertEqual, FlagStooges.CURLY&~FlagStooges.CURLY) test_pickle_dump_load(self.assertIs, FlagStooges) + test_pickle_dump_load(self.assertEqual, FlagStooges.BIG) + test_pickle_dump_load(self.assertEqual, + FlagStooges.CURLY|FlagStooges.BIG) test_pickle_dump_load(self.assertIs, FlagStoogesWithZero.CURLY) test_pickle_dump_load(self.assertEqual, FlagStoogesWithZero.CURLY|FlagStoogesWithZero.MOE) test_pickle_dump_load(self.assertIs, FlagStoogesWithZero.NOFLAG) + test_pickle_dump_load(self.assertEqual, FlagStoogesWithZero.BIG) + test_pickle_dump_load(self.assertEqual, + FlagStoogesWithZero.CURLY|FlagStoogesWithZero.BIG) test_pickle_dump_load(self.assertIs, IntFlagStooges.CURLY) test_pickle_dump_load(self.assertEqual, @@ -3266,11 +3276,19 @@ def test_pickle(self): test_pickle_dump_load(self.assertEqual, IntFlagStooges(0)) test_pickle_dump_load(self.assertEqual, IntFlagStooges(0x30)) test_pickle_dump_load(self.assertIs, IntFlagStooges) + test_pickle_dump_load(self.assertEqual, IntFlagStooges.BIG) + test_pickle_dump_load(self.assertEqual, IntFlagStooges.BIG|1) + test_pickle_dump_load(self.assertEqual, + IntFlagStooges.CURLY|IntFlagStooges.BIG) test_pickle_dump_load(self.assertIs, IntFlagStoogesWithZero.CURLY) test_pickle_dump_load(self.assertEqual, IntFlagStoogesWithZero.CURLY|IntFlagStoogesWithZero.MOE) test_pickle_dump_load(self.assertIs, IntFlagStoogesWithZero.NOFLAG) + test_pickle_dump_load(self.assertEqual, IntFlagStoogesWithZero.BIG) + test_pickle_dump_load(self.assertEqual, IntFlagStoogesWithZero.BIG|1) + test_pickle_dump_load(self.assertEqual, + IntFlagStoogesWithZero.CURLY|IntFlagStoogesWithZero.BIG) def test_contains_tf(self): Open = self.Open diff --git a/Misc/NEWS.d/next/Library/2023-06-06-11-50-33.gh-issue-105332.tmpgRA.rst b/Misc/NEWS.d/next/Library/2023-06-06-11-50-33.gh-issue-105332.tmpgRA.rst new file mode 100644 index 00000000000000..f48c4b33d5357e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-06-06-11-50-33.gh-issue-105332.tmpgRA.rst @@ -0,0 +1 @@ +Fix loading pickled data for :class:`enum.Flag` with specific values. From 33f10aa9eb27761b7a331e6c96817dcf4a982de9 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 7 Jun 2023 23:49:47 -0700 Subject: [PATCH 2/2] revert enum pickling from by-name to by-value --- Doc/howto/enum.rst | 11 ++++++- Lib/enum.py | 29 ++++++------------- Lib/test/test_enum.py | 10 ++++++- ...-06-06-11-50-33.gh-issue-105332.tmpgRA.rst | 2 +- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/Doc/howto/enum.rst b/Doc/howto/enum.rst index 68b75c529e92c7..4312b4c8140f5c 100644 --- a/Doc/howto/enum.rst +++ b/Doc/howto/enum.rst @@ -517,7 +517,16 @@ from that module. nested in other classes. It is possible to modify how enum members are pickled/unpickled by defining -:meth:`__reduce_ex__` in the enumeration class. +:meth:`__reduce_ex__` in the enumeration class. The default method is by-value, +but enums with complicated values may want to use by-name:: + + >>> class MyEnum(Enum): + ... __reduce_ex__ = enum.pickle_by_enum_name + +.. note:: + + Using by-name for flags is not recommended, as unnamed aliases will + not unpickle. Functional API diff --git a/Lib/enum.py b/Lib/enum.py index beb280c659e926..a22bcca4fd87a3 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -12,6 +12,7 @@ 'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP', 'global_flag_repr', 'global_enum_repr', 'global_str', 'global_enum', 'EnumCheck', 'CONTINUOUS', 'NAMED_FLAGS', 'UNIQUE', + 'pickle_by_global_name', 'pickle_by_enum_name', ] @@ -913,7 +914,6 @@ def _convert_(cls, name, module, filter, source=None, *, boundary=None, as_globa body['__module__'] = module tmp_cls = type(name, (object, ), body) cls = _simple_enum(etype=cls, boundary=boundary or KEEP)(tmp_cls) - cls.__reduce_ex__ = _reduce_ex_by_global_name if as_global: global_enum(cls) else: @@ -1216,7 +1216,7 @@ def __hash__(self): return hash(self._name_) def __reduce_ex__(self, proto): - return getattr, (self.__class__, self._name_) + return self.__class__, (self._value_, ) # enum.property is used to provide access to the `name` and # `value` attributes of enum members while keeping some measure of @@ -1283,8 +1283,14 @@ def _generate_next_value_(name, start, count, last_values): return name.lower() -def _reduce_ex_by_global_name(self, proto): +def pickle_by_global_name(self, proto): + # should not be used with Flag-type enums return self.name +_reduce_ex_by_global_name = pickle_by_global_name + +def pickle_by_enum_name(self, proto): + # should not be used with Flag-type enums + return getattr, (self.__class__, self._name_) class FlagBoundary(StrEnum): """ @@ -1306,22 +1312,6 @@ class Flag(Enum, boundary=STRICT): Support for flags """ - def __reduce_ex__(self, proto): - cls = self.__class__ - if self._value_ in cls._value2member_map_: - return cls, (self._value_,) - unknown = self._value_ & ~cls._flag_mask_ - member_value = self._value_ & cls._flag_mask_ - if unknown and member_value: - return _or_, (cls(member_value), unknown) - for val in _iter_bits_lsb(member_value): - rest = member_value & ~val - if rest: - return _or_, (cls(rest), cls._value2member_map_.get(val)) - else: - break - return cls, (self._value_,) - _numeric_repr_ = repr @staticmethod @@ -2048,7 +2038,6 @@ def _old_convert_(etype, name, module, filter, source=None, *, boundary=None): # unless some values aren't comparable, in which case sort by name members.sort(key=lambda t: t[0]) cls = etype(name, members, module=module, boundary=boundary or KEEP) - cls.__reduce_ex__ = _reduce_ex_by_global_name return cls _stdlib_enums = IntEnum, StrEnum, IntFlag diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 702498cb719c2b..02dc7c1ff6837c 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -31,6 +31,11 @@ def load_tests(loader, tests, ignore): '../../Doc/library/enum.rst', optionflags=doctest.ELLIPSIS|doctest.NORMALIZE_WHITESPACE, )) + if os.path.exists('Doc/howto/enum.rst'): + tests.addTests(doctest.DocFileSuite( + '../../Doc/howto/enum.rst', + optionflags=doctest.ELLIPSIS|doctest.NORMALIZE_WHITESPACE, + )) return tests MODULE = __name__ @@ -1946,7 +1951,6 @@ class NEI(NamedInt, Enum): __qualname__ = 'NEI' x = ('the-x', 1) y = ('the-y', 2) - self.assertIs(NEI.__new__, Enum.__new__) self.assertEqual(repr(NEI.x + NEI.y), "NamedInt('(the-x + the-y)', 3)") globals()['NamedInt'] = NamedInt @@ -1954,6 +1958,10 @@ class NEI(NamedInt, Enum): NI5 = NamedInt('test', 5) self.assertEqual(NI5, 5) self.assertEqual(NEI.y.value, 2) + with self.assertRaisesRegex(TypeError, "name and value must be specified"): + test_pickle_dump_load(self.assertIs, NEI.y) + # fix pickle support and try again + NEI.__reduce_ex__ = enum.pickle_by_enum_name test_pickle_dump_load(self.assertIs, NEI.y) test_pickle_dump_load(self.assertIs, NEI) diff --git a/Misc/NEWS.d/next/Library/2023-06-06-11-50-33.gh-issue-105332.tmpgRA.rst b/Misc/NEWS.d/next/Library/2023-06-06-11-50-33.gh-issue-105332.tmpgRA.rst index f48c4b33d5357e..31b6855a6ebfad 100644 --- a/Misc/NEWS.d/next/Library/2023-06-06-11-50-33.gh-issue-105332.tmpgRA.rst +++ b/Misc/NEWS.d/next/Library/2023-06-06-11-50-33.gh-issue-105332.tmpgRA.rst @@ -1 +1 @@ -Fix loading pickled data for :class:`enum.Flag` with specific values. +Revert pickling method from by-name back to by-value.