Skip to content

Commit 2f4a2d6

Browse files
miss-islingtonsobolevnethanfurman
authored
[3.12] gh-105332: [Enum] Fix unpickling flags in edge-cases (GH-105348) (GH-105520)
* revert enum pickling from by-name to by-value (cherry picked from commit 4ff5690) Co-authored-by: Nikita Sobolev <[email protected]> Co-authored-by: Ethan Furman <[email protected]>
1 parent 68eeab7 commit 2f4a2d6

File tree

4 files changed

+47
-23
lines changed

4 files changed

+47
-23
lines changed

Doc/howto/enum.rst

+10-1
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,16 @@ from that module.
517517
nested in other classes.
518518

519519
It is possible to modify how enum members are pickled/unpickled by defining
520-
:meth:`__reduce_ex__` in the enumeration class.
520+
:meth:`__reduce_ex__` in the enumeration class. The default method is by-value,
521+
but enums with complicated values may want to use by-name::
522+
523+
>>> class MyEnum(Enum):
524+
... __reduce_ex__ = enum.pickle_by_enum_name
525+
526+
.. note::
527+
528+
Using by-name for flags is not recommended, as unnamed aliases will
529+
not unpickle.
521530

522531

523532
Functional API

Lib/enum.py

+9-21
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP',
1313
'global_flag_repr', 'global_enum_repr', 'global_str', 'global_enum',
1414
'EnumCheck', 'CONTINUOUS', 'NAMED_FLAGS', 'UNIQUE',
15+
'pickle_by_global_name', 'pickle_by_enum_name',
1516
]
1617

1718

@@ -922,7 +923,6 @@ def _convert_(cls, name, module, filter, source=None, *, boundary=None, as_globa
922923
body['__module__'] = module
923924
tmp_cls = type(name, (object, ), body)
924925
cls = _simple_enum(etype=cls, boundary=boundary or KEEP)(tmp_cls)
925-
cls.__reduce_ex__ = _reduce_ex_by_global_name
926926
if as_global:
927927
global_enum(cls)
928928
else:
@@ -1240,7 +1240,7 @@ def __hash__(self):
12401240
return hash(self._name_)
12411241

12421242
def __reduce_ex__(self, proto):
1243-
return getattr, (self.__class__, self._name_)
1243+
return self.__class__, (self._value_, )
12441244

12451245
# enum.property is used to provide access to the `name` and
12461246
# `value` attributes of enum members while keeping some measure of
@@ -1307,8 +1307,14 @@ def _generate_next_value_(name, start, count, last_values):
13071307
return name.lower()
13081308

13091309

1310-
def _reduce_ex_by_global_name(self, proto):
1310+
def pickle_by_global_name(self, proto):
1311+
# should not be used with Flag-type enums
13111312
return self.name
1313+
_reduce_ex_by_global_name = pickle_by_global_name
1314+
1315+
def pickle_by_enum_name(self, proto):
1316+
# should not be used with Flag-type enums
1317+
return getattr, (self.__class__, self._name_)
13121318

13131319
class FlagBoundary(StrEnum):
13141320
"""
@@ -1330,23 +1336,6 @@ class Flag(Enum, boundary=STRICT):
13301336
Support for flags
13311337
"""
13321338

1333-
def __reduce_ex__(self, proto):
1334-
cls = self.__class__
1335-
unknown = self._value_ & ~cls._flag_mask_
1336-
member_value = self._value_ & cls._flag_mask_
1337-
if unknown and member_value:
1338-
return _or_, (cls(member_value), unknown)
1339-
for val in _iter_bits_lsb(member_value):
1340-
rest = member_value & ~val
1341-
if rest:
1342-
return _or_, (cls(rest), cls._value2member_map_.get(val))
1343-
else:
1344-
break
1345-
if self._name_ is None:
1346-
return cls, (self._value_,)
1347-
else:
1348-
return getattr, (cls, self._name_)
1349-
13501339
_numeric_repr_ = repr
13511340

13521341
@staticmethod
@@ -2073,7 +2062,6 @@ def _old_convert_(etype, name, module, filter, source=None, *, boundary=None):
20732062
# unless some values aren't comparable, in which case sort by name
20742063
members.sort(key=lambda t: t[0])
20752064
cls = etype(name, members, module=module, boundary=boundary or KEEP)
2076-
cls.__reduce_ex__ = _reduce_ex_by_global_name
20772065
return cls
20782066

20792067
_stdlib_enums = IntEnum, StrEnum, IntFlag

Lib/test/test_enum.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ def load_tests(loader, tests, ignore):
3131
'../../Doc/library/enum.rst',
3232
optionflags=doctest.ELLIPSIS|doctest.NORMALIZE_WHITESPACE,
3333
))
34+
if os.path.exists('Doc/howto/enum.rst'):
35+
tests.addTests(doctest.DocFileSuite(
36+
'../../Doc/howto/enum.rst',
37+
optionflags=doctest.ELLIPSIS|doctest.NORMALIZE_WHITESPACE,
38+
))
3439
return tests
3540

3641
MODULE = __name__
@@ -66,6 +71,7 @@ class FlagStooges(Flag):
6671
LARRY = 1
6772
CURLY = 2
6873
MOE = 4
74+
BIG = 389
6975
except Exception as exc:
7076
FlagStooges = exc
7177

@@ -74,17 +80,20 @@ class FlagStoogesWithZero(Flag):
7480
LARRY = 1
7581
CURLY = 2
7682
MOE = 4
83+
BIG = 389
7784

7885
class IntFlagStooges(IntFlag):
7986
LARRY = 1
8087
CURLY = 2
8188
MOE = 4
89+
BIG = 389
8290

8391
class IntFlagStoogesWithZero(IntFlag):
8492
NOFLAG = 0
8593
LARRY = 1
8694
CURLY = 2
8795
MOE = 4
96+
BIG = 389
8897

8998
# for pickle test and subclass tests
9099
class Name(StrEnum):
@@ -1942,14 +1951,17 @@ class NEI(NamedInt, Enum):
19421951
__qualname__ = 'NEI'
19431952
x = ('the-x', 1)
19441953
y = ('the-y', 2)
1945-
19461954
self.assertIs(NEI.__new__, Enum.__new__)
19471955
self.assertEqual(repr(NEI.x + NEI.y), "NamedInt('(the-x + the-y)', 3)")
19481956
globals()['NamedInt'] = NamedInt
19491957
globals()['NEI'] = NEI
19501958
NI5 = NamedInt('test', 5)
19511959
self.assertEqual(NI5, 5)
19521960
self.assertEqual(NEI.y.value, 2)
1961+
with self.assertRaisesRegex(TypeError, "name and value must be specified"):
1962+
test_pickle_dump_load(self.assertIs, NEI.y)
1963+
# fix pickle support and try again
1964+
NEI.__reduce_ex__ = enum.pickle_by_enum_name
19531965
test_pickle_dump_load(self.assertIs, NEI.y)
19541966
test_pickle_dump_load(self.assertIs, NEI)
19551967

@@ -3252,11 +3264,17 @@ def test_pickle(self):
32523264
test_pickle_dump_load(self.assertEqual,
32533265
FlagStooges.CURLY&~FlagStooges.CURLY)
32543266
test_pickle_dump_load(self.assertIs, FlagStooges)
3267+
test_pickle_dump_load(self.assertEqual, FlagStooges.BIG)
3268+
test_pickle_dump_load(self.assertEqual,
3269+
FlagStooges.CURLY|FlagStooges.BIG)
32553270

32563271
test_pickle_dump_load(self.assertIs, FlagStoogesWithZero.CURLY)
32573272
test_pickle_dump_load(self.assertEqual,
32583273
FlagStoogesWithZero.CURLY|FlagStoogesWithZero.MOE)
32593274
test_pickle_dump_load(self.assertIs, FlagStoogesWithZero.NOFLAG)
3275+
test_pickle_dump_load(self.assertEqual, FlagStoogesWithZero.BIG)
3276+
test_pickle_dump_load(self.assertEqual,
3277+
FlagStoogesWithZero.CURLY|FlagStoogesWithZero.BIG)
32603278

32613279
test_pickle_dump_load(self.assertIs, IntFlagStooges.CURLY)
32623280
test_pickle_dump_load(self.assertEqual,
@@ -3266,11 +3284,19 @@ def test_pickle(self):
32663284
test_pickle_dump_load(self.assertEqual, IntFlagStooges(0))
32673285
test_pickle_dump_load(self.assertEqual, IntFlagStooges(0x30))
32683286
test_pickle_dump_load(self.assertIs, IntFlagStooges)
3287+
test_pickle_dump_load(self.assertEqual, IntFlagStooges.BIG)
3288+
test_pickle_dump_load(self.assertEqual, IntFlagStooges.BIG|1)
3289+
test_pickle_dump_load(self.assertEqual,
3290+
IntFlagStooges.CURLY|IntFlagStooges.BIG)
32693291

32703292
test_pickle_dump_load(self.assertIs, IntFlagStoogesWithZero.CURLY)
32713293
test_pickle_dump_load(self.assertEqual,
32723294
IntFlagStoogesWithZero.CURLY|IntFlagStoogesWithZero.MOE)
32733295
test_pickle_dump_load(self.assertIs, IntFlagStoogesWithZero.NOFLAG)
3296+
test_pickle_dump_load(self.assertEqual, IntFlagStoogesWithZero.BIG)
3297+
test_pickle_dump_load(self.assertEqual, IntFlagStoogesWithZero.BIG|1)
3298+
test_pickle_dump_load(self.assertEqual,
3299+
IntFlagStoogesWithZero.CURLY|IntFlagStoogesWithZero.BIG)
32743300

32753301
def test_contains_tf(self):
32763302
Open = self.Open
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Revert pickling method from by-name back to by-value.

0 commit comments

Comments
 (0)