1# Copyright (C) 2008 John Paulett (john -at- paulett.org)
2# Copyright (C) 2009-2021 David Aguilar (davvid -at- gmail.com)
3# All rights reserved.
4#
5# This software is licensed as described in the file COPYING, which
6# you should have received as part of this distribution.
7from __future__ import absolute_import, division, unicode_literals
8import os
9import unittest
10import collections
11
12import pytest
13
14import jsonpickle
15import jsonpickle.backend
16import jsonpickle.handlers
17from jsonpickle import compat, tags, util
18from jsonpickle.compat import PY2, PY3
19from helper import SkippableTest
20
21
22class Thing(object):
23    def __init__(self, name):
24        self.name = name
25        self.child = None
26
27    def __repr__(self):
28        return 'Thing("%s")' % self.name
29
30
31class Capture(object):
32    def __init__(self, *args, **kwargs):
33        self.args = args
34        self.kwargs = kwargs
35
36
37class ThingWithProps(object):
38    def __init__(self, name='', dogs='reliable', monkies='tricksy'):
39        self.name = name
40        self._critters = (('dogs', dogs), ('monkies', monkies))
41
42    def _get_identity(self):
43        keys = [self.dogs, self.monkies, self.name]
44        return hash('-'.join([str(key) for key in keys]))
45
46    identity = property(_get_identity)
47
48    def _get_dogs(self):
49        return self._critters[0][1]
50
51    dogs = property(_get_dogs)
52
53    def _get_monkies(self):
54        return self._critters[1][1]
55
56    monkies = property(_get_monkies)
57
58    def __getstate__(self):
59        out = dict(
60            __identity__=self.identity,
61            nom=self.name,
62            dogs=self.dogs,
63            monkies=self.monkies,
64        )
65        return out
66
67    def __setstate__(self, state_dict):
68        self._critters = (
69            ('dogs', state_dict.get('dogs')),
70            ('monkies', state_dict.get('monkies')),
71        )
72        self.name = state_dict.get('nom', '')
73        ident = state_dict.get('__identity__')
74        if ident != self.identity:
75            raise ValueError('expanded object does not match originial state!')
76
77    def __eq__(self, other):
78        return self.identity == other.identity
79
80
81class UserDict(dict):
82    """A user class that inherits from :class:`dict`"""
83
84    def __init__(self, **kwargs):
85        dict.__init__(self, **kwargs)
86        self.valid = False
87
88
89class Outer(object):
90    class Middle(object):
91        class Inner(object):
92            pass
93
94
95class PicklingTestCase(unittest.TestCase):
96    def setUp(self):
97        self.pickler = jsonpickle.pickler.Pickler()
98        self.unpickler = jsonpickle.unpickler.Unpickler()
99        self.b85_pickler = jsonpickle.pickler.Pickler(use_base85=True)
100
101    def tearDown(self):
102        self.pickler.reset()
103        self.unpickler.reset()
104
105    @unittest.skipIf(not PY2, 'Python 2-specific base85 test')
106    def test_base85_always_false_on_py2(self):
107        self.assertFalse(self.b85_pickler.use_base85)
108
109    @unittest.skipIf(PY2, 'Base85 not supported on Python 2')
110    def test_base85_override_py3(self):
111        """Ensure the Python 2 check still lets us set use_base85 on Python 3"""
112        self.assertTrue(self.b85_pickler.use_base85)
113
114    @unittest.skipIf(PY2, 'Base85 not supported on Python 2')
115    def test_bytes_default_base85(self):
116        data = os.urandom(16)
117        encoded = util.b85encode(data)
118        self.assertEqual({tags.B85: encoded}, self.b85_pickler.flatten(data))
119
120    @unittest.skipIf(PY2, 'Base85 not supported on Python 2')
121    def test_py3_bytes_base64_default(self):
122        data = os.urandom(16)
123        encoded = util.b64encode(data)
124        self.assertEqual({tags.B64: encoded}, self.pickler.flatten(data))
125
126    @unittest.skipIf(not PY2, 'Python 2-specific base64 test')
127    def test_py2_default_base64(self):
128        data = os.urandom(16)
129        encoded = util.b64encode(data)
130        self.assertEqual({tags.B64: encoded}, self.pickler.flatten(data))
131
132    @unittest.skipIf(PY2, 'Base85 not supported on Python 2')
133    def test_decode_base85(self):
134        pickled = {tags.B85: 'P{Y4;Xv4O{u^=-c'}
135        expected = u'P\u00ffth\u00f6\u00f1 3!'.encode('utf-8')
136        self.assertEqual(expected, self.unpickler.restore(pickled))
137
138    @unittest.skipIf(PY2, 'Base85 not supported on Python 2')
139    def test_base85_still_handles_base64(self):
140        pickled = {tags.B64: 'UMO/dGjDtsOxIDMh'}
141        expected = u'P\u00ffth\u00f6\u00f1 3!'.encode('utf-8')
142        self.assertEqual(expected, self.unpickler.restore(pickled))
143
144    @unittest.skipIf(not PY2, 'Python 2-specific base85 test')
145    def test_base85_crashes_py2(self):
146        with self.assertRaises(NotImplementedError):
147            self.unpickler.restore({tags.B85: 'P{Y4;Xv4O{u^=-c'})
148
149    def test_string(self):
150        self.assertEqual('a string', self.pickler.flatten('a string'))
151        self.assertEqual('a string', self.unpickler.restore('a string'))
152
153    def test_unicode(self):
154        self.assertEqual('a string', self.pickler.flatten('a string'))
155        self.assertEqual('a string', self.unpickler.restore('a string'))
156
157    def test_int(self):
158        self.assertEqual(3, self.pickler.flatten(3))
159        self.assertEqual(3, self.unpickler.restore(3))
160
161    def test_float(self):
162        self.assertEqual(3.5, self.pickler.flatten(3.5))
163        self.assertEqual(3.5, self.unpickler.restore(3.5))
164
165    def test_boolean(self):
166        self.assertTrue(self.pickler.flatten(True))
167        self.assertFalse(self.pickler.flatten(False))
168        self.assertTrue(self.unpickler.restore(True))
169        self.assertFalse(self.unpickler.restore(False))
170
171    def test_none(self):
172        self.assertTrue(self.pickler.flatten(None) is None)
173        self.assertTrue(self.unpickler.restore(None) is None)
174
175    def test_list(self):
176        # multiple types of values
177        listA = [1, 35.0, 'value']
178        self.assertEqual(listA, self.pickler.flatten(listA))
179        self.assertEqual(listA, self.unpickler.restore(listA))
180        # nested list
181        listB = [40, 40, listA, 6]
182        self.assertEqual(listB, self.pickler.flatten(listB))
183        self.assertEqual(listB, self.unpickler.restore(listB))
184        # 2D list
185        listC = [[1, 2], [3, 4]]
186        self.assertEqual(listC, self.pickler.flatten(listC))
187        self.assertEqual(listC, self.unpickler.restore(listC))
188        # empty list
189        listD = []
190        self.assertEqual(listD, self.pickler.flatten(listD))
191        self.assertEqual(listD, self.unpickler.restore(listD))
192
193    def test_set(self):
194        setlist = ['orange', 'apple', 'grape']
195        setA = set(setlist)
196
197        flattened = self.pickler.flatten(setA)
198        for s in setlist:
199            self.assertTrue(s in flattened[tags.SET])
200
201        setA_pickle = {tags.SET: setlist}
202        self.assertEqual(setA, self.unpickler.restore(setA_pickle))
203
204    def test_dict(self):
205        dictA = {'key1': 1.0, 'key2': 20, 'key3': 'thirty', tags.JSON_KEY + '6': 6}
206        self.assertEqual(dictA, self.pickler.flatten(dictA))
207        self.assertEqual(dictA, self.unpickler.restore(dictA))
208        dictB = {}
209        self.assertEqual(dictB, self.pickler.flatten(dictB))
210        self.assertEqual(dictB, self.unpickler.restore(dictB))
211
212    def test_tuple(self):
213        # currently all collections are converted to lists
214        tupleA = (4, 16, 32)
215        tupleA_pickle = {tags.TUPLE: [4, 16, 32]}
216        self.assertEqual(tupleA_pickle, self.pickler.flatten(tupleA))
217        self.assertEqual(tupleA, self.unpickler.restore(tupleA_pickle))
218        tupleB = (4,)
219        tupleB_pickle = {tags.TUPLE: [4]}
220        self.assertEqual(tupleB_pickle, self.pickler.flatten(tupleB))
221        self.assertEqual(tupleB, self.unpickler.restore(tupleB_pickle))
222
223    def test_tuple_roundtrip(self):
224        data = (1, 2, 3)
225        newdata = jsonpickle.decode(jsonpickle.encode(data))
226        self.assertEqual(data, newdata)
227
228    def test_set_roundtrip(self):
229        data = {1, 2, 3}
230        newdata = jsonpickle.decode(jsonpickle.encode(data))
231        self.assertEqual(data, newdata)
232
233    def test_list_roundtrip(self):
234        data = [1, 2, 3]
235        newdata = jsonpickle.decode(jsonpickle.encode(data))
236        self.assertEqual(data, newdata)
237
238    def test_class(self):
239        inst = Thing('test name')
240        inst.child = Thing('child name')
241
242        flattened = self.pickler.flatten(inst)
243        self.assertEqual('test name', flattened['name'])
244        child = flattened['child']
245        self.assertEqual('child name', child['name'])
246
247        inflated = self.unpickler.restore(flattened)
248        self.assertEqual('test name', inflated.name)
249        self.assertTrue(type(inflated) is Thing)
250        self.assertEqual('child name', inflated.child.name)
251        self.assertTrue(type(inflated.child) is Thing)
252
253    def test_classlist(self):
254        array = [Thing('one'), Thing('two'), 'a string']
255
256        flattened = self.pickler.flatten(array)
257        self.assertEqual('one', flattened[0]['name'])
258        self.assertEqual('two', flattened[1]['name'])
259        self.assertEqual('a string', flattened[2])
260
261        inflated = self.unpickler.restore(flattened)
262        self.assertEqual('one', inflated[0].name)
263        self.assertTrue(type(inflated[0]) is Thing)
264        self.assertEqual('two', inflated[1].name)
265        self.assertTrue(type(inflated[1]) is Thing)
266        self.assertEqual('a string', inflated[2])
267
268    def test_classdict(self):
269        dict = {'k1': Thing('one'), 'k2': Thing('two'), 'k3': 3}
270
271        flattened = self.pickler.flatten(dict)
272        self.assertEqual('one', flattened['k1']['name'])
273        self.assertEqual('two', flattened['k2']['name'])
274        self.assertEqual(3, flattened['k3'])
275
276        inflated = self.unpickler.restore(flattened)
277        self.assertEqual('one', inflated['k1'].name)
278        self.assertTrue(type(inflated['k1']) is Thing)
279        self.assertEqual('two', inflated['k2'].name)
280        self.assertTrue(type(inflated['k2']) is Thing)
281        self.assertEqual(3, inflated['k3'])
282
283    def test_recursive(self):
284        """create a recursive structure and test that we can handle it"""
285        parent = Thing('parent')
286        child = Thing('child')
287        child.sibling = Thing('sibling')
288
289        parent.self = parent
290        parent.child = child
291        parent.child.twin = child
292        parent.child.parent = parent
293        parent.child.sibling.parent = parent
294
295        cloned = jsonpickle.decode(jsonpickle.encode(parent))
296
297        self.assertEqual(parent.name, cloned.name)
298        self.assertEqual(parent.child.name, cloned.child.name)
299        self.assertEqual(parent.child.sibling.name, cloned.child.sibling.name)
300        self.assertEqual(cloned, cloned.child.parent)
301        self.assertEqual(cloned, cloned.child.sibling.parent)
302        self.assertEqual(cloned, cloned.child.twin.parent)
303        self.assertEqual(cloned.child, cloned.child.twin)
304
305    def test_tuple_notunpicklable(self):
306        self.pickler.unpicklable = False
307
308        flattened = self.pickler.flatten(('one', 2, 3))
309        self.assertEqual(flattened, ['one', 2, 3])
310
311    def test_set_not_unpicklable(self):
312        self.pickler.unpicklable = False
313
314        flattened = self.pickler.flatten({'one', 2, 3})
315        self.assertTrue('one' in flattened)
316        self.assertTrue(2 in flattened)
317        self.assertTrue(3 in flattened)
318        self.assertTrue(isinstance(flattened, list))
319
320    def test_thing_with_module(self):
321        obj = Thing('with-module')
322        obj.themodule = os
323
324        flattened = self.pickler.flatten(obj)
325        inflated = self.unpickler.restore(flattened)
326        self.assertEqual(inflated.themodule, os)
327
328    def test_thing_with_module_safe(self):
329        obj = Thing('with-module')
330        obj.themodule = os
331        flattened = self.pickler.flatten(obj)
332        self.unpickler.safe = True
333        inflated = self.unpickler.restore(flattened)
334        self.assertEqual(inflated.themodule, None)
335
336    def test_thing_with_submodule(self):
337        from distutils import sysconfig
338
339        obj = Thing('with-submodule')
340        obj.submodule = sysconfig
341
342        flattened = self.pickler.flatten(obj)
343        inflated = self.unpickler.restore(flattened)
344        self.assertEqual(inflated.submodule, sysconfig)
345
346    def test_type_reference(self):
347        """This test ensures that users can store references to types."""
348        obj = Thing('object-with-type-reference')
349
350        # reference the built-in 'object' type
351        obj.typeref = object
352
353        flattened = self.pickler.flatten(obj)
354        self.assertEqual(flattened['typeref'], {tags.TYPE: 'builtins.object'})
355
356        inflated = self.unpickler.restore(flattened)
357        self.assertEqual(inflated.typeref, object)
358
359    def test_class_reference(self):
360        """This test ensures that users can store references to classes."""
361        obj = Thing('object-with-class-reference')
362
363        # reference the 'Thing' class (not an instance of the class)
364        obj.classref = Thing
365
366        flattened = self.pickler.flatten(obj)
367        self.assertEqual(flattened['classref'], {tags.TYPE: 'jsonpickle_test.Thing'})
368
369        inflated = self.unpickler.restore(flattened)
370        self.assertEqual(inflated.classref, Thing)
371
372    def test_supports_getstate_setstate(self):
373        obj = ThingWithProps('object-which-defines-getstate-setstate')
374        flattened = self.pickler.flatten(obj)
375        self.assertTrue(flattened[tags.STATE].get('__identity__'))
376        self.assertTrue(flattened[tags.STATE].get('nom'))
377        inflated = self.unpickler.restore(flattened)
378        self.assertEqual(obj, inflated)
379
380    def test_references(self):
381        obj_a = Thing('foo')
382        obj_b = Thing('bar')
383        coll = [obj_a, obj_b, obj_b]
384        flattened = self.pickler.flatten(coll)
385        inflated = self.unpickler.restore(flattened)
386        self.assertEqual(len(inflated), len(coll))
387        for x in range(len(coll)):
388            self.assertEqual(repr(coll[x]), repr(inflated[x]))
389
390    def test_references_in_number_keyed_dict(self):
391        """
392        Make sure a dictionary with numbers as keys and objects as values
393        can make the round trip.
394
395        Because JSON must coerce integers to strings in dict keys, the sort
396        order may have a tendency to change between pickling and unpickling,
397        and this could affect the object references.
398        """
399        one = Thing('one')
400        two = Thing('two')
401        twelve = Thing('twelve')
402        two.child = twelve
403        obj = {
404            1: one,
405            2: two,
406            12: twelve,
407        }
408        self.assertNotEqual(
409            list(sorted(obj.keys())), list(map(int, sorted(map(str, obj.keys()))))
410        )
411        flattened = self.pickler.flatten(obj)
412        inflated = self.unpickler.restore(flattened)
413        self.assertEqual(len(inflated), 3)
414        self.assertEqual(inflated['12'].name, 'twelve')
415
416    def test_builtin_error(self):
417        expect = AssertionError
418        json = jsonpickle.encode(expect)
419        actual = jsonpickle.decode(json)
420        self.assertEqual(expect, actual)
421        self.assertTrue(expect is actual)
422
423    def test_builtin_function(self):
424        expect = dir
425        json = jsonpickle.encode(expect)
426        actual = jsonpickle.decode(json)
427        self.assertEqual(expect, actual)
428        self.assertTrue(expect is actual)
429
430    def test_restore_legacy_builtins(self):
431        """
432        jsonpickle 0.9.6 and earlier used the Python 2 `__builtin__`
433        naming for builtins. Ensure those can be loaded until they're
434        no longer supported.
435        """
436        ae = jsonpickle.decode('{"py/type": "__builtin__.AssertionError"}')
437        assert ae is AssertionError
438        ae = jsonpickle.decode('{"py/type": "exceptions.AssertionError"}')
439        assert ae is AssertionError
440        cls = jsonpickle.decode('{"py/type": "__builtin__.int"}')
441        assert cls is int
442
443
444class JSONPickleTestCase(SkippableTest):
445    def setUp(self):
446        self.obj = Thing('A name')
447        self.expected_json = (
448            '{"%s": "jsonpickle_test.Thing", "name": "A name", "child": null}'
449            % tags.OBJECT
450        )
451
452    def test_API_names(self):
453        """
454        Enforce expected names in main module
455        """
456        names = list(vars(jsonpickle))
457        self.assertIn('pickler', names)
458        self.assertIn('unpickler', names)
459        self.assertIn('JSONBackend', names)
460        self.assertIn('__version__', names)
461        self.assertIn('register', names)
462        self.assertIn('unregister', names)
463        self.assertIn('Pickler', names)
464        self.assertIn('Unpickler', names)
465        self.assertIn('encode', names)
466        self.assertIn('decode', names)
467
468    def test_encode(self):
469        expect = self.obj
470        pickle = jsonpickle.encode(self.obj)
471        actual = jsonpickle.decode(pickle)
472        self.assertEqual(expect.name, actual.name)
473        self.assertEqual(expect.child, actual.child)
474
475    def test_encode_notunpicklable(self):
476        expect = {'name': 'A name', 'child': None}
477        pickle = jsonpickle.encode(self.obj, unpicklable=False)
478        actual = jsonpickle.decode(pickle)
479        self.assertEqual(expect['name'], actual['name'])
480
481    def test_decode(self):
482        actual = jsonpickle.decode(self.expected_json)
483        self.assertEqual(self.obj.name, actual.name)
484        self.assertEqual(type(self.obj), type(actual))
485
486    def test_json(self):
487        expect = self.obj
488        pickle = jsonpickle.encode(self.obj)
489        actual = jsonpickle.decode(pickle)
490        self.assertEqual(actual.name, expect.name)
491        self.assertEqual(actual.child, expect.child)
492
493        actual = jsonpickle.decode(self.expected_json)
494        self.assertEqual(self.obj.name, actual.name)
495        self.assertEqual(type(self.obj), type(actual))
496
497    def test_unicode_dict_keys(self):
498        if PY2:
499            uni = unichr(0x1234)  # noqa
500        else:
501            uni = chr(0x1234)
502        pickle = jsonpickle.encode({uni: uni})
503        actual = jsonpickle.decode(pickle)
504        self.assertTrue(uni in actual)
505        self.assertEqual(actual[uni], uni)
506
507    def test_tuple_dict_keys_default(self):
508        """Test that we handle dictionaries with tuples as keys."""
509        tuple_dict = {(1, 2): 3, (4, 5): {(7, 8): 9}}
510        pickle = jsonpickle.encode(tuple_dict)
511        expect = {'(1, 2)': 3, '(4, 5)': {'(7, 8)': 9}}
512        actual = jsonpickle.decode(pickle)
513        self.assertEqual(expect, actual)
514
515        tuple_dict = {(1, 2): [1, 2]}
516        pickle = jsonpickle.encode(tuple_dict)
517        actual = jsonpickle.decode(pickle)
518        self.assertEqual(actual['(1, 2)'], [1, 2])
519
520    def test_tuple_dict_keys_with_keys_enabled(self):
521        """Test that we handle dictionaries with tuples as keys."""
522        tuple_dict = {(1, 2): 3, (4, 5): {(7, 8): 9}}
523        pickle = jsonpickle.encode(tuple_dict, keys=True)
524        expect = tuple_dict
525        actual = jsonpickle.decode(pickle, keys=True)
526        self.assertEqual(expect, actual)
527
528        tuple_dict = {(1, 2): [1, 2]}
529        pickle = jsonpickle.encode(tuple_dict, keys=True)
530        actual = jsonpickle.decode(pickle, keys=True)
531        self.assertEqual(actual[(1, 2)], [1, 2])
532
533    def test_None_dict_key_default(self):
534        # We do string coercion for non-string keys so None becomes 'None'
535        expect = {'null': None}
536        obj = {None: None}
537        pickle = jsonpickle.encode(obj)
538        actual = jsonpickle.decode(pickle)
539        self.assertEqual(expect, actual)
540
541    def test_None_dict_key_with_keys_enabled(self):
542        expect = {None: None}
543        obj = {None: None}
544        pickle = jsonpickle.encode(obj, keys=True)
545        actual = jsonpickle.decode(pickle, keys=True)
546        self.assertEqual(expect, actual)
547
548    def test_object_dict_keys(self):
549        """Test that we handle random objects as keys."""
550        thing = Thing('random')
551        pickle = jsonpickle.encode({thing: True})
552        actual = jsonpickle.decode(pickle)
553        self.assertEqual(actual, {'Thing("random")': True})
554
555    def test_int_dict_keys_defaults(self):
556        int_dict = {1000: [1, 2]}
557        pickle = jsonpickle.encode(int_dict)
558        actual = jsonpickle.decode(pickle)
559        self.assertEqual(actual['1000'], [1, 2])
560
561    def test_int_dict_keys_with_keys_enabled(self):
562        int_dict = {1000: [1, 2]}
563        pickle = jsonpickle.encode(int_dict, keys=True)
564        actual = jsonpickle.decode(pickle, keys=True)
565        self.assertEqual(actual[1000], [1, 2])
566
567    def test_string_key_requiring_escape_dict_keys_with_keys_enabled(self):
568        json_key_dict = {tags.JSON_KEY + '6': [1, 2]}
569        pickled = jsonpickle.encode(json_key_dict, keys=True)
570        unpickled = jsonpickle.decode(pickled, keys=True)
571        self.assertEqual(unpickled[tags.JSON_KEY + '6'], [1, 2])
572
573    def test_string_key_not_requiring_escape_dict_keys_with_keys_enabled(self):
574        """test that string keys that do not require escaping are not escaped"""
575        str_dict = {'name': [1, 2]}
576        pickled = jsonpickle.encode(str_dict, keys=True)
577        unpickled = jsonpickle.decode(pickled)
578        self.assertTrue('name' in unpickled)
579
580    def test_dict_subclass(self):
581        obj = UserDict()
582        obj.valid = True
583        obj.s = 'string'
584        obj.d = 'd_string'
585        obj['d'] = {}
586        obj['s'] = 'test'
587        pickle = jsonpickle.encode(obj)
588        actual = jsonpickle.decode(pickle)
589
590        self.assertEqual(type(actual), UserDict)
591        self.assertTrue('d' in actual)
592        self.assertTrue('s' in actual)
593        self.assertTrue(hasattr(actual, 'd'))
594        self.assertTrue(hasattr(actual, 's'))
595        self.assertTrue(hasattr(actual, 'valid'))
596        self.assertEqual(obj['d'], actual['d'])
597        self.assertEqual(obj['s'], actual['s'])
598        self.assertEqual(obj.d, actual.d)
599        self.assertEqual(obj.s, actual.s)
600        self.assertEqual(obj.valid, actual.valid)
601
602    def test_list_of_objects(self):
603        """Test that objects in lists are referenced correctly"""
604        a = Thing('a')
605        b = Thing('b')
606        pickle = jsonpickle.encode([a, b, b])
607        actual = jsonpickle.decode(pickle)
608        self.assertEqual(actual[1], actual[2])
609        self.assertEqual(type(actual[0]), Thing)
610        self.assertEqual(actual[0].name, 'a')
611        self.assertEqual(actual[1].name, 'b')
612        self.assertEqual(actual[2].name, 'b')
613
614    def test_refs_keys_values(self):
615        """Test that objects in dict keys are referenced correctly"""
616        j = Thing('random')
617        object_dict = {j: j}
618        pickle = jsonpickle.encode(object_dict, keys=True)
619        actual = jsonpickle.decode(pickle, keys=True)
620        self.assertEqual(list(actual.keys()), list(actual.values()))
621
622    def test_object_keys_to_list(self):
623        """Test that objects in dict values are referenced correctly"""
624        j = Thing('random')
625        object_dict = {j: [j, j]}
626        pickle = jsonpickle.encode(object_dict, keys=True)
627        actual = jsonpickle.decode(pickle, keys=True)
628        obj = list(actual.keys())[0]
629        self.assertEqual(j.name, obj.name)
630        self.assertTrue(obj is actual[obj][0])
631        self.assertTrue(obj is actual[obj][1])
632
633    def test_refs_in_objects(self):
634        """Test that objects in lists are referenced correctly"""
635        a = Thing('a')
636        b = Thing('b')
637        pickle = jsonpickle.encode([a, b, b])
638        actual = jsonpickle.decode(pickle)
639        self.assertNotEqual(actual[0], actual[1])
640        self.assertEqual(actual[1], actual[2])
641        self.assertTrue(actual[1] is actual[2])
642
643    def test_refs_recursive(self):
644        """Test that complicated recursive refs work"""
645
646        a = Thing('a')
647        a.self_list = [Thing('0'), Thing('1'), Thing('2')]
648        a.first = a.self_list[0]
649        a.stuff = {a.first: a.first}
650        a.morestuff = {a.self_list[1]: a.stuff}
651
652        pickle = jsonpickle.encode(a, keys=True)
653        b = jsonpickle.decode(pickle, keys=True)
654
655        item = b.self_list[0]
656        self.assertEqual(b.first, item)
657        self.assertEqual(b.stuff[b.first], item)
658        self.assertEqual(b.morestuff[b.self_list[1]][b.first], item)
659
660    def test_load_backend(self):
661        """Test that we can call jsonpickle.load_backend()"""
662        jsonpickle.load_backend('simplejson', 'dumps', 'loads', ValueError)
663        self.assertTrue(True)
664
665    def test_set_preferred_backend_allows_magic(self):
666        """Tests that we can use the pluggable backends magically"""
667        backend = 'os.path'
668        jsonpickle.load_backend(backend, 'split', 'join', AttributeError)
669        jsonpickle.set_preferred_backend(backend)
670
671        slash_hello, world = jsonpickle.encode('/hello/world')
672        jsonpickle.remove_backend(backend)
673
674        self.assertEqual(slash_hello, '/hello')
675        self.assertEqual(world, 'world')
676
677    def test_load_backend_submodule(self):
678        """Test that we can load a submodule as a backend"""
679        jsonpickle.load_backend('os.path', 'split', 'join', AttributeError)
680        self.assertTrue(
681            'os.path' in jsonpickle.json._backend_names
682            and 'os.path' in jsonpickle.json._encoders
683            and 'os.path' in jsonpickle.json._decoders
684            and 'os.path' in jsonpickle.json._encoder_options
685            and 'os.path' in jsonpickle.json._decoder_exceptions
686        )
687        jsonpickle.remove_backend('os.path')
688
689    def _backend_is_partially_loaded(self, backend):
690        """Return True if the specified backend is incomplete"""
691        return (
692            backend in jsonpickle.json._backend_names
693            or backend in jsonpickle.json._encoders
694            or backend in jsonpickle.json._decoders
695            or backend in jsonpickle.json._encoder_options
696            or backend in jsonpickle.json._decoder_exceptions
697        )
698
699    def test_load_backend_handles_bad_encode(self):
700        """Test that we ignore bad encoders"""
701
702        load_backend = jsonpickle.load_backend
703        self.assertFalse(load_backend('os.path', 'bad!', 'split', AttributeError))
704        self.assertFalse(self._backend_is_partially_loaded('os.path'))
705
706    def test_load_backend_raises_on_bad_decode(self):
707        """Test that we ignore bad decoders"""
708
709        load_backend = jsonpickle.load_backend
710        self.assertFalse(load_backend('os.path', 'join', 'bad!', AttributeError))
711        self.assertFalse(self._backend_is_partially_loaded('os.path'))
712
713    def test_load_backend_handles_bad_loads_exc(self):
714        """Test that we ignore bad decoder exceptions"""
715
716        load_backend = jsonpickle.load_backend
717        self.assertFalse(load_backend('os.path', 'join', 'split', 'bad!'))
718        self.assertFalse(self._backend_is_partially_loaded('os.path'))
719
720    def test_list_item_reference(self):
721        thing = Thing('parent')
722        thing.child = Thing('child')
723        thing.child.refs = [thing]
724
725        encoded = jsonpickle.encode(thing)
726        decoded = jsonpickle.decode(encoded)
727
728        self.assertEqual(id(decoded.child.refs[0]), id(decoded))
729
730    def test_reference_to_list(self):
731        thing = Thing('parent')
732        thing.a = [1]
733        thing.b = thing.a
734        thing.b.append(thing.a)
735        thing.b.append([thing.a])
736
737        encoded = jsonpickle.encode(thing)
738        decoded = jsonpickle.decode(encoded)
739
740        self.assertEqual(decoded.a[0], 1)
741        self.assertEqual(decoded.b[0], 1)
742        self.assertEqual(id(decoded.a), id(decoded.b))
743        self.assertEqual(id(decoded.a), id(decoded.a[1]))
744        self.assertEqual(id(decoded.a), id(decoded.a[2][0]))
745
746    def test_make_refs_disabled_list(self):
747        obj_a = Thing('foo')
748        obj_b = Thing('bar')
749        coll = [obj_a, obj_b, obj_b]
750        encoded = jsonpickle.encode(coll, make_refs=False)
751        decoded = jsonpickle.decode(encoded)
752
753        self.assertEqual(len(decoded), 3)
754        self.assertTrue(decoded[0] is not decoded[1])
755        self.assertTrue(decoded[1] is not decoded[2])
756
757    def test_make_refs_disabled_reference_to_list(self):
758        thing = Thing('parent')
759        thing.a = [1]
760        thing.b = thing.a
761        thing.b.append(thing.a)
762        thing.b.append([thing.a])
763
764        encoded = jsonpickle.encode(thing, make_refs=False)
765        decoded = jsonpickle.decode(encoded)
766
767        assert decoded.a[0] == 1
768        assert decoded.b[0] == 1
769        assert decoded.a[1][0:3] == '[1,'
770        assert decoded.a[2][0][0:3] == '[1,'
771
772    def test_can_serialize_inner_classes(self):
773        class InnerScope(object):
774            """Private class visible to this method only"""
775
776            def __init__(self, name):
777                self.name = name
778
779        obj = InnerScope('test')
780        encoded = jsonpickle.encode(obj)
781        # Single class
782        decoded = jsonpickle.decode(encoded, classes=InnerScope)
783        self._test_inner_class(InnerScope, obj, decoded)
784        # List of classes
785        decoded = jsonpickle.decode(encoded, classes=[InnerScope])
786        self._test_inner_class(InnerScope, obj, decoded)
787        # Tuple of classes
788        decoded = jsonpickle.decode(encoded, classes=(InnerScope,))
789        self._test_inner_class(InnerScope, obj, decoded)
790        # Set of classes
791        decoded = jsonpickle.decode(encoded, classes={InnerScope})
792        self._test_inner_class(InnerScope, obj, decoded)
793
794    def _test_inner_class(self, InnerScope, obj, decoded):
795        self.assertTrue(isinstance(obj, InnerScope))
796        self.assertEqual(decoded.name, obj.name)
797
798    def test_can_serialize_nested_classes(self):
799        if PY2:
800            return self.skip('Serialization of nested classes requires ' 'Python >= 3')
801
802        middle = Outer.Middle
803        inner = Outer.Middle.Inner
804        encoded_middle = jsonpickle.encode(middle)
805        encoded_inner = jsonpickle.encode(inner)
806
807        decoded_middle = jsonpickle.decode(encoded_middle)
808        decoded_inner = jsonpickle.decode(encoded_inner)
809
810        self.assertTrue(isinstance(decoded_middle, type))
811        self.assertTrue(isinstance(decoded_inner, type))
812        self.assertEqual(decoded_middle, middle)
813        self.assertEqual(decoded_inner, inner)
814
815    def test_can_serialize_nested_class_objects(self):
816        if PY2:
817            return self.skip('Serialization of nested classes requires ' 'Python >= 3')
818
819        middle_obj = Outer.Middle()
820        middle_obj.attribute = 5
821        inner_obj = Outer.Middle.Inner()
822        inner_obj.attribute = 6
823        encoded_middle_obj = jsonpickle.encode(middle_obj)
824        encoded_inner_obj = jsonpickle.encode(inner_obj)
825
826        decoded_middle_obj = jsonpickle.decode(encoded_middle_obj)
827        decoded_inner_obj = jsonpickle.decode(encoded_inner_obj)
828
829        self.assertTrue(isinstance(decoded_middle_obj, Outer.Middle))
830        self.assertTrue(isinstance(decoded_inner_obj, Outer.Middle.Inner))
831        self.assertEqual(decoded_middle_obj.attribute, middle_obj.attribute)
832        self.assertEqual(decoded_inner_obj.attribute, inner_obj.attribute)
833
834
835class PicklableNamedTuple(object):
836    """
837    A picklable namedtuple wrapper, to demonstrate the need
838    for protocol 2 compatibility. Yes, this is contrived in
839    its use of new, but it demonstrates the issue.
840    """
841
842    def __new__(cls, propnames, vals):
843        # it's necessary to use the correct class name for class resolution
844        # classes that fake their own names may never be unpicklable
845        ntuple = collections.namedtuple(cls.__name__, propnames)
846        ntuple.__getnewargs__ = lambda self: (propnames, vals)
847        instance = ntuple.__new__(ntuple, *vals)
848        return instance
849
850
851class PicklableNamedTupleEx(object):
852    """
853    A picklable namedtuple wrapper, to demonstrate the need
854    for protocol 4 compatibility. Yes, this is contrived in
855    its use of new, but it demonstrates the issue.
856    """
857
858    def __getnewargs__(self):
859        raise NotImplementedError("This class needs __getnewargs_ex__")
860
861    def __new__(cls, newargs=__getnewargs__, **kwargs):
862        # it's necessary to use the correct class name for class resolution
863        # classes that fake their own names may never be unpicklable
864        ntuple = collections.namedtuple(cls.__name__, sorted(kwargs.keys()))
865        ntuple.__getnewargs_ex__ = lambda self: ((), kwargs)
866        ntuple.__getnewargs__ = newargs
867        instance = ntuple.__new__(ntuple, *[b for a, b in sorted(kwargs.items())])
868        return instance
869
870
871class PickleProtocol2Thing(object):
872    def __init__(self, *args):
873        self.args = args
874
875    def __getnewargs__(self):
876        return self.args
877
878    def __eq__(self, other):
879        """
880        Make PickleProtocol2Thing('slotmagic') ==
881             PickleProtocol2Thing('slotmagic')
882        """
883        if self.__dict__ == other.__dict__ and dir(self) == dir(other):
884            for prop in dir(self):
885                selfprop = getattr(self, prop)
886                if not callable(selfprop) and prop[0] != '_':
887                    if selfprop != getattr(other, prop):
888                        return False
889            return True
890        else:
891            return False
892
893
894# these two instances are used below and in tests
895slotmagic = PickleProtocol2Thing('slotmagic')
896dictmagic = PickleProtocol2Thing('dictmagic')
897
898
899class PickleProtocol2GetState(PickleProtocol2Thing):
900    def __new__(cls, *args):
901        instance = super(PickleProtocol2GetState, cls).__new__(cls)
902        instance.newargs = args
903        return instance
904
905    def __getstate__(self):
906        return 'I am magic'
907
908
909class PickleProtocol2GetStateDict(PickleProtocol2Thing):
910    def __getstate__(self):
911        return {'magic': True}
912
913
914class PickleProtocol2GetStateSlots(PickleProtocol2Thing):
915    def __getstate__(self):
916        return (None, {'slotmagic': slotmagic})
917
918
919class PickleProtocol2GetStateSlotsDict(PickleProtocol2Thing):
920    def __getstate__(self):
921        return ({'dictmagic': dictmagic}, {'slotmagic': slotmagic})
922
923
924class PickleProtocol2GetSetState(PickleProtocol2GetState):
925    def __setstate__(self, state):
926        """
927        Contrived example, easy to test
928        """
929        if state == "I am magic":
930            self.magic = True
931        else:
932            self.magic = False
933
934
935class PickleProtocol2ChildThing(object):
936    def __init__(self, child):
937        self.child = child
938
939    def __getnewargs__(self):
940        return ([self.child],)
941
942
943class PickleProtocol2ReduceString(object):
944    def __reduce__(self):
945        return __name__ + '.slotmagic'
946
947
948class PickleProtocol2ReduceExString(object):
949    def __reduce__(self):
950        assert False, "Should not be here"
951
952    def __reduce_ex__(self, n):
953        return __name__ + '.slotmagic'
954
955
956class PickleProtocol2ReduceTuple(object):
957    def __init__(self, argval, optional=None):
958        self.argval = argval
959        self.optional = optional
960
961    def __reduce__(self):
962        return (
963            PickleProtocol2ReduceTuple,  # callable
964            ('yam', 1),  # args
965            None,  # state
966            iter([]),  # listitems
967            iter([]),  # dictitems
968        )
969
970
971@compat.iterator
972class ReducibleIterator(object):
973    def __iter__(self):
974        return self
975
976    def __next__(self):
977        raise StopIteration()
978
979    def __reduce__(self):
980        return ReducibleIterator, ()
981
982
983def protocol_2_reduce_tuple_func(*args):
984    return PickleProtocol2ReduceTupleFunc(*args)
985
986
987class PickleProtocol2ReduceTupleFunc(object):
988    def __init__(self, argval, optional=None):
989        self.argval = argval
990        self.optional = optional
991
992    def __reduce__(self):
993        return (
994            protocol_2_reduce_tuple_func,  # callable
995            ('yam', 1),  # args
996            None,  # state
997            iter([]),  # listitems
998            iter([]),  # dictitems
999        )
1000
1001
1002def __newobj__(lol, fail):
1003    """
1004    newobj is special-cased, such that it is not actually called
1005    """
1006
1007
1008class PickleProtocol2ReduceNewobj(PickleProtocol2ReduceTupleFunc):
1009    def __new__(cls, *args):
1010        inst = super(cls, cls).__new__(cls)
1011        inst.newargs = args
1012        return inst
1013
1014    def __reduce__(self):
1015        return (
1016            __newobj__,  # callable
1017            (PickleProtocol2ReduceNewobj, 'yam', 1),  # args
1018            None,  # state
1019            iter([]),  # listitems
1020            iter([]),  # dictitems
1021        )
1022
1023
1024class PickleProtocol2ReduceTupleState(PickleProtocol2ReduceTuple):
1025    def __reduce__(self):
1026        return (
1027            PickleProtocol2ReduceTuple,  # callable
1028            ('yam', 1),  # args
1029            {'foo': 1},  # state
1030            iter([]),  # listitems
1031            iter([]),  # dictitems
1032        )
1033
1034
1035class PickleProtocol2ReduceTupleSetState(PickleProtocol2ReduceTuple):
1036    def __setstate__(self, state):
1037        self.bar = state['foo']
1038
1039    def __reduce__(self):
1040        return (
1041            type(self),  # callable
1042            ('yam', 1),  # args
1043            {'foo': 1},  # state
1044            iter([]),  # listitems
1045            iter([]),  # dictitems
1046        )
1047
1048
1049class PickleProtocol2ReduceTupleStateSlots(object):
1050    __slots__ = ('argval', 'optional', 'foo')
1051
1052    def __init__(self, argval, optional=None):
1053        self.argval = argval
1054        self.optional = optional
1055
1056    def __reduce__(self):
1057        return (
1058            PickleProtocol2ReduceTuple,  # callable
1059            ('yam', 1),  # args
1060            {'foo': 1},  # state
1061            iter([]),  # listitems
1062            iter([]),  # dictitems
1063        )
1064
1065
1066class PickleProtocol2ReduceListitemsAppend(object):
1067    def __init__(self):
1068        self.inner = []
1069
1070    def __reduce__(self):
1071        return (
1072            PickleProtocol2ReduceListitemsAppend,  # callable
1073            (),  # args
1074            {},  # state
1075            iter(['foo', 'bar']),  # listitems
1076            iter([]),  # dictitems
1077        )
1078
1079    def append(self, item):
1080        self.inner.append(item)
1081
1082
1083class PickleProtocol2ReduceListitemsExtend(object):
1084    def __init__(self):
1085        self.inner = []
1086
1087    def __reduce__(self):
1088        return (
1089            PickleProtocol2ReduceListitemsAppend,  # callable
1090            (),  # args
1091            {},  # state
1092            iter(['foo', 'bar']),  # listitems
1093            iter([]),  # dictitems
1094        )
1095
1096    def extend(self, items):
1097        self.inner.exend(items)
1098
1099
1100class PickleProtocol2ReduceDictitems(object):
1101    def __init__(self):
1102        self.inner = {}
1103
1104    def __reduce__(self):
1105        return (
1106            PickleProtocol2ReduceDictitems,  # callable
1107            (),  # args
1108            {},  # state
1109            [],  # listitems
1110            iter(zip(['foo', 'bar'], ['foo', 'bar'])),  # dictitems
1111        )
1112
1113    def __setitem__(self, k, v):
1114        return self.inner.__setitem__(k, v)
1115
1116
1117class PickleProtocol2Classic:
1118    def __init__(self, foo):
1119        self.foo = foo
1120
1121
1122class PickleProtocol2ClassicInitargs:
1123    def __init__(self, foo, bar=None):
1124        self.foo = foo
1125        if bar:
1126            self.bar = bar
1127
1128    def __getinitargs__(self):
1129        return ('choo', 'choo')
1130
1131
1132class PicklingProtocol4TestCase(unittest.TestCase):
1133    def test_pickle_newargs_ex(self):
1134        """
1135        Ensure we can pickle and unpickle an object whose class needs arguments
1136        to __new__ and get back the same typle
1137        """
1138        instance = PicklableNamedTupleEx(**{'a': 'b', 'n': 2})
1139        encoded = jsonpickle.encode(instance)
1140        decoded = jsonpickle.decode(encoded)
1141        self.assertEqual(instance, decoded)
1142
1143    def test_validate_reconstruct_by_newargs_ex(self):
1144        """
1145        Ensure that the exemplar tuple's __getnewargs_ex__ works
1146        This is necessary to know whether the breakage exists
1147        in jsonpickle or not
1148        """
1149        instance = PicklableNamedTupleEx(**{'a': 'b', 'n': 2})
1150        args, kwargs = instance.__getnewargs_ex__()
1151        newinstance = PicklableNamedTupleEx.__new__(
1152            PicklableNamedTupleEx, *args, **kwargs
1153        )
1154        self.assertEqual(instance, newinstance)
1155
1156    def test_references(self):
1157        shared = Thing('shared')
1158        instance = PicklableNamedTupleEx(**{'a': shared, 'n': shared})
1159        child = Thing('child')
1160        shared.child = child
1161        child.child = instance
1162
1163        encoded = jsonpickle.encode(instance)
1164        decoded = jsonpickle.decode(encoded)
1165
1166        self.assertEqual(decoded[0], decoded[1])
1167        self.assertTrue(decoded[0] is decoded[1])
1168        self.assertTrue(decoded.a is decoded.n)
1169        self.assertEqual(decoded.a.name, 'shared')
1170        self.assertEqual(decoded.a.child.name, 'child')
1171        self.assertTrue(decoded.a.child.child is decoded)
1172        self.assertTrue(decoded.n.child.child is decoded)
1173        self.assertTrue(decoded.a.child is decoded.n.child)
1174
1175        self.assertEqual(decoded.__class__.__name__, PicklableNamedTupleEx.__name__)
1176        # TODO the class itself looks just like the real class, but it's
1177        # actually a reconstruction; PicklableNamedTupleEx is not type(decoded).
1178        self.assertFalse(decoded.__class__ is PicklableNamedTupleEx)
1179
1180
1181class PicklingProtocol2TestCase(SkippableTest):
1182    def test_classic_init_has_args(self):
1183        """
1184        Test unpickling a classic instance whose init takes args,
1185        has no __getinitargs__
1186        Because classic only exists under 2, skipped if PY3
1187        """
1188        if PY3:
1189            return self.skip('No classic classes in PY3')
1190        instance = PickleProtocol2Classic(3)
1191        encoded = jsonpickle.encode(instance)
1192        decoded = jsonpickle.decode(encoded)
1193        self.assertEqual(decoded.foo, 3)
1194
1195    def test_getinitargs(self):
1196        """
1197        Test __getinitargs__ with classic instance
1198
1199        Because classic only exists under 2, skipped if PY3
1200        """
1201        if PY3:
1202            return self.skip('No classic classes in PY3')
1203        instance = PickleProtocol2ClassicInitargs(3)
1204        encoded = jsonpickle.encode(instance)
1205        decoded = jsonpickle.decode(encoded)
1206        self.assertEqual(decoded.bar, 'choo')
1207
1208    def test_reduce_complex_num(self):
1209        instance = 5j
1210        encoded = jsonpickle.encode(instance)
1211        decoded = jsonpickle.decode(encoded)
1212        self.assertEqual(decoded, instance)
1213
1214    def test_reduce_complex_zero(self):
1215        instance = 0j
1216        encoded = jsonpickle.encode(instance)
1217        decoded = jsonpickle.decode(encoded)
1218        self.assertEqual(decoded, instance)
1219
1220    def test_reduce_dictitems(self):
1221        'Test reduce with dictitems set (as a generator)'
1222        instance = PickleProtocol2ReduceDictitems()
1223        encoded = jsonpickle.encode(instance)
1224        decoded = jsonpickle.decode(encoded)
1225        self.assertEqual(decoded.inner, {'foo': 'foo', 'bar': 'bar'})
1226
1227    def test_reduce_listitems_extend(self):
1228        'Test reduce with listitems set (as a generator), yielding single items'
1229        instance = PickleProtocol2ReduceListitemsExtend()
1230        encoded = jsonpickle.encode(instance)
1231        decoded = jsonpickle.decode(encoded)
1232        self.assertEqual(decoded.inner, ['foo', 'bar'])
1233
1234    def test_reduce_listitems_append(self):
1235        'Test reduce with listitems set (as a generator), yielding single items'
1236        instance = PickleProtocol2ReduceListitemsAppend()
1237        encoded = jsonpickle.encode(instance)
1238        decoded = jsonpickle.decode(encoded)
1239        self.assertEqual(decoded.inner, ['foo', 'bar'])
1240
1241    def test_reduce_state_setstate(self):
1242        """Test reduce with the optional state argument set,
1243        on an object with a __setstate__"""
1244
1245        instance = PickleProtocol2ReduceTupleSetState(5)
1246        encoded = jsonpickle.encode(instance)
1247        decoded = jsonpickle.decode(encoded)
1248        self.assertEqual(decoded.argval, 'yam')
1249        self.assertEqual(decoded.optional, 1)
1250        self.assertEqual(decoded.bar, 1)
1251        self.assertFalse(hasattr(decoded, 'foo'))
1252
1253    def test_reduce_state_no_dict(self):
1254        """Test reduce with the optional state argument set,
1255        on an object with no __dict__, and no __setstate__"""
1256
1257        instance = PickleProtocol2ReduceTupleStateSlots(5)
1258        encoded = jsonpickle.encode(instance)
1259        decoded = jsonpickle.decode(encoded)
1260        self.assertEqual(decoded.argval, 'yam')
1261        self.assertEqual(decoded.optional, 1)
1262        self.assertEqual(decoded.foo, 1)
1263
1264    def test_reduce_state_dict(self):
1265        """Test reduce with the optional state argument set,
1266        on an object with a __dict__, and no __setstate__"""
1267
1268        instance = PickleProtocol2ReduceTupleState(5)
1269        encoded = jsonpickle.encode(instance)
1270        decoded = jsonpickle.decode(encoded)
1271        self.assertEqual(decoded.argval, 'yam')
1272        self.assertEqual(decoded.optional, 1)
1273        self.assertEqual(decoded.foo, 1)
1274
1275    def test_reduce_basic(self):
1276        """Test reduce with only callable and args"""
1277
1278        instance = PickleProtocol2ReduceTuple(5)
1279        encoded = jsonpickle.encode(instance)
1280        decoded = jsonpickle.decode(encoded)
1281        self.assertEqual(decoded.argval, 'yam')
1282        self.assertEqual(decoded.optional, 1)
1283
1284    def test_reduce_basic_func(self):
1285        """Test reduce with only callable and args
1286
1287        callable is a module-level function
1288
1289        """
1290        instance = PickleProtocol2ReduceTupleFunc(5)
1291        encoded = jsonpickle.encode(instance)
1292        decoded = jsonpickle.decode(encoded)
1293        self.assertEqual(decoded.argval, 'yam')
1294        self.assertEqual(decoded.optional, 1)
1295
1296    def test_reduce_newobj(self):
1297        """Test reduce with callable called __newobj__
1298
1299        ensures special-case behaviour
1300
1301        """
1302        instance = PickleProtocol2ReduceNewobj(5)
1303        encoded = jsonpickle.encode(instance)
1304        decoded = jsonpickle.decode(encoded)
1305        self.assertEqual(decoded.newargs, ('yam', 1))
1306
1307    def test_reduce_iter(self):
1308        instance = iter('123')
1309        self.assertTrue(util.is_iterator(instance))
1310        encoded = jsonpickle.encode(instance)
1311        decoded = jsonpickle.decode(encoded)
1312        self.assertEqual(next(decoded), '1')
1313        self.assertEqual(next(decoded), '2')
1314        self.assertEqual(next(decoded), '3')
1315
1316    def test_reduce_iterable(self):
1317        """
1318        An reducible object should be pickleable even if
1319        it is also iterable.
1320        """
1321        instance = ReducibleIterator()
1322        self.assertTrue(util.is_iterator(instance))
1323        encoded = jsonpickle.encode(instance)
1324        decoded = jsonpickle.decode(encoded)
1325        self.assertIsInstance(decoded, ReducibleIterator)
1326
1327    def test_reduce_string(self):
1328        """
1329        Ensure json pickle will accept the redirection to another object when
1330        __reduce__ returns a string
1331        """
1332        instance = PickleProtocol2ReduceString()
1333        encoded = jsonpickle.encode(instance)
1334        decoded = jsonpickle.decode(encoded)
1335        self.assertEqual(decoded, slotmagic)
1336
1337    def test_reduce_ex_string(self):
1338        """
1339        Ensure json pickle will accept the redirection to another object when
1340        __reduce_ex__ returns a string
1341
1342        ALSO tests that __reduce_ex__ is called in preference to __reduce__
1343        """
1344        instance = PickleProtocol2ReduceExString()
1345        encoded = jsonpickle.encode(instance)
1346        decoded = jsonpickle.decode(encoded)
1347        self.assertEqual(decoded, slotmagic)
1348
1349    def test_pickle_newargs(self):
1350        """
1351        Ensure we can pickle and unpickle an object whose class needs arguments
1352        to __new__ and get back the same typle
1353        """
1354        instance = PicklableNamedTuple(('a', 'b'), (1, 2))
1355        encoded = jsonpickle.encode(instance)
1356        decoded = jsonpickle.decode(encoded)
1357        self.assertEqual(instance, decoded)
1358
1359    def test_validate_reconstruct_by_newargs(self):
1360        """
1361        Ensure that the exemplar tuple's __getnewargs__ works
1362        This is necessary to know whether the breakage exists
1363        in jsonpickle or not
1364        """
1365        instance = PicklableNamedTuple(('a', 'b'), (1, 2))
1366        newinstance = PicklableNamedTuple.__new__(
1367            PicklableNamedTuple, *(instance.__getnewargs__())
1368        )
1369        self.assertEqual(instance, newinstance)
1370
1371    def test_getnewargs_priority(self):
1372        """
1373        Ensure newargs are used before py/state when decoding
1374        (As per PEP 307, classes are not supposed to implement
1375        all three magic methods)
1376        """
1377        instance = PickleProtocol2GetState('whatevs')
1378        encoded = jsonpickle.encode(instance)
1379        decoded = jsonpickle.decode(encoded)
1380        self.assertEqual(decoded.newargs, ('whatevs',))
1381
1382    def test_restore_dict_state(self):
1383        """
1384        Ensure that if getstate returns a dict, and there is no custom
1385        __setstate__, the dict is used as a source of variables to restore
1386        """
1387        instance = PickleProtocol2GetStateDict('whatevs')
1388        encoded = jsonpickle.encode(instance)
1389        decoded = jsonpickle.decode(encoded)
1390        self.assertTrue(decoded.magic)
1391
1392    def test_restore_slots_state(self):
1393        """
1394        Ensure that if getstate returns a 2-tuple with a dict in the second
1395        position, and there is no custom __setstate__, the dict is used as a
1396        source of variables to restore
1397        """
1398        instance = PickleProtocol2GetStateSlots('whatevs')
1399        encoded = jsonpickle.encode(instance)
1400        decoded = jsonpickle.decode(encoded)
1401        self.assertEqual(decoded.slotmagic.__dict__, slotmagic.__dict__)
1402        self.assertEqual(decoded.slotmagic, slotmagic)
1403
1404    def test_restore_slots_dict_state(self):
1405        """
1406        Ensure that if getstate returns a 2-tuple with a dict in both positions,
1407        and there is no custom __setstate__, the dicts are used as a source of
1408        variables to restore
1409        """
1410        instance = PickleProtocol2GetStateSlotsDict('whatevs')
1411        encoded = jsonpickle.encode(instance)
1412        decoded = jsonpickle.decode(encoded)
1413
1414        self.assertEqual(
1415            PickleProtocol2Thing('slotmagic'), PickleProtocol2Thing('slotmagic')
1416        )
1417        self.assertEqual(decoded.slotmagic.__dict__, slotmagic.__dict__)
1418        self.assertEqual(decoded.slotmagic, slotmagic)
1419        self.assertEqual(decoded.dictmagic, dictmagic)
1420
1421    def test_setstate(self):
1422        """
1423        Ensure output of getstate is passed to setstate
1424        """
1425        instance = PickleProtocol2GetSetState('whatevs')
1426        encoded = jsonpickle.encode(instance)
1427        decoded = jsonpickle.decode(encoded)
1428        self.assertTrue(decoded.magic)
1429
1430    def test_handles_nested_objects(self):
1431        child = PickleProtocol2Thing(None)
1432        instance = PickleProtocol2Thing(child, child)
1433
1434        encoded = jsonpickle.encode(instance)
1435        decoded = jsonpickle.decode(encoded)
1436
1437        self.assertEqual(PickleProtocol2Thing, decoded.__class__)
1438        self.assertEqual(PickleProtocol2Thing, decoded.args[0].__class__)
1439        self.assertEqual(PickleProtocol2Thing, decoded.args[1].__class__)
1440        self.assertTrue(decoded.args[0] is decoded.args[1])
1441
1442    def test_cyclical_objects(self, use_tuple=True):
1443        child = Capture(None)
1444        instance = Capture(child, child)
1445        # create a cycle
1446        if use_tuple:
1447            child.args = (instance,)
1448        else:
1449            child.args = [instance]
1450
1451        encoded = jsonpickle.encode(instance)
1452        decoded = jsonpickle.decode(encoded)
1453
1454        # Ensure the right objects were constructed
1455        self.assertEqual(Capture, decoded.__class__)
1456        self.assertEqual(Capture, decoded.args[0].__class__)
1457        self.assertEqual(Capture, decoded.args[1].__class__)
1458        self.assertEqual(Capture, decoded.args[0].args[0].__class__)
1459        self.assertEqual(Capture, decoded.args[1].args[0].__class__)
1460
1461        # It's turtles all the way down
1462        self.assertEqual(
1463            Capture,
1464            decoded.args[0]
1465            .args[0]
1466            .args[0]
1467            .args[0]
1468            .args[0]
1469            .args[0]
1470            .args[0]
1471            .args[0]
1472            .args[0]
1473            .args[0]
1474            .args[0]
1475            .args[0]
1476            .args[0]
1477            .args[0]
1478            .args[0]
1479            .__class__,
1480        )
1481        # Ensure that references are properly constructed
1482        self.assertTrue(decoded.args[0] is decoded.args[1])
1483        self.assertTrue(decoded is decoded.args[0].args[0])
1484        self.assertTrue(decoded is decoded.args[1].args[0])
1485        self.assertTrue(decoded.args[0] is decoded.args[0].args[0].args[0])
1486        self.assertTrue(decoded.args[0] is decoded.args[1].args[0].args[0])
1487
1488    def test_cyclical_objects_list(self):
1489        self.test_cyclical_objects(use_tuple=False)
1490
1491    def test_handles_cyclical_objects_in_lists(self):
1492        child = PickleProtocol2ChildThing(None)
1493        instance = PickleProtocol2ChildThing([child, child])
1494        child.child = instance  # create a cycle
1495
1496        encoded = jsonpickle.encode(instance)
1497        decoded = jsonpickle.decode(encoded)
1498
1499        self.assertTrue(decoded is decoded.child[0].child)
1500        self.assertTrue(decoded is decoded.child[1].child)
1501
1502    def test_cyclical_objects_unpickleable_false(self, use_tuple=True):
1503        child = Capture(None)
1504        instance = Capture(child, child)
1505        # create a cycle
1506        if use_tuple:
1507            child.args = (instance,)
1508        else:
1509            child.args = [instance]
1510        encoded = jsonpickle.encode(instance, unpicklable=False)
1511        decoded = jsonpickle.decode(encoded)
1512
1513        self.assertTrue(isinstance(decoded, dict))
1514        self.assertTrue('args' in decoded)
1515        self.assertTrue('kwargs' in decoded)
1516
1517        # Tuple is lost via json
1518        args = decoded['args']
1519        self.assertTrue(isinstance(args, list))
1520
1521        # Get the children
1522        self.assertEqual(len(args), 2)
1523        decoded_child0 = args[0]
1524        decoded_child1 = args[1]
1525        # Circular references become None
1526        assert decoded_child0 == {'args': [None], 'kwargs': {}}
1527        assert decoded_child1 == {'args': [None], 'kwargs': {}}
1528
1529    def test_cyclical_objects_unpickleable_false_list(self):
1530        self.test_cyclical_objects_unpickleable_false(use_tuple=False)
1531
1532
1533def test_dict_references_are_preserved():
1534    data = {}
1535    actual = jsonpickle.decode(jsonpickle.encode([data, data]))
1536    assert isinstance(actual, list)
1537    assert isinstance(actual[0], dict)
1538    assert isinstance(actual[1], dict)
1539    assert actual[0] is actual[1]
1540
1541
1542def test_repeat_objects_are_expanded():
1543    """Ensure that all objects are present in the json output"""
1544    # When references are disabled we should create expanded copies
1545    # of any object that appears more than once in the object stream.
1546    alice = Thing('alice')
1547    bob = Thing('bob')
1548    alice.child = bob
1549
1550    car = Thing('car')
1551    car.driver = alice
1552    car.owner = alice
1553    car.passengers = [alice, bob]
1554
1555    pickler = jsonpickle.Pickler(make_refs=False)
1556    flattened = pickler.flatten(car)
1557
1558    assert flattened['name'] == 'car'
1559    assert flattened['driver']['name'] == 'alice'
1560    assert flattened['owner']['name'] == 'alice'
1561    assert flattened['passengers'][0]['name'] == 'alice'
1562    assert flattened['passengers'][1]['name'] == 'bob'
1563    assert flattened['driver']['child']['name'] == 'bob'
1564    assert flattened['passengers'][0]['child']['name'] == 'bob'
1565
1566
1567if __name__ == '__main__':
1568    pytest.main([__file__])
1569