Skip to content

Commit 1b6da36

Browse files
committed
Fix ABC.__new__ bug and add a heinous ABCEnum test case
1 parent 7a19c1f commit 1b6da36

File tree

2 files changed

+171
-18
lines changed

2 files changed

+171
-18
lines changed

dill/_dill.py

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,7 +1619,13 @@ def save_cell(pickler, obj):
16191619
log.info("# Ce3")
16201620
return
16211621
if is_dill(pickler, child=True):
1622-
postproc = next(iter(pickler._postproc.values()), None)
1622+
if id(f) in pickler._postproc:
1623+
# Already seen. Add to its postprocessing.
1624+
postproc = pickler._postproc[id(f)]
1625+
else:
1626+
# Haven't seen it. Add to the highest possible object and set its
1627+
# value as late as possible to prevent cycle.
1628+
postproc = next(iter(pickler._postproc.values()), None)
16231629
if postproc is not None:
16241630
log.info("Ce2: %s" % obj)
16251631
# _CELL_REF is defined in _shims.py to support older versions of
@@ -1830,7 +1836,7 @@ def _get_typedict_type(cls, clsdict, postproc_list):
18301836
return clsdict
18311837
# return _dict_from_dictproxy(cls.__dict__)
18321838

1833-
def _get_typedict_abc(obj, _dict, state, postproc_list):
1839+
def _get_typedict_abc(obj, _dict, attrs, postproc_list):
18341840
log.info("ABC: %s" % obj)
18351841
if hasattr(abc, '_get_dump'):
18361842
(registry, _, _, _) = abc._get_dump(obj)
@@ -1851,9 +1857,9 @@ def _get_typedict_abc(obj, _dict, state, postproc_list):
18511857
else:
18521858
del _dict['_abc_impl']
18531859
log.info("# ABC")
1854-
return _dict, state
1860+
return _dict, attrs
18551861

1856-
def _get_typedict_enum(obj, _dict, state, postproc_list):
1862+
def _get_typedict_enum(obj, _dict, attrs, postproc_list):
18571863
log.info("E: %s" % obj)
18581864
metacls = type(obj)
18591865
original_dict = {}
@@ -1866,8 +1872,12 @@ def _get_typedict_enum(obj, _dict, state, postproc_list):
18661872
_dict.pop('_value2member_map_', None)
18671873
_dict.pop('_generate_next_value_', None)
18681874

1875+
if attrs is not None:
1876+
attrs.update(_dict)
1877+
_dict = attrs
1878+
18691879
log.info("# E")
1870-
return original_dict, (None, _dict)
1880+
return original_dict, _dict
18711881

18721882
@register(TypeType)
18731883
def save_type(pickler, obj, postproc_list=None):
@@ -1912,7 +1922,6 @@ def save_type(pickler, obj, postproc_list=None):
19121922
log.info("# T7")
19131923

19141924
else:
1915-
obj_name = getattr(obj, '__qualname__', getattr(obj, '__name__', None))
19161925
_byref = getattr(pickler, '_byref', None)
19171926
obj_recursive = id(obj) in getattr(pickler, '_postproc', ())
19181927
incorrectly_named = not _locate_function(obj, pickler)
@@ -1923,25 +1932,30 @@ def save_type(pickler, obj, postproc_list=None):
19231932
# thanks to Tom Stepleton pointing out pickler._session unneeded
19241933
_t = 'T3'
19251934
_dict = _get_typedict_type(obj, obj.__dict__.copy(), postproc_list) # copy dict proxy to a dict
1926-
state = None
1935+
attrs = None
19271936

19281937
for name in _dict.get("__slots__", []):
19291938
del _dict[name]
19301939

1931-
if PY3 and obj_name != obj.__name__:
1932-
postproc_list.append((setattr, (obj, '__qualname__', obj_name)))
1933-
19341940
if isinstance(obj, abc.ABCMeta):
1935-
_dict, state = _get_typedict_abc(obj, _dict, state, postproc_list)
1941+
_dict, attrs = _get_typedict_abc(obj, _dict, attrs, postproc_list)
19361942

19371943
if EnumMeta and isinstance(obj, EnumMeta):
1938-
_dict, state = _get_typedict_enum(obj, _dict, state, postproc_list)
1939-
1940-
#print (_dict)
1941-
#print ("%s\n%s" % (type(obj), obj.__name__))
1942-
#print ("%s\n%s" % (obj.__bases__, obj.__dict__))
1943-
1944-
if PY3 and type(obj) is not type or hasattr(obj, '__orig_bases__'):
1944+
_dict, attrs = _get_typedict_enum(obj, _dict, attrs, postproc_list)
1945+
1946+
qualname = getattr(obj, '__qualname__', None)
1947+
if attrs is not None:
1948+
if qualname is not None:
1949+
attrs['__qualname__'] = qualname
1950+
for k, v in attrs.items():
1951+
postproc_list.append((setattr, (obj, k, v)))
1952+
state = _dict, attrs
1953+
elif qualname is not None:
1954+
postproc_list.append((setattr, (obj, '__qualname__', qualname)))
1955+
state = _dict
1956+
1957+
if True: # PY3 and type(obj) is not type or hasattr(obj, '__orig_bases__'):
1958+
# This case will always work, but might be overkill.
19451959
from types import new_class
19461960
_metadict = {
19471961
'metaclass': type(obj)
@@ -1962,6 +1976,7 @@ def save_type(pickler, obj, postproc_list=None):
19621976
)), state, obj=obj, postproc_list=postproc_list)
19631977
log.info("# %s" % _t)
19641978
else:
1979+
obj_name = getattr(obj, '__qualname__', getattr(obj, '__name__', None))
19651980
log.info("T4: %s" % obj)
19661981
if incorrectly_named:
19671982
warnings.warn('Cannot locate reference to %r.' % (obj,), PicklingWarning)

tests/test_enum.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
try:
2+
import enum
3+
from enum import Enum, IntEnum, EnumMeta, Flag, IntFlag, unique, auto
4+
except:
5+
Enum = None
6+
7+
import abc
8+
9+
import dill
10+
import sys
11+
12+
dill.settings['recurse'] = True
13+
14+
"""
15+
Test cases copied from https://raw.githubusercontent.com/python/cpython/3.10/Lib/test/test_enum.py
16+
17+
Copyright 1991-1995 by Stichting Mathematisch Centrum, Amsterdam, The Netherlands.
18+
19+
All Rights Reserved
20+
Permission to use, copy, modify, and distribute this software and its
21+
documentation for any purpose and without fee is hereby granted, provided that
22+
the above copyright notice appear in all copies and that both that copyright
23+
notice and this permission notice appear in supporting documentation, and that
24+
the names of Stichting Mathematisch Centrum or CWI or Corporation for National
25+
Research Initiatives or CNRI not be used in advertising or publicity pertaining
26+
to distribution of the software without specific, written prior permission.
27+
28+
While CWI is the initial source for this software, a modified version is made
29+
available by the Corporation for National Research Initiatives (CNRI) at the
30+
Internet address http://www.python.org.
31+
32+
STICHTING MATHEMATISCH CENTRUM AND CNRI DISCLAIM ALL WARRANTIES WITH REGARD TO
33+
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS,
34+
IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM OR CNRI BE LIABLE FOR ANY
35+
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
36+
FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
37+
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
38+
PERFORMANCE OF THIS SOFTWARE.
39+
"""
40+
41+
def test_enums():
42+
43+
class Stooges(Enum):
44+
LARRY = 1
45+
CURLY = 2
46+
MOE = 3
47+
48+
class IntStooges(int, Enum):
49+
LARRY = 1
50+
CURLY = 2
51+
MOE = 3
52+
53+
class FloatStooges(float, Enum):
54+
LARRY = 1.39
55+
CURLY = 2.72
56+
MOE = 3.142596
57+
58+
class FlagStooges(Flag):
59+
LARRY = 1
60+
CURLY = 2
61+
MOE = 3
62+
63+
# https://stackoverflow.com/a/56135108
64+
class ABCEnumMeta(abc.ABCMeta, EnumMeta):
65+
def __new__(mcls, *args, **kw):
66+
abstract_enum_cls = super().__new__(mcls, *args, **kw)
67+
# Only check abstractions if members were defined.
68+
if abstract_enum_cls._member_map_:
69+
try: # Handle existence of undefined abstract methods.
70+
absmethods = list(abstract_enum_cls.__abstractmethods__)
71+
if absmethods:
72+
missing = ', '.join(f'{method!r}' for method in absmethods)
73+
plural = 's' if len(absmethods) > 1 else ''
74+
raise TypeError(
75+
f"cannot instantiate abstract class {abstract_enum_cls.__name__!r}"
76+
f" with abstract method{plural} {missing}")
77+
except AttributeError:
78+
pass
79+
return abstract_enum_cls
80+
81+
if dill._dill.PY3:
82+
l = locals()
83+
exec("""class StrEnum(str, abc.ABC, Enum, metaclass=ABCEnumMeta):
84+
'accepts only string values'
85+
def invisible(self):
86+
return "did you see me?" """, None, l)
87+
StrEnum = l['StrEnum']
88+
else:
89+
class StrEnum(str, abc.ABC, Enum):
90+
__metaclass__ = ABCEnumMeta
91+
'accepts only string values'
92+
def invisible(self):
93+
return "did you see me?"
94+
95+
class Name(StrEnum):
96+
BDFL = 'Guido van Rossum'
97+
FLUFL = 'Barry Warsaw'
98+
99+
assert 'invisible' in dir(dill.copy(Name).BDFL)
100+
assert 'invisible' in dir(dill.copy(Name.BDFL))
101+
assert dill.copy(Name.BDFL) is not Name.BDFL
102+
103+
Question = Enum('Question', 'who what when where why', module=__name__)
104+
Answer = Enum('Answer', 'him this then there because')
105+
Theory = Enum('Theory', 'rule law supposition', qualname='spanish_inquisition')
106+
107+
class Fruit(Enum):
108+
TOMATO = 1
109+
BANANA = 2
110+
CHERRY = 3
111+
112+
assert dill.copy(Fruit).TOMATO.value == 1 and dill.copy(Fruit).TOMATO != 1 \
113+
and dill.copy(Fruit).TOMATO is not Fruit.TOMATO
114+
115+
from datetime import date
116+
class Holiday(date, Enum):
117+
NEW_YEAR = 2013, 1, 1
118+
IDES_OF_MARCH = 2013, 3, 15
119+
120+
# TODO: Fix this case
121+
# assert hasattr(dill.copy(Holiday), 'NEW_YEAR')
122+
123+
class SuperEnum(IntEnum):
124+
def __new__(cls, value, description=""):
125+
obj = int.__new__(cls, value)
126+
obj._value_ = value
127+
obj.description = description
128+
return obj
129+
130+
class SubEnum(SuperEnum):
131+
sample = 5
132+
133+
if sys.hexversion >= 0x030a0000:
134+
assert 'description' in dir(dill.copy(SubEnum.sample))
135+
assert 'description' in dir(dill.copy(SubEnum).sample)
136+
137+
if __name__ == '__main__':
138+
test_enums()

0 commit comments

Comments
 (0)