Ronald Oussoren avatar Ronald Oussoren committed 7c9d629

Improved test coverage for archiving python objects

100% line coverage for Lib/objc/_pycoder.py, with a number of
bugfixes that were found by the new tests.

Comments (0)

Files changed (3)

pyobjc-core/Lib/objc/_pycoder.py

 encode_dispatch[dict] = save_dict
 
 def save_global(coder, obj, name=None):
-
     if name is None:
         name = obj.__name__
 
         return long(coder.decodeObject())
 decode_dispatch[kOP_LONG] = load_long
 
-def load_float(coder, setValue):
+def load_float(coder, setValue): # pragma: no cover
+    # Only used with old versions of PyObjC (before 2.3), keep
+    # for backward compatibility.
     if coder.allowsKeyedCoding():
         return coder.decodeFloatForKey_(kVALUE)
     else:
 
 def load_global_ext(coder, setValue):
     if coder.allowsKeyedCoding():
-        code = coder.intForKey_(kCODE)
+        code = coder.decodeIntForKey_(kCODE)
     else:
         code = coder.__pyobjc__decodeInt()
     nil = []
     __import__(module)
     mod = sys.modules[module]
     klass = getattr(mod, name)
+    copyreg._extension_cache[code] = klass
     return klass
 decode_dispatch[kOP_GLOBAL_EXT] = load_global_ext
 
         state, slotstate = state
 
     if state:
-        try:
-            inst_dict = value.__dict__
-            for k in state:
-                v = state[k]
-                if type(k) == str:
-                    inst_dict[intern(k)] = v
-                else:
-                    inst_dict[k] = v
+        # Note: pickle.py catches RuntimeError here,
+        # that's for supporting restricted mode and
+        # is not relevant for PyObjC.
+        inst_dict = value.__dict__
+        for k in state:
+            v = state[k]
+            if type(k) == str:
+                inst_dict[intern(k)] = v
+            else:
+                inst_dict[k] = v
 
-        except RuntimeError:
-            for k, v in state.items():
-                setattr(value, intern(k), v)
 
     if slotstate:
         for k, v in slotstate.items():
     setstate = getattr(value, "__setstate__", None)
     if setstate:
         setstate(state)
-        return
+        return value
 
     slotstate = None
     if isinstance(state, tuple) and len(state) == 2:
         state, slotstate = state
 
     if state:
-        try:
-            inst_dict = value.__dict__
+        # NOTE: picke.py catches RuntimeError here
+        # to support restricted execution, that is not
+        # relevant for PyObjC.
+        inst_dict = value.__dict__
 
-            for k in state:
-                v = state[k]
-                if type(k) == str:
-                    inst_dict[intern(k)] = v
-                else:
-                    inst_dict[k] = v
+        for k in state:
+            v = state[k]
+            if type(k) == str:
+                inst_dict[intern(k)] = v
+            else:
+                inst_dict[k] = v
 
-        except RuntimeError:
-            for k, v in state.items():
-                setattr(value, intern(k), v)
 
     if slotstate:
         for k, v in slotstate.items():
         return
 
     # Check for a class with a custom metaclass
-    try:
-        issc = issubclass(t, type)
-    except TypeError:
-        issc = 0
+    # XXX: pickle.py catches TypeError here, that's for
+    #      compatibility with ancient versions of Boost 
+    #      (before Python 2.2) and is not needed here.
+    issc = issubclass(t, type)
 
     if issc:
         save_global(coder, self)
         if reduce is not None:
             rv = reduce(2)
 
-        else:
+        else: # pragma: no cover
+            # This path will never be used because object implements 
+            # __reduce_ex__ (at least in python2.6 and later)
             rv = getattr(self, "__reduce__", None)
             if reduce is not None:
                 rv = reduce()
                         (t.__name__, self))
 
     if type(rv) is str:
-        save_global(coder, rv)
+        save_global(coder, self, rv)
         return
 
     if type(rv) is not tuple:

pyobjc-core/NEWS.txt

   result in an unexpected value. In particular, if any element of the sequence was :data:`None`
   before archiving it would by ``NSNull.null()`` when read back.
 
+- Using NSArchiver or NSKeyedArchiver to encode and decode (pure) python objects didn't always
+  work correctly. Found by improved unittests.
+
 
 Version 2.4.1
 -------------

pyobjc-core/PyObjCTest/test_archive_python.py

     import copy_reg as copyreg
 
 from PyObjCTools.TestSupport import *
+import objc._pycoder as pycoder
 
 from PyObjCTest.fnd import NSArchiver, NSUnarchiver
 from PyObjCTest.fnd import NSKeyedArchiver, NSKeyedUnarchiver
 
 MyList = test.pickletester.MyList
 
+class reduce_global (object):
+    def __reduce__(self):
+        return "reduce_global"
+reduce_global = reduce_global()
+
 # Quick hack to add a proper __repr__ to class C in
 # pickletester, makes it a lot easier to debug.
 def C__repr__(self):
     def __getinitargs__(self):
         return (1,2)
 
+class state_obj_1:
+    def __getstate__(self):
+        return ({'a': 1, 42: 3}, {'b': 2})
+
+
 class mystr(str):
     __slots__ = ()
 
 class a_newstyle_class (object):
     pass
 
+class newstyle_with_slots (object):
+    __slots__ = ('a', 'b', '__dict__')
+
+class newstyle_with_setstate (object):
+    def __setstate__(self, state):
+        self.state = state
+
+
+
 def make_instance(state):
     o = a_reducing_class()
     o.__dict__.update(state)
         self.archiverClass = NSKeyedArchiver
         self.unarchiverClass = NSKeyedUnarchiver
 
+    def test_unknown_type(self):
+        try:
+            orig = pycoder.decode_dispatch[pycoder.kOP_FLOAT_STR]
+            del pycoder.decode_dispatch[pycoder.kOP_FLOAT_STR]
+
+            o = 14.2
+            buf = self.archiverClass.archivedDataWithRootObject_(o)
+            self.assertRaises(pickle.UnpicklingError, self.unarchiverClass.unarchiveObjectWithData_, buf)
+
+
+        finally:
+            pycoder.decode_dispatch[pycoder.kOP_FLOAT_STR] = orig
+
+
+
     def test_reducing_issues(self):
         class Error1 (object):
             def __reduce__(self):
         o = a_newstyle_class()
         o.attr1 = False
         o.attr2 = None
+        o.__dict__[42] = 3
 
         buf = self.archiverClass.archivedDataWithRootObject_(o)
         self.assertIsInstance(buf, NSData)
             mystr = orig
 
 
-        # XXX: this test doesn't actually do what I want, the extension path is not used.
         try:
-            copyreg.add_extension(a_newstyle_class.__module__, a_newstyle_class, 42)
+            copyreg.add_extension(a_newstyle_class.__module__, a_newstyle_class.__name__, 42)
+            self.assertIn((a_newstyle_class.__module__, a_newstyle_class.__name__), copyreg._extension_registry)
 
-            o = a_newstyle_class()
+            o = a_newstyle_class
             buf = self.archiverClass.archivedDataWithRootObject_(o)
             self.assertIsInstance(buf, NSData)
             v = self.unarchiverClass.unarchiveObjectWithData_(buf)
-            self.assertIsInstance(v, a_newstyle_class)
+            self.assertIs(v, o)
 
-            copyreg.remove_extension(a_newstyle_class.__module__, a_newstyle_class, 42)
+            self.assertIsInstance(buf, NSData)
+            v = self.unarchiverClass.unarchiveObjectWithData_(buf)
+            self.assertIs(v, o)
+
+            copyreg.remove_extension(a_newstyle_class.__module__, a_newstyle_class.__name__, 42)
             self.assertRaises(ValueError, self.unarchiverClass.unarchiveObjectWithData_, buf)
 
         finally:
             mystr = orig
             try:
-                copyreg.remove_extension(a_newstyle_class.__module__, a_newstyle_class, 42)
+                copyreg.remove_extension(a_newstyle_class.__module__, a_newstyle_class.__name__, 42)
             except ValueError:
                 pass
 
         self.assertIsInstance(buf, NSData)
         self.assertRaises(TypeError, self.unarchiverClass.unarchiveObjectWithData_, buf)
 
+    def test_class_with_slots(self):
+        # Test dumpling a class with slots
+        o = newstyle_with_slots()
+        o.a = 1
+        o.b = 2
+        o.c = 3
+
+        buf = self.archiverClass.archivedDataWithRootObject_(o)
+        self.assertIsInstance(buf, NSData)
+        v = self.unarchiverClass.unarchiveObjectWithData_(buf)
+        self.assertIsInstance(v, newstyle_with_slots)
+        self.assertEqual(v.a, 1)
+        self.assertEqual(v.b, 2)
+        self.assertEqual(v.c, 3)
+
+    @onlyPython2
+    def test_class_with_state(self):
+        o = state_obj_1()
+        buf = self.archiverClass.archivedDataWithRootObject_(o)
+        self.assertIsInstance(buf, NSData)
+        v = self.unarchiverClass.unarchiveObjectWithData_(buf)
+        self.assertIsInstance(v, state_obj_1)
+        self.assertEqual(v.a, 1)
+        self.assertEqual(v.b, 2)
+        self.assertEqual(v.__dict__[42], 3)
+
+    def test_class_with_setstate(self):
+        o = newstyle_with_setstate()
+        o.a = 1
+        o.b = 2
+        buf = self.archiverClass.archivedDataWithRootObject_(o)
+        self.assertIsInstance(buf, NSData)
+        v = self.unarchiverClass.unarchiveObjectWithData_(buf)
+        self.assertIsInstance(v, newstyle_with_setstate)
+        self.assertEquals(v.state, {'a': 1, 'b': 2})
+        
+    def test_reduce_as_global(self):
+        # Test class where __reduce__ returns a string (the name of a global)
+
+        o = reduce_global
+        buf = self.archiverClass.archivedDataWithRootObject_(o)
+        self.assertIsInstance(buf, NSData)
+        v = self.unarchiverClass.unarchiveObjectWithData_(buf)
+        self.assertIs(v, reduce_global)
+
+
+    def test_reduce_invalid(self):
+        class invalid_reduce (object):
+            def __reduce__(self):
+                return 42
+        self.assertRaises(pickle.PicklingError, self.archiverClass.archivedDataWithRootObject_, invalid_reduce())
+
+        class invalid_reduce (object):
+            def __reduce__(self):
+                return (1,)
+        self.assertRaises(pickle.PicklingError, self.archiverClass.archivedDataWithRootObject_, invalid_reduce())
+
+        class invalid_reduce (object):
+            def __reduce__(self):
+                return (1,2,3,4,5,6)
+        self.assertRaises(pickle.PicklingError, self.archiverClass.archivedDataWithRootObject_, invalid_reduce())
+
 
     def test_basic_objects(self):
         buf = self.archiverClass.archivedDataWithRootObject_(a_function)
         self.assertIsInstance(v, a_reducing_class)
         self.assertIs(v.value, v)
 
+    def test_reducing_object(self):
+        o = a_reducing_class()
+        o.value = 42
+        buf = self.archiverClass.archivedDataWithRootObject_(o)
+        self.assertIsInstance(buf, NSData)
+        v = self.unarchiverClass.unarchiveObjectWithData_(buf)
+        self.assertIsInstance(v, a_reducing_class)
+        self.assertEqual(o.value, 42)
+
     def testRecusiveNesting(self):
         l = []
         d = {1:l}
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.