Skip to content

Commit

Permalink
Add support for classic instances with arguments to __init__; add sup…
Browse files Browse the repository at this point in the history
…port for __getinitargs__; this makes for full pickle protocol 2 support. Also has full PY3 compatibility.
  • Loading branch information
marcintustin committed Sep 2, 2014
1 parent 2147158 commit 026220f
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 22 deletions.
21 changes: 13 additions & 8 deletions jsonpickle/pickler.py
Expand Up @@ -224,10 +224,9 @@ def _flatten_obj_instance(self, obj):
has_dict = hasattr(obj, '__dict__')
has_slots = not has_dict and hasattr(obj, '__slots__')
has_getnewargs = hasattr(obj, '__getnewargs__')
has_getinitargs = hasattr(obj, '__getinitargs__')
has_reduce, has_reduce_ex = util.has_reduce(obj)

# import pdb; pdb.set_trace()

# Support objects with __getstate__(); this ensures that
# both __setstate__() and __getstate__() are implemented
has_getstate = hasattr(obj, '__getstate__')
Expand Down Expand Up @@ -272,8 +271,9 @@ def _flatten_obj_instance(self, obj):

if reduce_val:
try:
# At present, we only handle the case where __reduce__ returns a string
if isinstance(reduce_val, basestring):
# At this stage, we only handle the case where __reduce__ returns a string
# other reduce functionality is implemented further down
if isinstance(reduce_val, (str, unicode)):
varpath = iter(reduce_val.split('.'))
# curmod will be transformed by the loop into the value to pickle
curmod = sys.modules[next(varpath)]
Expand All @@ -287,6 +287,9 @@ def _flatten_obj_instance(self, obj):

if has_getnewargs:
data[tags.NEWARGS] = self._flatten(obj.__getnewargs__())

if has_getinitargs:
data[tags.INITARGS] = self._flatten(obj.__getinitargs__())

if has_getstate:
try:
Expand Down Expand Up @@ -318,11 +321,11 @@ def _flatten_obj_instance(self, obj):
return [self._flatten(v) for v in obj]

if util.is_iterator(obj):
data[tags.ITERATOR] = map(self._flatten, islice(obj, self._max_iter))
# force list in python 3
data[tags.ITERATOR] = list(map(self._flatten, islice(obj, self._max_iter)))
return data


if reduce_val and not isinstance(reduce_val, basestring):
if reduce_val and not isinstance(reduce_val, (str, unicode)):
# at this point, reduce_val should be some kind of iterable
# pad out to len 5
rv_as_list = list(reduce_val)
Expand All @@ -334,7 +337,7 @@ def _flatten_obj_instance(self, obj):
if rv_as_list[0].__name__ == '__newobj__':
rv_as_list[0] = tags.NEWOBJ

data[tags.REDUCE] = map(self._flatten, rv_as_list)
data[tags.REDUCE] = list(map(self._flatten, rv_as_list))

# lift out iterators, so we don't have to iterator and uniterator their content
# on unpickle
Expand All @@ -343,6 +346,8 @@ def _flatten_obj_instance(self, obj):

if data[tags.REDUCE][4]:
data[tags.REDUCE][4] = data[tags.REDUCE][4][tags.ITERATOR]

return data

if has_dict:
# Support objects that subclasses list and set
Expand Down
2 changes: 2 additions & 0 deletions jsonpickle/tags.py
Expand Up @@ -11,6 +11,7 @@

FUNCTION = 'py/function'
ID = 'py/id'
INITARGS = 'py/initargs'
ITERATOR = 'py/iterator'
JSON_KEY = 'json://'
NEWARGS = 'py/newargs'
Expand All @@ -29,6 +30,7 @@
RESERVED = set([
FUNCTION,
ID,
INITARGS,
ITERATOR,
NEWARGS,
NEWOBJ,
Expand Down
34 changes: 30 additions & 4 deletions jsonpickle/unpickler.py
Expand Up @@ -240,6 +240,7 @@ def _loadfactory(self, obj):
def _restore_object_instance(self, obj, cls):
factory = self._loadfactory(obj)
args = getargs(obj)
is_oldstyle = not (isinstance(cls, type) or getattr(cls, '__meta__', None))

# This is a placeholder proxy object which allows child objects to
# reference the parent object before it has been instantiated.
Expand All @@ -249,7 +250,7 @@ def _restore_object_instance(self, obj, cls):
if args:
args = self._restore(args)
try:
if hasattr(cls, '__new__'): # new style classes
if (not is_oldstyle) and hasattr(cls, '__new__'): # new style classes
if factory:
instance = cls.__new__(cls, factory, *args)
instance.default_factory = factory
Expand All @@ -258,10 +259,16 @@ def _restore_object_instance(self, obj, cls):
else:
instance = object.__new__(cls)
except TypeError: # old-style classes
is_oldstyle = True

if is_oldstyle:
try:
instance = cls()
except TypeError: # fail gracefully
return self._mkref(obj)
instance = cls(*args)
except TypeError: # fail gracefully
try:
instance = make_blank_classic(cls)
except: # fail gracefully
return self._mkref(obj)

proxy.instance = instance
self._swapref(proxy, instance)
Expand Down Expand Up @@ -466,6 +473,10 @@ def getargs(obj):
# Let saved newargs take precedence over everything
if has_tag(obj, tags.NEWARGS):
return obj[tags.NEWARGS]

if has_tag(obj, tags.INITARGS):
return obj[tags.INITARGS]

try:
seq_list = obj[tags.SEQ]
obj_dict = obj[tags.OBJECT]
Expand All @@ -480,6 +491,21 @@ def getargs(obj):
return []


class _trivialclassic:
"""
A trivial class that can be instantiated with no args
"""

def make_blank_classic(cls):
"""
Implement the mandated strategy for dealing with classic classes
which cannot be instantiated without __getinitargs__ because they
take parameters
"""
instance = _trivialclassic()
instance.__class__ = cls
return instance

def loadrepr(reprstr):
"""Returns an instance of the object from the object's repr() string.
It involves the dynamic specification of code.
Expand Down
10 changes: 7 additions & 3 deletions jsonpickle/util.py
Expand Up @@ -9,7 +9,6 @@
"""Helper functions for pickling and unpickling. Most functions assist in
determining the type of an object.
"""
import __builtin__
import base64
import collections
import io
Expand All @@ -23,6 +22,8 @@
from jsonpickle.compat import long
from jsonpickle.compat import PY3

if not PY3:
import __builtin__

SEQUENCES = (list, set, tuple)
SEQUENCES_SET = set(SEQUENCES)
Expand Down Expand Up @@ -271,9 +272,12 @@ def is_list_like(obj):


def is_iterator(obj):
is_file = False
if not PY3:
is_file = isinstance(obj, __builtin__.file)

return (isinstance(obj, collections.Iterator) and
not isinstance(obj, io.IOBase) and
not isinstance(obj, __builtin__.file))
not isinstance(obj, io.IOBase) and not is_file)


def is_reducible(obj):
Expand Down
51 changes: 44 additions & 7 deletions tests/jsonpickle_test.py
Expand Up @@ -17,7 +17,7 @@
from jsonpickle import tags, util
from jsonpickle.compat import unicode
from jsonpickle.compat import unichr
from jsonpickle.compat import PY32
from jsonpickle.compat import PY32, PY3


class Thing(object):
Expand Down Expand Up @@ -776,7 +776,7 @@ def __newobj__(lol, fail):
class PickleProtocol2ReduceNewobj(PickleProtocol2ReduceTupleFunc):

def __new__(cls, *args):
inst = super(cls, cls).__new__(cls, *args)
inst = super(cls, cls).__new__(cls)
inst.newargs = args
return inst

Expand Down Expand Up @@ -873,30 +873,69 @@ def __reduce__(self):
def __setitem__(self, k, v):
return self.inner.__setitem__(k, v)

class PickleProtocol2Classic:

def __init__(self, foo):
self.foo = foo


class PickleProtocol2ClassicInitargs:

def __init__(self, foo, bar=None):
self.foo = foo
if bar:
self.bar=bar

def __getinitargs__(self):
return ('choo', 'choo')


class PicklingProtocol2TestCase(unittest.TestCase):

def test_classic_init_has_args(self):
"""
Test unpickling a classic instance whose init takes args,
has no __getinitargs__
Because classic only exists under 2, skipped if PY3
"""
if PY3:
self.skipTest('No classic classes in PY3')
instance = PickleProtocol2Classic(3)
encoded = jsonpickle.encode(instance)
decoded = jsonpickle.decode(encoded)
self.assertEqual(decoded.foo, 3)

def test_getinitargs(self):
"""
Test __getinitargs__ with classic instance
Because classic only exists under 2, skipped if PY3
"""
if PY3:
self.skipTest('No classic classes in PY3')
instance = PickleProtocol2ClassicInitargs(3)
encoded = jsonpickle.encode(instance)
decoded = jsonpickle.decode(encoded)
self.assertEqual(decoded.bar, 'choo')

def test_reduce_dictitems(self):
'Test reduce with dictitems set (as a generator)'
instance = PickleProtocol2ReduceDictitems()
encoded = jsonpickle.encode(instance)
print encoded
decoded = jsonpickle.decode(encoded)
self.assertEqual(decoded.inner, {u'foo': u'foo', u'bar': u'bar'})

def test_reduce_listitems_extend(self):
'Test reduce with listitems set (as a generator), yielding single items'
instance = PickleProtocol2ReduceListitemsExtend()
encoded = jsonpickle.encode(instance)
# print encoded
decoded = jsonpickle.decode(encoded)
self.assertEqual(decoded.inner, ['foo', 'bar'])

def test_reduce_listitems_append(self):
'Test reduce with listitems set (as a generator), yielding single items'
instance = PickleProtocol2ReduceListitemsAppend()
encoded = jsonpickle.encode(instance)
# print encoded
decoded = jsonpickle.decode(encoded)
self.assertEqual(decoded.inner, ['foo', 'bar'])

Expand Down Expand Up @@ -962,15 +1001,13 @@ def test_reduce_newobj(self):
instance = PickleProtocol2ReduceNewobj(5)
encoded = jsonpickle.encode(instance)
decoded = jsonpickle.decode(encoded)
print encoded
self.assertEqual(decoded.newargs, ('yam', 1))

def test_reduce_iter(self):
instance = iter('123')
self.assertTrue(util.is_iterator(instance))
encoded = jsonpickle.encode(instance)
decoded = jsonpickle.decode(encoded)
print encoded
self.assertEqual(next(decoded), '1')
self.assertEqual(next(decoded), '2')
self.assertEqual(next(decoded), '3')
Expand Down

0 comments on commit 026220f

Please sign in to comment.