Skip to content

Commit 914d47f

Browse files
authored
Support dict views and functools.lru_cache (#449)
* Support dictionary views * Remove some unnecessary functions and add LRU cache
1 parent e2831d0 commit 914d47f

File tree

3 files changed

+226
-58
lines changed

3 files changed

+226
-58
lines changed

dill/_dill.py

Lines changed: 173 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ def _trace(boolean):
4040
OLDER = (PY3 and sys.hexversion < 0x3040000) or (sys.hexversion < 0x2070ab1)
4141
OLD33 = (sys.hexversion < 0x3030000)
4242
OLD37 = (sys.hexversion < 0x3070000)
43+
OLD39 = (sys.hexversion < 0x3090000)
44+
OLD310 = (sys.hexversion < 0x30a0000)
4345
PY34 = (0x3040000 <= sys.hexversion < 0x3050000)
4446
if PY3: #XXX: get types from .objtypes ?
4547
import builtins as __builtin__
@@ -219,6 +221,14 @@ class _member(object):
219221
ItemGetterType = type(itemgetter(0))
220222
AttrGetterType = type(attrgetter('__repr__'))
221223

224+
try:
225+
from functools import _lru_cache_wrapper as LRUCacheType
226+
except:
227+
LRUCacheType = None
228+
229+
if not isinstance(LRUCacheType, type):
230+
LRUCacheType = None
231+
222232
def get_file_type(*args, **kwargs):
223233
open = kwargs.pop("open", __builtin__.open)
224234
f = open(os.devnull, *args, **kwargs)
@@ -262,6 +272,8 @@ def get_file_type(*args, **kwargs):
262272
except NameError: ExitType = None
263273
singletontypes = []
264274

275+
from collections import OrderedDict
276+
265277
import inspect
266278

267279
### Shims for different versions of Python and dill
@@ -914,6 +926,23 @@ def __getattribute__(self, attr):
914926
attrs[index] = ".".join([attrs[index], attr])
915927
return type(self)(attrs, index)
916928

929+
class _dictproxy_helper(dict):
930+
def __ror__(self, a):
931+
return a
932+
933+
_dictproxy_helper_instance = _dictproxy_helper()
934+
935+
__d = {}
936+
try:
937+
# In CPython 3.9 and later, this trick can be used to exploit the
938+
# implementation of the __or__ function of MappingProxyType to get the true
939+
# mapping referenced by the proxy. It may work for other implementations,
940+
# but is not guaranteed.
941+
MAPPING_PROXY_TRICK = __d is (DictProxyType(__d) | _dictproxy_helper_instance)
942+
except:
943+
MAPPING_PROXY_TRICK = False
944+
del __d
945+
917946
# _CELL_REF and _CELL_EMPTY are used to stay compatible with versions of dill
918947
# whose _create_cell functions do not have a default value.
919948
# _CELL_REF can be safely removed entirely (replaced by empty tuples for calls
@@ -1166,6 +1195,62 @@ def save_module_dict(pickler, obj):
11661195
log.info("# D2")
11671196
return
11681197

1198+
1199+
if not OLD310 and MAPPING_PROXY_TRICK:
1200+
def save_dict_view(dicttype):
1201+
def save_dict_view_for_function(func):
1202+
def _save_dict_view(pickler, obj):
1203+
log.info("Dkvi: <%s>" % (obj,))
1204+
mapping = obj.mapping | _dictproxy_helper_instance
1205+
pickler.save_reduce(func, (mapping,), obj=obj)
1206+
log.info("# Dkvi")
1207+
return _save_dict_view
1208+
return [
1209+
(funcname, save_dict_view_for_function(getattr(dicttype, funcname)))
1210+
for funcname in ('keys', 'values', 'items')
1211+
]
1212+
else:
1213+
# The following functions are based on 'cloudpickle'
1214+
# https://github.com/cloudpipe/cloudpickle/blob/5d89947288a18029672596a4d719093cc6d5a412/cloudpickle/cloudpickle.py#L922-L940
1215+
# Copyright (c) 2012, Regents of the University of California.
1216+
# Copyright (c) 2009 `PiCloud, Inc. <http://www.picloud.com>`_.
1217+
# License: https://github.com/cloudpipe/cloudpickle/blob/master/LICENSE
1218+
def save_dict_view(dicttype):
1219+
def save_dict_keys(pickler, obj):
1220+
log.info("Dk: <%s>" % (obj,))
1221+
dict_constructor = _shims.Reduce(dicttype.fromkeys, (list(obj),))
1222+
pickler.save_reduce(dicttype.keys, (dict_constructor,), obj=obj)
1223+
log.info("# Dk")
1224+
1225+
def save_dict_values(pickler, obj):
1226+
log.info("Dv: <%s>" % (obj,))
1227+
dict_constructor = _shims.Reduce(dicttype, (enumerate(obj),))
1228+
pickler.save_reduce(dicttype.values, (dict_constructor,), obj=obj)
1229+
log.info("# Dv")
1230+
1231+
def save_dict_items(pickler, obj):
1232+
log.info("Di: <%s>" % (obj,))
1233+
pickler.save_reduce(dicttype.items, (dicttype(obj),), obj=obj)
1234+
log.info("# Di")
1235+
1236+
return (
1237+
('keys', save_dict_keys),
1238+
('values', save_dict_values),
1239+
('items', save_dict_items)
1240+
)
1241+
1242+
for __dicttype in (
1243+
dict,
1244+
OrderedDict
1245+
):
1246+
__obj = __dicttype()
1247+
for __funcname, __savefunc in save_dict_view(__dicttype):
1248+
__tview = type(getattr(__obj, __funcname)())
1249+
if __tview not in Pickler.dispatch:
1250+
Pickler.dispatch[__tview] = __savefunc
1251+
del __dicttype, __obj, __funcname, __tview, __savefunc
1252+
1253+
11691254
@register(ClassType)
11701255
def save_classobj(pickler, obj): #FIXME: enable pickler._byref
11711256
if obj.__module__ == '__main__': #XXX: use _main_module.__name__ everywhere?
@@ -1206,24 +1291,25 @@ def save_socket(pickler, obj):
12061291
log.info("# So")
12071292
return
12081293

1209-
@register(ItemGetterType)
1210-
def save_itemgetter(pickler, obj):
1211-
log.info("Ig: %s" % obj)
1212-
helper = _itemgetter_helper()
1213-
obj(helper)
1214-
pickler.save_reduce(type(obj), tuple(helper.items), obj=obj)
1215-
log.info("# Ig")
1216-
return
1294+
if sys.hexversion <= 0x3050000:
1295+
@register(ItemGetterType)
1296+
def save_itemgetter(pickler, obj):
1297+
log.info("Ig: %s" % obj)
1298+
helper = _itemgetter_helper()
1299+
obj(helper)
1300+
pickler.save_reduce(type(obj), tuple(helper.items), obj=obj)
1301+
log.info("# Ig")
1302+
return
12171303

1218-
@register(AttrGetterType)
1219-
def save_attrgetter(pickler, obj):
1220-
log.info("Ag: %s" % obj)
1221-
attrs = []
1222-
helper = _attrgetter_helper(attrs)
1223-
obj(helper)
1224-
pickler.save_reduce(type(obj), tuple(attrs), obj=obj)
1225-
log.info("# Ag")
1226-
return
1304+
@register(AttrGetterType)
1305+
def save_attrgetter(pickler, obj):
1306+
log.info("Ag: %s" % obj)
1307+
attrs = []
1308+
helper = _attrgetter_helper(attrs)
1309+
obj(helper)
1310+
pickler.save_reduce(type(obj), tuple(attrs), obj=obj)
1311+
log.info("# Ag")
1312+
return
12271313

12281314
def _save_file(pickler, obj, open_):
12291315
if obj.closed:
@@ -1303,13 +1389,33 @@ def save_stringo(pickler, obj):
13031389
log.info("# Io")
13041390
return
13051391

1306-
@register(PartialType)
1307-
def save_functor(pickler, obj):
1308-
log.info("Fu: %s" % obj)
1309-
pickler.save_reduce(_create_ftype, (type(obj), obj.func, obj.args,
1310-
obj.keywords), obj=obj)
1311-
log.info("# Fu")
1312-
return
1392+
if 0x2050000 <= sys.hexversion < 0x3010000:
1393+
@register(PartialType)
1394+
def save_functor(pickler, obj):
1395+
log.info("Fu: %s" % obj)
1396+
pickler.save_reduce(_create_ftype, (type(obj), obj.func, obj.args,
1397+
obj.keywords), obj=obj)
1398+
log.info("# Fu")
1399+
return
1400+
1401+
if LRUCacheType is not None:
1402+
from functools import lru_cache
1403+
@register(LRUCacheType)
1404+
def save_lru_cache(pickler, obj):
1405+
log.info("LRU: %s" % obj)
1406+
if OLD39:
1407+
kwargs = obj.cache_info()
1408+
args = (kwargs.maxsize,)
1409+
else:
1410+
kwargs = obj.cache_parameters()
1411+
args = (kwargs['maxsize'], kwargs['typed'])
1412+
if args != lru_cache.__defaults__:
1413+
wrapper = Reduce(lru_cache, args, is_callable=True)
1414+
else:
1415+
wrapper = lru_cache
1416+
pickler.save_reduce(wrapper, (obj.__wrapped__,), obj=obj)
1417+
log.info("# LRU")
1418+
return
13131419

13141420
@register(SuperType)
13151421
def save_super(pickler, obj):
@@ -1318,41 +1424,42 @@ def save_super(pickler, obj):
13181424
log.info("# Su")
13191425
return
13201426

1321-
@register(BuiltinMethodType)
1322-
def save_builtin_method(pickler, obj):
1323-
if obj.__self__ is not None:
1324-
if obj.__self__ is __builtin__:
1325-
module = 'builtins' if PY3 else '__builtin__'
1326-
_t = "B1"
1327-
log.info("%s: %s" % (_t, obj))
1427+
if OLDER or not PY3:
1428+
@register(BuiltinMethodType)
1429+
def save_builtin_method(pickler, obj):
1430+
if obj.__self__ is not None:
1431+
if obj.__self__ is __builtin__:
1432+
module = 'builtins' if PY3 else '__builtin__'
1433+
_t = "B1"
1434+
log.info("%s: %s" % (_t, obj))
1435+
else:
1436+
module = obj.__self__
1437+
_t = "B3"
1438+
log.info("%s: %s" % (_t, obj))
1439+
if is_dill(pickler, child=True):
1440+
_recurse = pickler._recurse
1441+
pickler._recurse = False
1442+
pickler.save_reduce(_get_attr, (module, obj.__name__), obj=obj)
1443+
if is_dill(pickler, child=True):
1444+
pickler._recurse = _recurse
1445+
log.info("# %s" % _t)
13281446
else:
1329-
module = obj.__self__
1330-
_t = "B3"
1331-
log.info("%s: %s" % (_t, obj))
1332-
if is_dill(pickler, child=True):
1333-
_recurse = pickler._recurse
1334-
pickler._recurse = False
1335-
pickler.save_reduce(_get_attr, (module, obj.__name__), obj=obj)
1336-
if is_dill(pickler, child=True):
1337-
pickler._recurse = _recurse
1338-
log.info("# %s" % _t)
1339-
else:
1340-
log.info("B2: %s" % obj)
1341-
name = getattr(obj, '__qualname__', getattr(obj, '__name__', None))
1342-
StockPickler.save_global(pickler, obj, name=name)
1343-
log.info("# B2")
1344-
return
1447+
log.info("B2: %s" % obj)
1448+
name = getattr(obj, '__qualname__', getattr(obj, '__name__', None))
1449+
StockPickler.save_global(pickler, obj, name=name)
1450+
log.info("# B2")
1451+
return
13451452

1346-
@register(MethodType) #FIXME: fails for 'hidden' or 'name-mangled' classes
1347-
def save_instancemethod0(pickler, obj):# example: cStringIO.StringI
1348-
log.info("Me: %s" % obj) #XXX: obj.__dict__ handled elsewhere?
1349-
if PY3:
1350-
pickler.save_reduce(MethodType, (obj.__func__, obj.__self__), obj=obj)
1351-
else:
1352-
pickler.save_reduce(MethodType, (obj.im_func, obj.im_self,
1353-
obj.im_class), obj=obj)
1354-
log.info("# Me")
1355-
return
1453+
@register(MethodType) #FIXME: fails for 'hidden' or 'name-mangled' classes
1454+
def save_instancemethod0(pickler, obj):# example: cStringIO.StringI
1455+
log.info("Me: %s" % obj) #XXX: obj.__dict__ handled elsewhere?
1456+
if PY3:
1457+
pickler.save_reduce(MethodType, (obj.__func__, obj.__self__), obj=obj)
1458+
else:
1459+
pickler.save_reduce(MethodType, (obj.im_func, obj.im_self,
1460+
obj.im_class), obj=obj)
1461+
log.info("# Me")
1462+
return
13561463

13571464
if sys.hexversion >= 0x20500f0:
13581465
if not IS_PYPY:
@@ -1440,7 +1547,15 @@ def save_cell(pickler, obj):
14401547
log.info("# Ce1")
14411548
return
14421549

1443-
if not IS_PYPY:
1550+
if MAPPING_PROXY_TRICK:
1551+
@register(DictProxyType)
1552+
def save_dictproxy(pickler, obj):
1553+
log.info("Mp: %s" % obj)
1554+
mapping = obj | _dictproxy_helper_instance
1555+
pickler.save_reduce(DictProxyType, (mapping,), obj=obj)
1556+
log.info("# Mp")
1557+
return
1558+
elif not IS_PYPY:
14441559
if not OLD33:
14451560
@register(DictProxyType)
14461561
def save_dictproxy(pickler, obj):

tests/test_dictviews.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env python
2+
#
3+
# Author: Mike McKerns (mmckerns @caltech and @uqfoundation)
4+
# Copyright (c) 2008-2016 California Institute of Technology.
5+
# Copyright (c) 2016-2021 The Uncertainty Quantification Foundation.
6+
# License: 3-clause BSD. The full license text is available at:
7+
# - https://github.com/uqfoundation/dill/blob/master/LICENSE
8+
9+
import dill
10+
from dill._dill import OLD310, MAPPING_PROXY_TRICK
11+
12+
def test_dictviews():
13+
x = {'a': 1}
14+
assert dill.copy(x.keys())
15+
assert dill.copy(x.values())
16+
assert dill.copy(x.items())
17+
18+
def test_dictproxy_trick():
19+
if not OLD310 and MAPPING_PROXY_TRICK:
20+
x = {'a': 1}
21+
all_views = (x.values(), x.items(), x.keys(), x)
22+
seperate_views = dill.copy(all_views)
23+
new_x = seperate_views[-1]
24+
new_x['b'] = 2
25+
new_x['c'] = 1
26+
assert len(new_x) == 3 and len(x) == 1
27+
assert len(seperate_views[0]) == 3 and len(all_views[0]) == 1
28+
assert len(seperate_views[1]) == 3 and len(all_views[1]) == 1
29+
assert len(seperate_views[2]) == 3 and len(all_views[2]) == 1
30+
assert dict(all_views[1]) == x
31+
assert dict(seperate_views[1]) == new_x
32+
33+
if __name__ == '__main__':
34+
test_dictviews()
35+
test_dictproxy_trick()

tests/test_functions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# License: 3-clause BSD. The full license text is available at:
66
# - https://github.com/uqfoundation/dill/blob/master/LICENSE
77

8+
import functools
89
import dill
910
import sys
1011
dill.settings['recurse'] = True
@@ -35,6 +36,14 @@ def function_d(d, d1, d2=1):
3536
def function_e(e, *e1, e2=1, e3=2):
3637
return e + sum(e1) + e2 + e3''')
3738

39+
globalvar = 0
40+
41+
@functools.lru_cache(None)
42+
def function_with_cache(x):
43+
global globalvar
44+
globalvar += x
45+
return globalvar
46+
3847

3948
def function_with_unassigned_variable():
4049
if False:
@@ -58,6 +67,15 @@ def test_functions():
5867
assert dill.loads(dumped_func_d)(1, 2, 3) == 6
5968
assert dill.loads(dumped_func_d)(1, 2, d2=3) == 6
6069

70+
if is_py3():
71+
function_with_cache(1)
72+
globalvar = 0
73+
dumped_func_cache = dill.dumps(function_with_cache)
74+
assert function_with_cache(2) == 3
75+
assert function_with_cache(1) == 1
76+
assert function_with_cache(3) == 6
77+
assert function_with_cache(2) == 3
78+
6179
empty_cell = function_with_unassigned_variable()
6280
cell_copy = dill.loads(dill.dumps(empty_cell))
6381
assert 'empty' in str(cell_copy.__closure__[0])

0 commit comments

Comments
 (0)