Repository: jsonpickle/jsonpickle (python)

Issue:
make_refs=False Not working as expected or breaking the _in_cycle
So reviewing a few issues here I feel like that some are originated from the root cause which is that _in_cycle reference if not working properly when make_ref is false.

First, here is a simple script of how to reproduce the issue fast and easy.

```python
import datetime
from pprint import pprint

import jsonpickle
import pytz

d1 = datetime.datetime(2020, 6, 6, 6, 6, 6, 6, tzinfo=pytz.UTC)
d2 = datetime.datetime(2020, 6, 6, 6, 6, 6, 6, tzinfo=pytz.UTC)
l = [d1, d2]
encoded_list = jsonpickle.encode(l, make_refs=False)
pprint(encoded_list)
decoded_list = jsonpickle.decode(encoded_list)
```

I have been doing some investigation, the issue happens only when `make_refs=False`. 
and source of it is in the `_in_cycle` method, specifically this condition `(not make_refs and id(obj) in objs)` something is not right here.

As per my understanding when flatten is called for object if it will have the same ID so objs will contain him so _in_cycle will return true and `repr` will be called.

Pull request:
pickler: create copies of seen objects
Previously the make_refs=False / unpicklable=False code path
would avoid emitting data for objects it had already seen.
It mostly did this to avoid cycles, as its reference
system is disabled in these modes, and thus the result
was that null was emitted for any objects that appear
more than once in the object graph.

Improve the behavior by keeping track of objects that we
have already flattened and simply return the pre-flattened
representation when we re-encounter the same object.

This is a simple solution that fixes encoding of datetime
objects with UTC tzinfo in these scenarios.

Resolves #333 #334 #337
Signed-off-by: David Aguilar <davvid@gmail.com>

Patch:
diff --git a/jsonpickle/pickler.py b/jsonpickle/pickler.py
index 7907f81..12007ee 100644
--- a/jsonpickle/pickler.py
+++ b/jsonpickle/pickler.py
@@ -164,6 +164,8 @@ class Pickler(object):
         self._max_iter = max_iter
         # Whether to allow decimals to pass-through
         self._use_decimal = use_decimal
+        # A cache of objects that have already been flattened.
+        self._flattened = {}
 
         if self.use_base85:
             self._bytes_tag = tags.B85
@@ -179,6 +181,7 @@ class Pickler(object):
         self._objs = {}
         self._depth = -1
         self._seen = []
+        self._flattened = {}
 
     def _push(self):
         """Steps down one level in the namespace."""
@@ -252,7 +255,16 @@ class Pickler(object):
         return self._flatten(obj)
 
     def _flatten(self, obj):
+        if self.unpicklable and self.make_refs:
+            result = self._flatten_impl(obj)
+        else:
+            try:
+                result = self._flattened[id(obj)]
+            except KeyError:
+                result = self._flattened[id(obj)] = self._flatten_impl(obj)
+        return result
 
+    def _flatten_impl(self, obj):
         #########################################
         # if obj is nonrecursive return immediately
         # for performance reasons we don't want to do recursive checks


Test patch:
diff --git a/tests/jsonpickle_test.py b/tests/jsonpickle_test.py
index b573e30..d436159 100644
--- a/tests/jsonpickle_test.py
+++ b/tests/jsonpickle_test.py
@@ -764,10 +764,10 @@ class JSONPickleTestCase(SkippableTest):
         encoded = jsonpickle.encode(thing, make_refs=False)
         decoded = jsonpickle.decode(encoded)
 
-        self.assertEqual(decoded.a[0], 1)
-        self.assertEqual(decoded.b[0:3], '[1,')
-        self.assertEqual(decoded.a[1][0:3], '[1,')
-        self.assertEqual(decoded.a[2][0][0:3], '[1,')
+        assert decoded.a[0] == 1
+        assert decoded.b[0] == 1
+        assert decoded.a[1][0:3] == '[1,'
+        assert decoded.a[2][0][0:3] == '[1,'
 
     def test_can_serialize_inner_classes(self):
         class InnerScope(object):
@@ -1522,14 +1522,39 @@ class PicklingProtocol2TestCase(SkippableTest):
         self.assertEqual(len(args), 2)
         decoded_child0 = args[0]
         decoded_child1 = args[1]
-        self.assertTrue(isinstance(decoded_child0, dict))
         # Circular references become None
-        self.assertEqual(decoded_child1, None)
+        assert decoded_child0 == {'args': [None], 'kwargs': {}}
+        assert decoded_child1 == {'args': [None], 'kwargs': {}}
 
     def test_cyclical_objects_unpickleable_false_list(self):
         self.test_cyclical_objects_unpickleable_false(use_tuple=False)
 
 
+def test_repeat_objects_are_expanded():
+    """Ensure that all objects are present in the json output"""
+    # When references are disabled we should create expanded copies
+    # of any object that appears more than once in the object stream.
+    alice = Thing('alice')
+    bob = Thing('bob')
+    alice.child = bob
+
+    car = Thing('car')
+    car.driver = alice
+    car.owner = alice
+    car.passengers = [alice, bob]
+
+    pickler = jsonpickle.Pickler(make_refs=False)
+    flattened = pickler.flatten(car)
+
+    assert flattened['name'] == 'car'
+    assert flattened['driver']['name'] == 'alice'
+    assert flattened['owner']['name'] == 'alice'
+    assert flattened['passengers'][0]['name'] == 'alice'
+    assert flattened['passengers'][1]['name'] == 'bob'
+    assert flattened['driver']['child']['name'] == 'bob'
+    assert flattened['passengers'][0]['child']['name'] == 'bob'
+
+
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(JSONPickleTestCase))
diff --git a/tests/object_test.py b/tests/object_test.py
index e65caf4..dbffc4e 100644
--- a/tests/object_test.py
+++ b/tests/object_test.py
@@ -6,6 +6,7 @@ import datetime
 import decimal
 import re
 import threading
+import pytz
 
 import jsonpickle
 from jsonpickle import compat
@@ -1134,5 +1135,17 @@ def test_unicode_mixin():
     assert new_obj.ok()
 
 
+def test_datetime_with_tz_copies_refs():
+    """Ensure that we create copies of referenced objects"""
+    d0 = datetime.datetime(2020, 5, 5, 5, 5, 5, 5, tzinfo=pytz.UTC)
+    d1 = datetime.datetime(2020, 5, 5, 5, 5, 5, 5, tzinfo=pytz.UTC)
+    obj = [d0, d1]
+    encoded = jsonpickle.encode(obj, make_refs=False)
+    decoded = jsonpickle.decode(encoded)
+    assert len(decoded) == 2
+    assert decoded[0] == d0
+    assert decoded[1] == d1
+
+
 if __name__ == '__main__':
     pytest.main([__file__])