Skip to content

Add ability to pickle dynamically create modules #52

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 3, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions cloudpickle/cloudpickle.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

import operator
import io
import imp
import pickle
import struct
import sys
Expand Down Expand Up @@ -134,8 +135,19 @@ def save_module(self, obj):
"""
Save a module as an import
"""
mod_name = obj.__name__
# If module is successfully found then it is not a dynamically created module
try:
_find_module(mod_name)
is_dynamic = False
except ImportError:
is_dynamic = True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to pull this logic out into a separate function?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree


self.modules.add(obj)
self.save_reduce(subimport, (obj.__name__,), obj=obj)
if is_dynamic:
self.save_reduce(dynamic_subimport, (obj.__name__, vars(obj)), obj=obj)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

obj=obj here is actually important to enable memoization, but it doesn't ensure that the produced dynamic modules will be singletons. Example:

dynmod.x = 1
m1, m2 = pickle.depickle([dynmod, dynmod])
m1.x = 2  # m1 and m2 are identical, m2.x is also 2
m3 = pickle.depickle(dynmod)
# m3 is a different module altogether

else:
self.save_reduce(subimport, (obj.__name__,), obj=obj)
dispatch[types.ModuleType] = save_module

def save_codeobject(self, obj):
Expand Down Expand Up @@ -313,7 +325,7 @@ def extract_func_data(self, func):
return (code, f_globals, defaults, closure, dct, base_globals)

def save_builtin_function(self, obj):
if obj.__module__ is "__builtin__":
if obj.__module__ == "__builtin__":
return self.save_global(obj)
return self.save_function(obj)
dispatch[types.BuiltinFunctionType] = save_builtin_function
Expand Down Expand Up @@ -584,11 +596,20 @@ def save_file(self, obj):
self.save(retval)
self.memoize(obj)

def save_ellipsis(self, obj):
self.save_reduce(_gen_ellipsis, ())

def save_not_implemented(self, obj):
self.save_reduce(_gen_not_implemented, ())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this not handled by normal exceptions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does NotImplemented ever carry arguments?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, my apologies, I was thinking of NotImplementedError


if PY3:
dispatch[io.TextIOWrapper] = save_file
else:
dispatch[file] = save_file

dispatch[type(Ellipsis)] = save_ellipsis
dispatch[type(NotImplemented)] = save_not_implemented

"""Special functions for Add-on libraries"""
def inject_addons(self):
"""Plug in system. Register additional pickling functions if modules already loaded"""
Expand Down Expand Up @@ -620,6 +641,12 @@ def subimport(name):
return sys.modules[name]


def dynamic_subimport(name, vars):
mod = imp.new_module(name)
mod.__dict__.update(vars)
sys.modules[name] = mod
return mod

# restores function attributes
def _restore_attr(obj, attr):
for key, val in attr.items():
Expand Down Expand Up @@ -663,6 +690,11 @@ def _genpartial(func, args, kwds):
kwds = {}
return partial(func, *args, **kwds)

def _gen_ellipsis():
return Ellipsis

def _gen_not_implemented():
return NotImplemented

def _fill_function(func, globals, defaults, dict):
""" Fills in the rest of function data into the skeleton function object
Expand Down Expand Up @@ -698,6 +730,18 @@ def _make_skel_func(code, closures, base_globals = None):
None, None, closure)


def _find_module(mod_name):
"""
Iterate over each part instead of calling imp.find_module directly.
This function is able to find submodules (e.g. sickit.tree)
"""
path = None
for part in mod_name.split('.'):
if path is not None:
path = [path]
file, path, description = imp.find_module(part, path)
return file, path, description

"""Constructors for 3rd party libraries
Note: These can never be renamed due to client compatibility issues"""

Expand Down
38 changes: 38 additions & 0 deletions tests/cloudpickle_test.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from __future__ import division
import imp
import unittest
import pytest
import pickle
import sys
import functools
import platform
import textwrap

try:
# try importing numpy and scipy. These are not hard dependencies and
Expand All @@ -26,6 +28,7 @@
from io import BytesIO

import cloudpickle
from cloudpickle.cloudpickle import _find_module

from .testutils import subprocess_pickle_echo

Expand Down Expand Up @@ -252,6 +255,41 @@ def f(self, x):
self.assertEqual(g.im_class.__name__, F.f.im_class.__name__)
# self.assertEqual(g(F(), 1), 2) # still fails

def test_module(self):
self.assertEqual(pickle, pickle_depickle(pickle))

def test_dynamic_module(self):
mod = imp.new_module('mod')
code = '''
x = 1
def f(y):
return x + y
'''
exec(textwrap.dedent(code), mod.__dict__)
mod2 = pickle_depickle(mod)
self.assertEqual(mod.x, mod2.x)
self.assertEqual(mod.f(5), mod2.f(5))

# Test dynamic modules when imported back are singletons
mod1, mod2 = pickle_depickle([mod, mod])
self.assertEqual(id(mod1), id(mod2))

def test_find_module(self):
import pickle # ensure this test is decoupled from global imports
_find_module('pickle')

with pytest.raises(ImportError):
_find_module('invalid_module')

with pytest.raises(ImportError):
valid_module = imp.new_module('valid_module')
_find_module('valid_module')

def test_Ellipsis(self):
self.assertEqual(Ellipsis, pickle_depickle(Ellipsis))

def test_NotImplemented(self):
self.assertEqual(NotImplemented, pickle_depickle(NotImplemented))

if __name__ == '__main__':
unittest.main()