changeset 46:92ae1e882cef

Enhance pickling support
author Franz Glasner <f.glasner@feldmann-mg.com>
date Wed, 02 Aug 2023 16:51:34 +0200
parents 8a560e0c3180
children e7f6fc454f84
files data_schema/__init__.py tests/test_schema.py
diffstat 2 files changed, 103 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/data_schema/__init__.py	Wed Aug 02 13:58:25 2023 +0200
+++ b/data_schema/__init__.py	Wed Aug 02 16:51:34 2023 +0200
@@ -34,6 +34,7 @@
 import enum
 import pickle
 import re
+import sys
 import urllib.parse
 
 import rfc3986
@@ -147,8 +148,6 @@
 
 TYPE_RE = type(re.compile(r"\A.+\Z"))
 
-_SENTINEL = object()
-
 SCHEMA_REF_KEY = "$ref"
 """Key name for schema references (like a symlink within a schema)"""
 
@@ -159,6 +158,20 @@
 """URI path to the current schema"""
 
 
+class _SENTINELType(object):
+
+    @staticmethod
+    def _get_single(module, name):
+        return getattr(sys.modules[module], name)
+
+    def __reduce_ex__(self, proto):
+        """Make sure the _SENTINEL is ever only instantiated as singleton"""
+        return (_SENTINELType._get_single, (self.__module__, "_SENTINEL"))
+
+
+_SENTINEL = _SENTINELType()
+
+
 def problem_message(pr):
     """
 
@@ -309,11 +322,7 @@
             self.is_sub_root = is_sub_root
 
     def __reduce_ex__(self, proto):
-        rv = super().__reduce_ex__(proto)
-        #assert False, repr(rv)
-        print("RRRRRRR\n\n", repr(rv))
-        assert False, repr(rv)
-        return rv
+        return super().__reduce_ex__(proto)
 
     def __getstate__(self):
         return (1, self.parent, self.is_sub_root)
@@ -447,8 +456,10 @@
                 raise TypeError("one of `key` and `index` must be given in a non-root context")
             if root_object is not _SENTINEL:
                 raise TypeError("non-root context may not have a root object")
+            self.root_object = root_object
             if root_schema is not _SENTINEL:
                 raise TypeError("non-root context may not have a root schema")
+            self.root_schema = root_schema
         if key is not _SENTINEL and index is not _SENTINEL:
             raise ValueError("only one of `key` and `index` may be given in a context")
         if key_index is not _SENTINEL and key is _SENTINEL:
@@ -473,6 +484,30 @@
             raise pickle.UnpicklingError(
                 "Unsupported pickle version for _Context: %d" % (ver,))
 
+    def __eq__(self, other):
+        if not isinstance(other, Context):
+            return NotImplemented
+        return ((self._parent == other._parent)
+                and (self._key == other._key)
+                and (self._key_index == other._key_index)
+# XXX FIXME ???
+#                and (self.root_object == other.root_object)
+#                and (self.root_schema == other.root_schema)
+#                and (self._current_object == other._current_object)
+                and (self._settings == other._settings)
+        )
+
+    def __ne__(self, other):
+        #
+        # While the default in Python3 is sensible implementing is recommended
+        # when a built-in __eq__ is overwritten (Raymond Hettinger).
+        #
+        # Do not use not self == other because NotImplemented is not handled
+        # properly in some early Python versions (including Py2).
+        #
+        equal = self.__eq__(other)
+        return NotImplemented if equal is NotImplemented else not equal
+
     @property
     def parent(self):
         return self._parent
--- a/tests/test_schema.py	Wed Aug 02 13:58:25 2023 +0200
+++ b/tests/test_schema.py	Wed Aug 02 16:51:34 2023 +0200
@@ -25,6 +25,12 @@
 
 class Pickling(unittest.TestCase):
 
+    def test_sentinel_singleinst(self):
+        s = data_schema._SENTINEL
+        s2 = pickle.loads(pickle.dumps(s))
+        self.assertIs(s, s2)
+        self.assertIs(s2, s)
+
     def test_severity(self):
         for sev in SEVERITY:
             b = pickle.dumps(sev)
@@ -112,6 +118,12 @@
         self.assertEqual(0, len(schema))
         self.assertFalse(schema)
 
+    def test_root_creation_pickle(self):
+        schema = data_schema._Schema(None, True)
+        self.assertIsInstance(schema, dict)
+        schema2 = pickle.loads(pickle.dumps(schema))
+        self.assertEqual(schema, schema2)
+
     def test_root_creation_wrong(self):
         self.assertRaises(
             ValueError,
@@ -126,6 +138,14 @@
         self.assertTrue(schema.is_sub_root)
         self.assertIs(schema, schema.SELF)
 
+    def test_root_properties_pickle(self):
+        schema = data_schema._Schema(None, True)
+        schema2 = pickle.loads(pickle.dumps(schema))
+        self.assertIsNone(schema2.parent)
+        self.assertIs(schema2, schema2.ROOT)
+        self.assertTrue(schema2.is_sub_root)
+        self.assertIs(schema2, schema2.SELF)
+
     def test_dict_len_bool(self):
         schema = data_schema._Schema(None, True, a=1, b=2)
         self.assertTrue(schema)
@@ -137,6 +157,15 @@
         self.assertEqual(schema1, schema2)
         self.assertIsNot(schema1, schema2)
 
+    def test_pickle(self):
+        schema1 = data_schema._Schema(None, True, a=1, b=2)
+        schema2 = pickle.loads(pickle.dumps(schema1))
+        self.assertEqual(schema1, schema2)
+        self.assertIsNot(schema1, schema2)
+        self.assertEqual(2, len(schema2))
+        self.assertEqual(1, schema2["a"])
+        self.assertEqual(2, schema2["b"])
+
     def test_copy(self):
         schema = data_schema._Schema(None, True, type="str")
         schema2 = schema.copy()
@@ -200,6 +229,22 @@
         self.assertTrue(ctx.root_schema is schema)
         self.assertTrue(ctx.settings is settings)
 
+    def test_root_context_pickle(self):
+        obj = object()
+        schema = object()
+        settings = data_schema.ValidationSettings(
+            skip_keys=[], break_on_keynames_problems=True,
+            data_stream_loader=None,
+            schema_loader=data_schema.default_schema_loader)
+        ctx = data_schema.Context(
+            None, root_object=obj, root_schema=schema, settings=settings)
+        self.assertEqual("<ROOT>", str(ctx))
+        self.assertTrue(ctx.root_object is obj)
+        self.assertTrue(ctx.root_schema is schema)
+        self.assertTrue(ctx.settings is settings)
+        ctx2 = pickle.loads(pickle.dumps(ctx))
+        self.assertEqual(ctx, ctx2)
+
     def test_parent_of_root_context(self):
         obj = object()
         schema = object()
@@ -289,6 +334,22 @@
         ctx3 = data_schema.Context(ctx2, key="key3")
         self.assertEqual("<Context path=`key1 / INDEX:2 / key3'>", repr(ctx3))
 
+    def test_repr_pickle(self):
+        settings = data_schema.ValidationSettings(
+            skip_keys=[], break_on_keynames_problems=True,
+            data_stream_loader=None,
+            schema_loader=data_schema.default_schema_loader)
+        root = data_schema.Context(None, settings=settings)
+        ctx1 = data_schema.Context(root, key="key1")
+        ctx2 = data_schema.Context(ctx1, index=2)
+        ctx3 = data_schema.Context(ctx2, key="key3")
+
+        ctx3_2 = pickle.loads(pickle.dumps(ctx3))
+
+        self.assertEqual(ctx3, ctx3_2)
+        self.assertEqual(repr(ctx3), repr(ctx3_2))
+        self.assertEqual("<Context path=`key1 / INDEX:2 / key3'>", repr(ctx3_2))
+
     def test_root(self):
         settings = data_schema.ValidationSettings(
             skip_keys=[], break_on_keynames_problems=True,