Commits

Andrew Wooster committed 4bf73ec Merge with conflicts

Merge branch 'master' of git://github.com/kelleyk/biplist into kelleyk-master

Conflicts:
biplist/__init__.py

  • Participants
  • Parent commits d8319c0, 31419a5

Comments (0)

Files changed (9)

 # Emacs noise
 *~
 
+# tox noise
+*.egg
+
 # other noise
 .svn
 Andrew Wooster (andrew@planetaryscale.com)
+
+Ported to Python 3 by Kevin Kelley (kelleyk@kelleyk.net)

File biplist/__init__.py

         print "Not a plist:", e
 """
 
+import sys
 from collections import namedtuple
-from cStringIO import StringIO
 import calendar
 import datetime
 import math
 import sys
 import time
 
+import six
+
 __all__ = [
     'Uid', 'Data', 'readPlist', 'writePlist', 'readPlistFromString',
     'writePlistToString', 'InvalidPlistException', 'NotBinaryPlistException'
     def __repr__(self):
         return "Uid(%d)" % self
 
-class Data(str):
+class Data(six.binary_type):
     """Wrapper around str types for representing Data values."""
     pass
 
     """Raises NotBinaryPlistException, InvalidPlistException"""
     didOpen = False
     result = None
-    if isinstance(pathOrFile, (str, unicode)):
+    if isinstance(pathOrFile, (six.binary_type, six.text_type)):
         pathOrFile = open(pathOrFile, 'rb')
         didOpen = True
     try:
         reader = PlistReader(pathOrFile)
         result = reader.parse()
-    except NotBinaryPlistException, e:
+    except NotBinaryPlistException as e:
         try:
             pathOrFile.seek(0)
             result = plistlib.readPlist(pathOrFile)
             result = wrapDataObject(result, for_binary=True)
-        except Exception, e:
+        except Exception as e:
             raise InvalidPlistException(e)
     if didOpen:
         pathOrFile.close()
         o = wrapDataObject(list(o), for_binary)
         o = tuple(o)
     elif isinstance(o, list):
-        for i in xrange(len(o)):
+        for i in range(len(o)):
             o[i] = wrapDataObject(o[i], for_binary)
     elif isinstance(o, dict):
         for k in o:
         return plistlib.writePlist(rootObject, pathOrFile)
     else:
         didOpen = False
-        if isinstance(pathOrFile, (str, unicode)):
+        if isinstance(pathOrFile, (six.binary_type, six.text_type)):
             pathOrFile = open(pathOrFile, 'wb')
             didOpen = True
         writer = PlistWriter(pathOrFile)
         return result
 
 def readPlistFromString(data):
-    return readPlist(StringIO(data))
+    return readPlist(six.BytesIO(data))
 
 def writePlistToString(rootObject, binary=True):
     if not binary:
         rootObject = wrapDataObject(rootObject, binary)
-        return plistlib.writePlistToString(rootObject)
+        if six.PY3:
+            return plistlib.writePlistToBytes(rootObject)
+        else:
+            return plistlib.writePlistToString(rootObject)
     else:
-        io = StringIO()
+        io = six.BytesIO()
         writer = PlistWriter(io)
         writer.writeRoot(rootObject)
         return io.getvalue()
 def is_stream_binary_plist(stream):
     stream.seek(0)
     header = stream.read(7)
-    if header == 'bplist0':
+    if header == six.b('bplist0'):
         return True
     else:
         return False
                 offset_i += 1
             self.setCurrentOffsetToObjectNumber(self.trailer.topLevelObjectNumber)
             result = self.readObject()
-        except TypeError, e:
+        except TypeError as e:
             raise InvalidPlistException(e)
         return result
     
         return "<BoolWrapper: %s>" % self.value
 
 class PlistWriter(object):
-    header = 'bplist00bybiplist1.0'
+    header = six.b('bplist00bybiplist1.0')
     file = None
     byteCounts = None
     trailer = None
         output = self.writeOffsetTable(output)
         output += pack('!xxxxxxBBQQQ', *self.trailer)
         self.file.write(output)
-    
+
     def wrapRoot(self, root):
         if isinstance(root, bool):
             if root is True:
             return HashableWrapper(n)
         elif isinstance(root, dict):
             n = {}
-            for key, value in root.iteritems():
+            for key, value in six.iteritems(root):
                 n[self.wrapRoot(key)] = self.wrapRoot(value)
             return HashableWrapper(n)
         elif isinstance(root, list):
                 raise InvalidPlistException('Dictionary keys cannot be null in plists.')
             elif isinstance(key, Data):
                 raise InvalidPlistException('Data cannot be dictionary keys in plists.')
-            elif not isinstance(key, (str, unicode)):
+            elif not isinstance(key, (six.binary_type, six.text_type)):
                 raise InvalidPlistException('Keys must be strings.')
         
         def proc_size(size):
         elif isinstance(obj, Uid):
             size = self.intSize(obj)
             self.incrementByteCount('uidBytes', incr=1+size)
-        elif isinstance(obj, (int, long)):
+        elif isinstance(obj, six.integer_types):
             size = self.intSize(obj)
             self.incrementByteCount('intBytes', incr=1+size)
         elif isinstance(obj, (float)):
         elif isinstance(obj, Data):
             size = proc_size(len(obj))
             self.incrementByteCount('dataBytes', incr=1+size)
-        elif isinstance(obj, (str, unicode)):
+        elif isinstance(obj, (six.text_type, six.binary_type)):
             size = proc_size(len(obj))
             self.incrementByteCount('stringBytes', incr=1+size)
         elif isinstance(obj, HashableWrapper):
             elif isinstance(obj, dict):
                 size = proc_size(len(obj))
                 self.incrementByteCount('dictBytes', incr=1+size)
-                for key, value in obj.iteritems():
+                for key, value in six.iteritems(obj):
                     check_key(key)
                     self.computeOffsets(key, asReference=True)
                     self.computeOffsets(value, asReference=True)
            object was written.
         """
         def proc_variable_length(format, length):
-            result = ''
+            result = six.b('')
             if length > 0b1110:
                 result += pack('!B', (format << 4) | 0b1111)
                 result = self.writeObject(length, result)
                 result += pack('!B', (format << 4) | length)
             return result
         
-        if isinstance(obj, unicode) and obj == u'':
+        if isinstance(obj, six.text_type) and obj == six.u(''):
             # The Apple Plist decoder can't decode a zero length Unicode string.
-            obj = ''
-        
+            obj = six.b('')
+       
         if setReferencePosition:
             self.referencePositions[obj] = len(output)
         
             size = self.intSize(obj)
             output += pack('!B', (0b1000 << 4) | size - 1)
             output += self.binaryInt(obj)
-        elif isinstance(obj, (int, long)):
+        elif isinstance(obj, six.integer_types):
             bytes = self.intSize(obj)
             root = math.log(bytes, 2)
             output += pack('!B', (0b0001 << 4) | int(root))
         elif isinstance(obj, Data):
             output += proc_variable_length(0b0100, len(obj))
             output += obj
-        elif isinstance(obj, (str, unicode)):
-            if isinstance(obj, unicode):
-                bytes = obj.encode('utf_16_be')
-                output += proc_variable_length(0b0110, len(bytes)/2)
-                output += bytes
-            else:
-                bytes = obj
-                output += proc_variable_length(0b0101, len(bytes))
-                output += bytes
+        elif isinstance(obj, six.text_type):
+            bytes = obj.encode('utf_16_be')
+            output += proc_variable_length(0b0110, len(bytes)//2)
+            output += bytes
+        elif isinstance(obj, six.binary_type):
+            bytes = obj
+            output += proc_variable_length(0b0101, len(bytes))
+            output += bytes
         elif isinstance(obj, HashableWrapper):
             obj = obj.value
             if isinstance(obj, (set, list, tuple)):
                 keys = []
                 values = []
                 objectsToWrite = []
-                for key, value in obj.iteritems():
+                for key, value in six.iteritems(obj):
                     keys.append(key)
                     values.append(value)
                 for key in keys:
     def writeOffsetTable(self, output):
         """Writes all of the object reference offsets."""
         all_positions = []
-        writtenReferences = self.writtenReferences.items()
-        writtenReferences.sort(lambda x,y: cmp(x[1], y[1]))
+        writtenReferences = list(self.writtenReferences.items())
+        writtenReferences.sort(key=lambda x: x[1])
         for obj,order in writtenReferences:
+            # Porting note: Elsewhere we deliberately replace empty unicdoe strings
+            # with empty binary strings, but the empty unicode string
+            # goes into writtenReferences.  This isn't an issue in Py2
+            # because u'' and b'' have the same hash; but it is in
+            # Py3, where they don't.
+            if six.PY3 and obj == six.u(''):
+                obj = six.b('')
             position = self.referencePositions.get(obj)
             if position is None:
                 raise InvalidPlistException("Error while writing offsets table. Object not found. %s" % obj)
         return result
     
     def binaryInt(self, obj, bytes=None):
-        result = ''
+        result = six.b('')
         if bytes is None:
             bytes = self.intSize(obj)
         if bytes == 1:
 
 major, minor, micro, releaselevel, serial = sys.version_info
 
-if major == 2 and minor < 6:
-    print("Python >= 2.6 is required to use this module.")
-    sys.exit(1)
-elif major >= 3:
-    print("There is no support for Python 3 yet in this module.")
+if major <= 1 or (major == 2 and minor < 6) or (major == 3 and minor < 2):
+    # N.B.: Haven't tested with older py3k versions.
+    print('This module supports Python 2 >= 2.6 and Python 3 >= 3.2.')
     sys.exit(1)
 
 author = 'Andrew Wooster'
 email = 'andrew@planetaryscale.com'
-version = '0.4'
+version = '0.5'
 desc = 'biplist is a library for reading/writing binary plists.'
 
 setup(
     name = 'biplist',
     version = version,
-    url = 'https://github.com/wooster/biplist',
-    download_url = 'https://github.com/wooster/biplist/downloads/biplist-0.4.tar.gz',
+    url = 'https://github.com/kelleyk/py3k-biplist',
     license = 'BSD',
     description = desc,
     long_description = 
 format for property lists on OS X. This is a library for generating binary
 plists which can be read by OS X, iOS, or other clients.
 
-This module requires Python 2.6 or higher.""",
+This module requires Python 2.6 or higher or Python 3.2 or higher.""",
     author = author,
     author_email = email,
     packages = find_packages(),
     ],    
     setup_requires = ['nose', 'coverage'],
     test_suite = 'nose.collector',
-    install_requires = [
-    ],
+    install_requires = ['six'],
+    requires = ['six'],
 )

File tests/test_invalid.py

     
     def testTooShort(self):
         try:
-            readPlistFromString("bplist0")
+            readPlistFromString(six.b("bplist0"))
             self.fail("Should not successfully read plist which is too short.")
         except InvalidPlistException as e:
             pass
     
     def testInvalid(self):
         try:
-            readPlistFromString("bplist0-------------------------------------")
+            readPlistFromString(six.b("bplist0-------------------------------------"))
             self.fail("Should not successfully read invalid plist.")
         except InvalidPlistException as e:
             pass

File tests/test_utils.py

 import os
 import subprocess
 import sys
+import six
 
 def data_path(path):
     return os.path.join(os.path.dirname(globals()["__file__"]), 'data', path)
     p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
     stdin, stdout = (p.stdin, p.stdout)
     output = stdout.read()
-    output = output.strip("\n")
+    output = output.strip(six.b("\n"))
     status = stdin.close()
     p.wait()
     return p.returncode, output

File tests/test_valid.py

 import os
 from test_utils import *
 import unittest
+import six
 
 class TestValidPlistFile(unittest.TestCase):
     def setUp(self):
     
     def validateSimpleBinaryRoot(self, root):
         self.assertTrue(type(root) == dict, "Root should be dictionary.")
-        self.assertTrue(type(root['dateItem']) == datetime.datetime, "date should be datetime")
-        self.assertEquals(root['dateItem'], datetime.datetime(2010, 8, 19, 22, 27, 30, 385449), "dates not equal" )
-        self.assertEquals(root['numberItem'], -10000000000000000L, "number not of expected value")
-        self.assertEquals(root['unicodeItem'], u'abc\u212cdef\u2133')
-        self.assertEquals(root['stringItem'], 'Hi there')
-        self.assertEquals(root['realItem'], 0.47)
-        self.assertEquals(root['boolItem'], True)
-        self.assertEquals(root['arrayItem'], ['item0'])
+        self.assertTrue(type(root[six.b('dateItem')]) == datetime.datetime, "date should be datetime")
+        self.assertEquals(root[six.b('dateItem')], datetime.datetime(2010, 8, 19, 22, 27, 30, 385449), "dates not equal" )
+        self.assertEquals(root[six.b('numberItem')], -10000000000000000, "number not of expected value")
+        self.assertEquals(root[six.b('unicodeItem')], six.u('abc\u212cdef\u2133'))
+        self.assertEquals(root[six.b('stringItem')], six.b('Hi there'))
+        self.assertEquals(root[six.b('realItem')], 0.47)
+        self.assertEquals(root[six.b('boolItem')], True)
+        self.assertEquals(root[six.b('arrayItem')], [six.b('item0')])
         
     def testFileRead(self):
         try:
     
     def testUnicodeRoot(self):
         result = readPlist(data_path('unicode_root.plist'))
-        self.assertEquals(result, u"Mirror's Edge\u2122 for iPad")
+        self.assertEquals(result, six.u("Mirror's Edge\u2122 for iPad"))
     
     def testEmptyUnicodeRoot(self):
+        # Porting note: this test was tricky; it was only passing in
+        # Python 2 because the empty byte-string returned by
+        # readPlist() is considered equal to the empty unicode-string
+        # in the assertion.  Confusingly enough, given the name of the
+        # test, the value in unicode_empty.plist has the format byte
+        # 0b0101 (ASCII string), so the value being asserted against
+        # appears to be what is wrong.
         result = readPlist(data_path('unicode_empty.plist'))
-        self.assertEquals(result, u"")
+        self.assertEquals(result, six.b(""))
     
     def testSmallReal(self):
         result = readPlist(data_path('small_real.plist'))
-        self.assertEquals(result, {'4 byte real':0.5})
+        self.assertEquals(result, {six.b('4 byte real'):0.5})
     
     def testKeyedArchiverPlist(self):
         """
         ...
         """
         result = readPlist(data_path('nskeyedarchiver_example.plist'))
-        self.assertEquals(result, {'$version': 100000, 
-            '$objects': 
-                ['$null', 
-                 {'$class': Uid(3), 'somekey': Uid(2)}, 
-                 'object value as string', 
-                 {'$classes': ['Archived', 'NSObject'], '$classname': 'Archived'}
+        self.assertEquals(result, {six.b('$version'): 100000, 
+            six.b('$objects'): 
+                [six.b('$null'),
+                 {six.b('$class'): Uid(3), six.b('somekey'): Uid(2)}, 
+                 six.b('object value as string'),
+                 {six.b('$classes'): [six.b('Archived'), six.b('NSObject')], six.b('$classname'): six.b('Archived')}
                  ], 
-            '$top': {'root': Uid(1)}, '$archiver': 'NSKeyedArchiver'})
+            six.b('$top'): {six.b('root'): Uid(1)}, six.b('$archiver'): six.b('NSKeyedArchiver')})
         self.assertEquals("Uid(1)", repr(Uid(1)))
     
 if __name__ == '__main__':

File tests/test_write.py

 from biplist import PlistWriter
 import datetime
 import os
-from cStringIO import StringIO
+#from cStringIO import StringIO
 import subprocess
 import tempfile
 from test_utils import *
 import unittest
+import six
 
 class TestWritePlist(unittest.TestCase):
     def setUp(self):
         pass
     
-    def roundTrip(self, root, xml=False):
+    def roundTrip(self, root, xml=False, expected=None):
+        # 'expected' is more fallout from the
+        # don't-write-empty-unicode-strings issue.
         plist = writePlistToString(root, binary=(not xml))
         self.assertTrue(len(plist) > 0)
         readResult = readPlistFromString(plist)
-        self.assertEquals(readResult, root)
+        self.assertEquals(readResult, (expected if expected is not None else root))
         self.lintPlist(plist)
     
     def lintPlist(self, plistString):
 
     def testXMLPlistWithData(self):
         for binmode in (True, False):
-            binplist = writePlistToString({'data': Data('\x01\xac\xf0\xff')}, binary=binmode)
+            binplist = writePlistToString({'data': Data(six.b('\x01\xac\xf0\xff'))}, binary=binmode)
             plist = readPlistFromString(binplist)
             self.assertTrue(isinstance(plist['data'], Data), \
                 "unable to encode then decode Data into %s plist" % ("binary" if binmode else "XML"))
 
     def testConvertToXMLPlistWithData(self):
-        binplist = writePlistToString({'data': Data('\x01\xac\xf0\xff')})
+        binplist = writePlistToString({'data': Data(six.b('\x01\xac\xf0\xff'))})
         plist = readPlistFromString(binplist)
         xmlplist = writePlistToString(plist, binary=False)
         self.assertTrue(len(xmlplist) > 0, "unable to convert plist with Data from binary to XML")
         self.roundTrip(False)
     
     def testDuplicate(self):
-        l = ["foo" for i in xrange(0, 100)]
+        l = ["foo" for i in range(0, 100)]
         self.roundTrip(l)
         
     def testListRoot(self):
         self.roundTrip(root)
     
     def testString(self):
-        self.roundTrip('0')
-        self.roundTrip('')
-        self.roundTrip({'a':''})
+        self.roundTrip(six.b('0'))
+        self.roundTrip(six.b(''))
+        self.roundTrip({six.b('a'):six.b('')})
     
     def testLargeDict(self):
         d = {}
-        for i in xrange(0, 1000):
+        for i in range(0, 1000):
             d['%d' % i] = '%d' % i
         self.roundTrip(d)
         
             path = '/var/tmp/test.plist'
             writePlist([1, 2, 3], path, binary=is_binary)
             self.assertTrue(os.path.exists(path))
-            self.lintPlist(open(path).read())
+            self.lintPlist(open(path, 'rb').read())
     
     def testNone(self):
         self.roundTrip(None)
         except InvalidPlistException as e:
             pass
         try:
-            self.roundTrip({Data("hello world"):1})
+            self.roundTrip({Data(six.b("hello world")):1})
             self.fail("Data is not a valid key in Cocoa.")
         except InvalidPlistException as e:
             pass
         edges = [-pow(2, 7), pow(2, 7) - 1, -pow(2, 15), pow(2, 15) - 1, -pow(2, 31), pow(2, 31) - 1]
         self.roundTrip(edges)
         
-        io = StringIO()
+        io = six.BytesIO()
         writer = PlistWriter(io)
         bytes = [(1, [pow(2, 7) - 1]),
                  (2, [pow(2, 15) - 1]),
             pass
     
     def testWriteData(self):
-        self.roundTrip(Data("woohoo"))
+        self.roundTrip(Data(six.b("woohoo")))
         
     def testUnicode(self):
-        unicodeRoot = u"Mirror's Edge\u2122 for iPad"
+        unicodeRoot = six.u("Mirror's Edge\u2122 for iPad")
         writePlist(unicodeRoot, "/tmp/odd.plist")
         self.roundTrip(unicodeRoot)
-        unicodeStrings = [u"Mirror's Edge\u2122 for iPad", u'Weightbot \u2014 Track your Weight in Style']
+        unicodeStrings = [six.u("Mirror's Edge\u2122 for iPad"), six.u('Weightbot \u2014 Track your Weight in Style')]
         self.roundTrip(unicodeStrings)
-        self.roundTrip({u"":u""})
-        self.roundTrip(u"")
+        self.roundTrip({six.u(""):six.u("")}, expected={six.b(''):six.b('')})
+        self.roundTrip(six.u(""), expected=six.b(''))
         
     def testUidWrite(self):
         self.roundTrip({'$version': 100000, 
+[tox]
+envlist = py26, py27, py32
+[testenv]
+deps = nose
+commands = nosetests