Repository: jsonpickle/jsonpickle
Pull request #269 (2019-06-30 05:27:46)

Fail safe mode
see #205

re-created PR because previous PR's history got wonky.


Patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..17e15f2
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,15 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "Python: Current File",
+            "type": "python",
+            "request": "launch",
+            "program": "${file}",
+            "console": "integratedTerminal"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..5c1fca6
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,7 @@
+{
+    "python.pythonPath": "env27\\Scripts\\python.exe",
+    "python.testing.pytestEnabled": true,
+    "files.trimTrailingWhitespace": true,
+    "files.insertFinalNewline": true,
+    "files.trimFinalNewlines": true
+}
diff --git a/jsonpickle/pickler.py b/jsonpickle/pickler.py
index f2f3292..d960572 100644
--- a/jsonpickle/pickler.py
+++ b/jsonpickle/pickler.py
@@ -31,7 +31,8 @@ def encode(value,
            max_iter=None,
            use_decimal=False,
            numeric_keys=False,
-           use_base85=False):
+           use_base85=False,
+           fail_safe=None):
     """Return a JSON formatted representation of value, a Python object.
 
     :param unpicklable: If set to False then the output will not contain the
@@ -66,6 +67,13 @@ def encode(value,
 
         NOTE: A side-effect of the above settings is that float values will be
         converted to Decimal when converting to json.
+    :param use_base85:
+        If possible, use base85 to encode binary data. Base85 bloats binary data
+        by 1/4 as opposed to base64, which expands it by 1/3. This argument is
+        ignored on Python 2 because it doesn't support it.
+    :param fail_safe: If set to a function exceptions are ignored when pickling
+        and if a exception happens the function is called and the return value
+        is used as the value for the object that caused the error
 
     >>> encode('my string') == '"my string"'
     True
@@ -76,10 +84,6 @@ def encode(value,
     >>> encode({'foo': [1, 2, [3, 4]]}, max_depth=1)
     '{"foo": "[1, 2, [3, 4]]"}'
 
-    :param use_base85:
-        If possible, use base85 to encode binary data. Base85 bloats binary data
-        by 1/4 as opposed to base64, which expands it by 1/3. This argument is
-        ignored on Python 2 because it doesn't support it.
     """
     backend = backend or json
     context = context or Pickler(
@@ -92,7 +96,8 @@ def encode(value,
             max_iter=max_iter,
             numeric_keys=numeric_keys,
             use_decimal=use_decimal,
-            use_base85=use_base85)
+            use_base85=use_base85,
+            fail_safe=fail_safe)
     return backend.encode(context.flatten(value, reset=reset))
 
 
@@ -108,7 +113,8 @@ class Pickler(object):
                  max_iter=None,
                  numeric_keys=False,
                  use_decimal=False,
-                 use_base85=False):
+                 use_base85=False,
+                 fail_safe=None):
         self.unpicklable = unpicklable
         self.make_refs = make_refs
         self.backend = backend or json
@@ -136,6 +142,9 @@ class Pickler(object):
             self._bytes_tag = tags.B64
             self._bytes_encoder = util.b64encode
 
+        # ignore exceptions
+        self.fail_safe = fail_safe
+
     def reset(self):
         self._objs = {}
         self._depth = -1
@@ -222,19 +231,29 @@ class Pickler(object):
 
     def _flatten_obj(self, obj):
         self._seen.append(obj)
+
         max_reached = self._max_reached()
-        in_cycle = _in_cycle(obj, self._objs, max_reached, self.make_refs)
-        if in_cycle:
-            # break the cycle
-            flatten_func = repr
-        else:
-            flatten_func = self._get_flattener(obj)
 
-        if flatten_func is None:
-            self._pickle_warning(obj)
-            return None
+        try:
+
+            in_cycle = _in_cycle(obj, self._objs, max_reached, self.make_refs)
+            if in_cycle:
+                # break the cycle
+                flatten_func = repr
+            else:
+                flatten_func = self._get_flattener(obj)
+
+            if flatten_func is None:
+                self._pickle_warning(obj)
+                return None
+
+            return flatten_func(obj)
 
-        return flatten_func(obj)
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except Exception as e:
+            if self.fail_safe is None: raise
+            else: return self.fail_safe(e)
 
     def _list_recurse(self, obj):
         return [self._flatten(v) for v in obj]


Test patch:
diff --git a/tests/object_test.py b/tests/object_test.py
index 5c6967a..ecefb0e 100644
--- a/tests/object_test.py
+++ b/tests/object_test.py
@@ -237,6 +237,35 @@ class ThingWithTimedeltaAttribute(object):
     def __getinitargs__(self):
         return self.offset,
 
+class FailSafeTestCase(SkippableTest):
+    class BadClass:
+        def __getstate__(self):
+            a # will raise NameError
+
+    good = 'good'
+
+    to_pickle = [BadClass(), good]
+
+    def test_no_error(self):
+        encoded = jsonpickle.encode(self.to_pickle, fail_safe=lambda e:None)
+        decoded = jsonpickle.decode(encoded)
+        assert decoded[0] is None
+        assert decoded[1] == 'good'
+
+    def test_error_recorded(self):
+        recordedEx = []
+        def recorder(exception):
+            recordedEx.append(exception)
+
+        encoded = jsonpickle.encode(self.to_pickle, fail_safe=recorder)
+        assert len(recordedEx) == 1
+        assert isinstance(recordedEx[0], Exception)
+
+    def test_custom_err_msg(self):
+        CUSTOM_ERR_MSG = "custom err msg"
+        encoded = jsonpickle.encode(self.to_pickle, fail_safe=lambda e:CUSTOM_ERR_MSG)
+        decoded = jsonpickle.decode(encoded)
+        assert decoded[0] == CUSTOM_ERR_MSG
 
 class IntKeysObject(object):
 
@@ -907,6 +936,7 @@ class ExternalHandlerTestCase(unittest.TestCase):
 
 def suite():
     suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(FailSafeTestCase))
     suite.addTest(unittest.makeSuite(AdvancedObjectsTestCase))
     suite.addTest(unittest.makeSuite(ExternalHandlerTestCase))
     return suite