Hello community, here is the log from the commit of package python-biplist for openSUSE:Factory checked in at 2017-09-04 12:31:28 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-biplist (Old) and /work/SRC/openSUSE:Factory/.python-biplist.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-biplist" Mon Sep 4 12:31:28 2017 rev:10 rq:519841 version:1.0.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-biplist/python-biplist.changes 2015-04-27 13:05:35.000000000 +0200 +++ /work/SRC/openSUSE:Factory/.python-biplist.new/python-biplist.changes 2017-09-04 12:31:31.036051169 +0200 @@ -1,0 +2,21 @@ +Thu Aug 31 06:07:29 UTC 2017 - toddrme2...@gmail.com + +- Update to version 1.0.2 + * Sort sets and dictionaries by key when writing. +- Update to version 1.0.1 + * Adding back in Python 2.6 support. This will be removed again in a future version. +- Update to version 1.0.0 + * This release changes the type of `Uid` from a subclass of `int` to a subclass of `object`. + * This change was made to address GitHub issue Ints are being turned into Uids and vice versa when both are present in a plist. + * This release also bumps the minimum supported Python versions to 2.7 and 3.4. +- Update to version 0.9.1 + * Fixes GitHub issue ERROR: testLargeDates (test_valid.TestValidPlistFile) + * Fixes Empty Data object converted as empty string + * Creates 1-byte strings when possible + +------------------------------------------------------------------- +Thu Aug 24 13:33:20 UTC 2017 - jmate...@suse.com + +- singlespec auto-conversion + +------------------------------------------------------------------- @@ -38,0 +60 @@ + Old: ---- biplist-0.9.tar.gz New: ---- biplist-1.0.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-biplist.spec ++++++ --- /var/tmp/diff_new_pack.zuKIGu/_old 2017-09-04 12:31:32.275876850 +0200 +++ /var/tmp/diff_new_pack.zuKIGu/_new 2017-09-04 12:31:32.283875725 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-biplist # -# Copyright (c) 2015 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany. # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -15,26 +15,31 @@ # Please submit bugfixes or comments via http://bugs.opensuse.org/ # +%ifarch x86_64 +%bcond_without test +%else +%bcond_with test +%endif +%{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-biplist -Version: 0.9 +Version: 1.0.2 Release: 0 -Url: https://bitbucket.org/wooster/biplist Summary: A library for reading/writing binary plists License: BSD-3-Clause Group: Development/Languages/Python -Source: http://pypi.python.org/packages/source/b/biplist/biplist-%{version}.tar.gz -BuildRoot: %{_tmppath}/%{name}-%{version}-build +Url: https://bitbucket.org/wooster/biplist +Source: https://files.pythonhosted.org/packages/source/b/biplist/biplist-%{version}.tar.gz +BuildRequires: %{python_module devel} +BuildRequires: %{python_module setuptools} BuildRequires: fdupes -BuildRequires: python-coverage -BuildRequires: python-devel >= 2.7 -BuildRequires: python-nose -BuildRequires: python-setuptools -%if 0%{?suse_version} && 0%{?suse_version} <= 1110 -%{!?python_sitelib: %global python_sitelib %(python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} -%else -BuildArch: noarch +BuildRequires: python-rpm-macros +%if %{with test} +BuildRequires: %{python_module coverage} +BuildRequires: %{python_module nose} %endif +BuildArch: noarch +%python_subpackages %description biplist is a binary plist parser/generator for Python. @@ -47,16 +52,18 @@ %setup -q -n biplist-%{version} %build -python setup.py build +%python_build %install -python setup.py install --prefix=%{_prefix} --root=%{buildroot} -%fdupes %buildroot/%_prefix +%python_install +%python_expand %fdupes %{buildroot}%{$python_sitelib} +%if %{with test} %check -python setup.py -q test +%python_exec setup.py test +%endif -%files +%files %{python_files} %defattr(-,root,root,-) %doc AUTHORS LICENSE README.md %{python_sitelib}/* ++++++ biplist-0.9.tar.gz -> biplist-1.0.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/biplist-0.9/PKG-INFO new/biplist-1.0.2/PKG-INFO --- old/biplist-0.9/PKG-INFO 2014-10-26 20:09:20.000000000 +0100 +++ new/biplist-1.0.2/PKG-INFO 2017-05-11 01:02:57.000000000 +0200 @@ -1,19 +1,19 @@ Metadata-Version: 1.1 Name: biplist -Version: 0.9 +Version: 1.0.2 Summary: biplist is a library for reading/writing binary plists. Home-page: https://bitbucket.org/wooster/biplist Author: Andrew Wooster Author-email: and...@planetaryscale.com License: BSD -Download-URL: https://bitbucket.org/wooster/biplist/downloads/biplist-0.9.tar.gz +Download-URL: https://bitbucket.org/wooster/biplist/downloads/biplist-1.0.2.tar.gz Description: `biplist` is a binary plist parser/generator for Python. Binary Property List (plist) files provide a faster and smaller serialization 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 or Python 3.2 or higher. + This module requires Python 2.6 or higher or Python 3.4 or higher. Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/biplist-0.9/biplist/__init__.py new/biplist-1.0.2/biplist/__init__.py --- old/biplist-0.9/biplist/__init__.py 2014-10-26 20:03:11.000000000 +0100 +++ new/biplist-1.0.2/biplist/__init__.py 2017-05-11 00:53:56.000000000 +0200 @@ -44,13 +44,12 @@ print "Not a plist:", e """ -import sys from collections import namedtuple import datetime import io import math import plistlib -from struct import pack, unpack +from struct import pack, unpack, unpack_from from struct import error as struct_error import sys import time @@ -79,23 +78,41 @@ # Apple uses Jan 1, 2001 as a base for all plist date/times. apple_reference_date = datetime.datetime.utcfromtimestamp(978307200) -class Uid(int): +class Uid(object): """Wrapper around integers for representing UID values. This is used in keyed archiving.""" + integer = 0 + def __init__(self, integer): + self.integer = integer + def __repr__(self): - return "Uid(%d)" % self + return "Uid(%d)" % self.integer + + def __eq__(self, other): + if isinstance(self, Uid) and isinstance(other, Uid): + return self.integer == other.integer + return False + + def __cmp__(self, other): + return self.integer - other.integer + + def __lt__(self, other): + return self.integer < other.integer + + def __hash__(self): + return self.integer + + def __int__(self): + return int(self.integer) class Data(bytes): - """Wrapper around str types for representing Data values.""" - pass + """Wrapper around bytes to distinguish Data values.""" class InvalidPlistException(Exception): """Raised when the plist is incorrectly formatted.""" - pass class NotBinaryPlistException(Exception): """Raised when a binary plist was expected but not encountered.""" - pass def readPlist(pathOrFile): """Raises NotBinaryPlistException, InvalidPlistException""" @@ -379,7 +396,7 @@ def readAsciiString(self, length): result = unpack("!%ds" % length, self.contents[self.currentOffset:self.currentOffset+length])[0] self.currentOffset += length - return result + return str(result.decode('ascii')) def readUnicode(self, length): actual_length = length*2 @@ -426,7 +443,9 @@ result = int.from_bytes(data, 'big') else: for byte in data: - result = (result << 8) | unpack('>B', byte)[0] + if not isinstance(byte, int): # Python3.0-3.1.x return ints, 2.x return str + byte = unpack_from('>B', byte)[0] + result = (result << 8) | byte else: raise InvalidPlistException("Encountered integer longer than 16 bytes.") return result @@ -456,6 +475,51 @@ def __repr__(self): return "<FloatWrapper: %s>" % self.value +class StringWrapper(object): + __instances = {} + + encodedValue = None + encoding = None + + def __new__(cls, value): + '''Ensure we only have a only one instance for any string, + and that we encode ascii as 1-byte-per character when possible''' + + encodedValue = None + + for encoding in ('ascii', 'utf_16_be'): + try: + encodedValue = value.encode(encoding) + except: pass + if encodedValue is not None: + if encodedValue not in cls.__instances: + cls.__instances[encodedValue] = super(StringWrapper, cls).__new__(cls) + cls.__instances[encodedValue].encodedValue = encodedValue + cls.__instances[encodedValue].encoding = encoding + return cls.__instances[encodedValue] + + raise ValueError('Unable to get ascii or utf_16_be encoding for %s' % repr(value)) + + def __len__(self): + '''Return roughly the number of characters in this string (half the byte length)''' + if self.encoding == 'ascii': + return len(self.encodedValue) + else: + return len(self.encodedValue)//2 + + def __lt__(self, other): + return self.encodedValue < other.encodedValue + + @property + def encodingMarker(self): + if self.encoding == 'ascii': + return 0b0101 + else: + return 0b0110 + + def __repr__(self): + return '<StringWrapper (%s): %s>' % (self.encoding, self.encodedValue) + class PlistWriter(object): header = b'bplist00bybiplist1.0' file = None @@ -507,10 +571,9 @@ """ output = self.header wrapped_root = self.wrapRoot(root) - should_reference_root = True#not isinstance(wrapped_root, HashableWrapper) - self.computeOffsets(wrapped_root, asReference=should_reference_root, isRoot=True) + self.computeOffsets(wrapped_root, asReference=True, isRoot=True) self.trailer = self.trailer._replace(**{'objectRefSize':self.intSize(len(self.computedUniques))}) - (_, output) = self.writeObjectReference(wrapped_root, output) + self.writeObjectReference(wrapped_root, output) output = self.writeObject(wrapped_root, output, setReferencePosition=True) # output size at this point is an upper bound on how big the @@ -552,6 +615,10 @@ elif isinstance(root, tuple): n = tuple([self.wrapRoot(value) for value in root]) return HashableWrapper(n) + elif isinstance(root, (str, unicode)) and not isinstance(root, Data): + return StringWrapper(root) + elif isinstance(root, bytes): + return Data(root) else: return root @@ -564,7 +631,7 @@ 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, (bytes, unicode)): + elif not isinstance(key, StringWrapper): raise InvalidPlistException('Keys must be strings.') def proc_size(size): @@ -584,7 +651,7 @@ elif isinstance(obj, BoolWrapper): self.incrementByteCount('boolBytes') elif isinstance(obj, Uid): - size = self.intSize(obj) + size = self.intSize(obj.integer) self.incrementByteCount('uidBytes', incr=1+size) elif isinstance(obj, (int, long)): size = self.intSize(obj) @@ -597,7 +664,7 @@ elif isinstance(obj, Data): size = proc_size(len(obj)) self.incrementByteCount('dataBytes', incr=1+size) - elif isinstance(obj, (unicode, bytes)): + elif isinstance(obj, StringWrapper): size = proc_size(len(obj)) self.incrementByteCount('stringBytes', incr=1+size) elif isinstance(obj, HashableWrapper): @@ -621,7 +688,7 @@ self.computeOffsets(key, asReference=True) self.computeOffsets(value, asReference=True) else: - raise InvalidPlistException("Unknown object type.") + raise InvalidPlistException("Unknown object type: %s (%s)" % (type(obj).__name__, repr(obj))) def writeObjectReference(self, obj, output): """Tries to write an object reference, adding it to the references @@ -653,9 +720,10 @@ result += pack('!B', (format << 4) | length) return result - if isinstance(obj, (str, unicode)) and obj == unicodeEmpty: - # The Apple Plist decoder can't decode a zero length Unicode string. - obj = b'' + def timedelta_total_seconds(td): + # Shim for Python 2.6 compatibility, which doesn't have total_seconds. + # Make one argument a float to ensure the right calculation. + return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10.0**6) / 10.0**6 if setReferencePosition: self.referencePositions[obj] = len(output) @@ -668,9 +736,9 @@ else: output += pack('!B', 0b00001001) elif isinstance(obj, Uid): - size = self.intSize(obj) + size = self.intSize(obj.integer) output += pack('!B', (0b1000 << 4) | size - 1) - output += self.binaryInt(obj) + output += self.binaryInt(obj.integer) elif isinstance(obj, (int, long)): byteSize = self.intSize(obj) root = math.log(byteSize, 2) @@ -681,16 +749,18 @@ output += pack('!B', (0b0010 << 4) | 3) output += self.binaryReal(obj) elif isinstance(obj, datetime.datetime): - timestamp = (obj - apple_reference_date).total_seconds() + try: + timestamp = (obj - apple_reference_date).total_seconds() + except AttributeError: + timestamp = timedelta_total_seconds(obj - apple_reference_date) output += pack('!B', 0b00110011) output += pack('!d', float(timestamp)) elif isinstance(obj, Data): output += proc_variable_length(0b0100, len(obj)) output += obj - elif isinstance(obj, unicode): - byteData = obj.encode('utf_16_be') - output += proc_variable_length(0b0110, len(byteData)//2) - output += byteData + elif isinstance(obj, StringWrapper): + output += proc_variable_length(obj.encodingMarker, len(obj)) + output += obj.encodedValue elif isinstance(obj, bytes): output += proc_variable_length(0b0101, len(obj)) output += obj @@ -703,7 +773,7 @@ output += proc_variable_length(0b1010, len(obj)) objectsToWrite = [] - for objRef in obj: + for objRef in sorted(obj) if isinstance(obj, set) else obj: (isNew, output) = self.writeObjectReference(objRef, output) if isNew: objectsToWrite.append(objRef) @@ -714,7 +784,7 @@ keys = [] values = [] objectsToWrite = [] - for key, value in iteritems(obj): + for key, value in sorted(iteritems(obj)): keys.append(key) values.append(value) for key in keys: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/biplist-0.9/biplist.egg-info/PKG-INFO new/biplist-1.0.2/biplist.egg-info/PKG-INFO --- old/biplist-0.9/biplist.egg-info/PKG-INFO 2014-10-26 20:09:20.000000000 +0100 +++ new/biplist-1.0.2/biplist.egg-info/PKG-INFO 2017-05-11 01:02:57.000000000 +0200 @@ -1,19 +1,19 @@ Metadata-Version: 1.1 Name: biplist -Version: 0.9 +Version: 1.0.2 Summary: biplist is a library for reading/writing binary plists. Home-page: https://bitbucket.org/wooster/biplist Author: Andrew Wooster Author-email: and...@planetaryscale.com License: BSD -Download-URL: https://bitbucket.org/wooster/biplist/downloads/biplist-0.9.tar.gz +Download-URL: https://bitbucket.org/wooster/biplist/downloads/biplist-1.0.2.tar.gz Description: `biplist` is a binary plist parser/generator for Python. Binary Property List (plist) files provide a faster and smaller serialization 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 or Python 3.2 or higher. + This module requires Python 2.6 or higher or Python 3.4 or higher. Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/biplist-0.9/setup.py new/biplist-1.0.2/setup.py --- old/biplist-0.9/setup.py 2014-10-26 20:04:50.000000000 +0100 +++ new/biplist-1.0.2/setup.py 2017-05-11 00:55:28.000000000 +0200 @@ -12,21 +12,21 @@ major, minor, micro, releaselevel, serial = sys.version_info -if major <= 1 or (major == 2 and minor < 6) or (major == 3 and minor < 2): +if major <= 1 or (major == 2 and minor < 6) or (major == 3 and minor < 4): # N.B.: Haven't tested with older py3k versions. - print('This module supports Python 2 >= 2.6 and Python 3 >= 3.2.') + print('This module supports Python 2 >= 2.6 and Python 3 >= 3.4.') sys.exit(1) author = 'Andrew Wooster' email = 'and...@planetaryscale.com' -version = '0.9' +version = '1.0.2' desc = 'biplist is a library for reading/writing binary plists.' setup( name = 'biplist', version = version, url = 'https://bitbucket.org/wooster/biplist', - download_url = 'https://bitbucket.org/wooster/biplist/downloads/biplist-0.9.tar.gz', + download_url = 'https://bitbucket.org/wooster/biplist/downloads/biplist-%s.tar.gz' % version, license = 'BSD', description = desc, long_description = @@ -36,7 +36,7 @@ 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 or Python 3.2 or higher.""", +This module requires Python 2.6 or higher or Python 3.4 or higher.""", author = author, author_email = email, packages = find_packages(), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/biplist-0.9/tests/test_valid.py new/biplist-1.0.2/tests/test_valid.py --- old/biplist-0.9/tests/test_valid.py 2014-10-26 20:03:33.000000000 +0100 +++ new/biplist-1.0.2/tests/test_valid.py 2016-06-18 01:44:56.000000000 +0200 @@ -19,19 +19,19 @@ def validateSimpleBinaryRoot(self, root): self.assertTrue(type(root) == dict, "Root should be dictionary.") - self.assertTrue(type(root[b'dateItem']) == datetime.datetime, "date should be datetime") - us = root[b'dateItem'].microsecond + self.assertTrue(type(root['dateItem']) == datetime.datetime, "date should be datetime") + us = root['dateItem'].microsecond if us == 385448: # Python 3 doesn't round microseconds to the nearest value. - self.assertEqual(root[b'dateItem'], datetime.datetime(2010, 8, 19, 22, 27, 30, 385448), "dates not equal" ) + self.assertEqual(root['dateItem'], datetime.datetime(2010, 8, 19, 22, 27, 30, 385448), "dates not equal" ) else: - self.assertEqual(root[b'dateItem'], datetime.datetime(2010, 8, 19, 22, 27, 30, 385449), "dates not equal" ) - self.assertEqual(root[b'numberItem'], -10000000000000000, "number not of expected value") - self.assertEqual(root[b'unicodeItem'], toUnicode('abc\u212cdef\u2133')) - self.assertEqual(root[b'stringItem'], b'Hi there') - self.assertEqual(root[b'realItem'], 0.47) - self.assertEqual(root[b'boolItem'], True) - self.assertEqual(root[b'arrayItem'], [b'item0']) + self.assertEqual(root['dateItem'], datetime.datetime(2010, 8, 19, 22, 27, 30, 385449), "dates not equal" ) + self.assertEqual(root['numberItem'], -10000000000000000, "number not of expected value") + self.assertEqual(root['unicodeItem'], toUnicode('abc\u212cdef\u2133')) + self.assertEqual(root['stringItem'], 'Hi there') + self.assertEqual(root['realItem'], 0.47) + self.assertEqual(root['boolItem'], True) + self.assertEqual(root['arrayItem'], ['item0']) def testFileRead(self): try: @@ -55,17 +55,17 @@ # 0b0101 (ASCII string), so the value being asserted against # appears to be what is wrong. result = readPlist(data_path('unicode_empty.plist')) - self.assertEqual(result, b'') + self.assertEqual(result, '') def testSmallReal(self): result = readPlist(data_path('small_real.plist')) - self.assertEqual(result, {b'4 byte real':0.5}) + self.assertEqual(result, {'4 byte real':0.5}) def testLargeIntegers(self): result = readPlist(data_path('large_int_limits.plist')) - self.assertEqual(result[b'Max 8 Byte Unsigned Integer'], 18446744073709551615) - self.assertEqual(result[b'Min 8 Byte Signed Integer'], -9223372036854775808) - self.assertEqual(result[b'Max 8 Byte Signed Integer'], 9223372036854775807) + self.assertEqual(result['Max 8 Byte Unsigned Integer'], 18446744073709551615) + self.assertEqual(result['Min 8 Byte Signed Integer'], -9223372036854775808) + self.assertEqual(result['Max 8 Byte Signed Integer'], 9223372036854775807) def testLargeDates(self): result = readPlist(data_path("BFPersistentEventInfo.plist")) @@ -86,15 +86,31 @@ ... """ result = readPlist(data_path('nskeyedarchiver_example.plist')) - self.assertEqual(result, {b'$version': 100000, - b'$objects': - [b'$null', - {b'$class': Uid(3), b'somekey': Uid(2)}, - b'object value as string', - {b'$classes': [b'Archived', b'NSObject'], b'$classname': b'Archived'} - ], - b'$top': {b'root': Uid(1)}, b'$archiver': b'NSKeyedArchiver'}) + self.assertEqual(result, { + '$version': 100000, + '$objects': + [ + '$null', + {'$class':Uid(3), 'somekey':Uid(2)}, + 'object value as string', + {'$classes':['Archived', 'NSObject'], '$classname':'Archived'} + ], + '$top': {'root':Uid(1)}, + '$archiver':'NSKeyedArchiver' + }) self.assertEqual("Uid(1)", repr(Uid(1))) + def testUidComparisons(self): + self.assertTrue(Uid(-2) < Uid(-1)) + self.assertTrue(Uid(-1) < Uid(0)) + self.assertTrue(Uid(1) > Uid(0)) + self.assertTrue(Uid(1) > Uid(-2)) + self.assertTrue(Uid(-1) == Uid(-1)) + self.assertTrue(Uid(0) == Uid(0)) + self.assertTrue(Uid(1) == Uid(1)) + + self.assertFalse(1 == Uid(1)) + self.assertFalse(Uid(0) == 0) + if __name__ == '__main__': unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/biplist-0.9/tests/test_write.py new/biplist-1.0.2/tests/test_write.py --- old/biplist-0.9/tests/test_write.py 2014-10-26 20:03:11.000000000 +0100 +++ new/biplist-1.0.2/tests/test_write.py 2016-06-18 01:44:56.000000000 +0200 @@ -1,39 +1,82 @@ +#!/usr/local/env python +# -*- coding: utf-8 -*- + +import datetime, io, os, subprocess, sys, tempfile, unittest + from biplist import * from biplist import PlistWriter -import datetime -import io -import os -#from cStringIO import StringIO -import subprocess -import tempfile from test_utils import * -import unittest try: unicode + unicodeStr = lambda x: x.decode('utf-8') + toUnicode = lambda x: x.decode('unicode-escape') except NameError: unicode = str + unicodeStr = lambda x: x + toUnicode = lambda x: x +try: + xrange +except NameError: + xrange = range class TestWritePlist(unittest.TestCase): - def setUp(self): - pass - 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)) + def roundTrip(self, case, xml=False, expected=None, reprTest=True): + # reprTest may fail randomly if True and the values being encoded include a dictionary with more + # than one key. + + # convert to plist string + plist = writePlistToString(case, binary=(not xml)) self.assertTrue(len(plist) > 0) + + # confirm that lint is happy with the result + self.lintPlist(plist) + + # convert back readResult = readPlistFromString(plist) - self.assertEqual(readResult, (expected if expected is not None else root)) - self.lintPlist(plist) - - def lintPlist(self, plistString): - if os.path.exists('/usr/bin/plutil'): - f = tempfile.NamedTemporaryFile() - f.write(plistString) - f.flush() - name = f.name - (status, output) = run_command(['/usr/bin/plutil', '-lint', name]) + + # test equality + if reprTest is True: + self.assertEqual(repr(case if expected is None else expected), repr(readResult)) + else: + self.assertEqual((case if expected is None else expected), readResult) + + # write to file + plistFile = tempfile.NamedTemporaryFile(mode='wb+', suffix='.plist') + writePlist(case, plistFile, binary=(xml is False)) + plistFile.seek(0) + + # confirm that lint is happy with the result + self.lintPlist(plistFile) + + # read back from file + fileResult = readPlist(plistFile) + + # test equality + if reprTest is True: + self.assertEqual(repr(case if expected is None else expected), repr(fileResult)) + else: + self.assertEqual((case if expected is None else expected), fileResult) + + def lintPlist(self, plist): + if os.access('/usr/bin/plutil', os.X_OK): + plistFile = None + plistFilePath = None + + if hasattr(plist, 'name'): + plistFilePath = plist.name + else: + if hasattr(plist, 'read'): + plistFile = tempfile.NamedTemporaryFile('w%s' % ('b' if 'b' in plist.mode else '')) + plistFile.write(plist.read()) + else: + plistFile = tempfile.NamedTemporaryFile('w%s' % ('b' if isinstance(plist, bytes) else '')) + plistFile.write(plist) + plistFilePath = plistFile.name + plistFile.flush() + + status, output = run_command(['/usr/bin/plutil', '-lint', plistFilePath]) if status != 0: self.fail("plutil verification failed (status %d): %s" % (status, output)) @@ -58,36 +101,32 @@ self.roundTrip(False) def testDuplicate(self): - l = ["foo" for i in range(0, 100)] + l = ["foo" for i in xrange(0, 100)] self.roundTrip(l) def testListRoot(self): self.roundTrip([1, 2, 3]) def testDictRoot(self): - self.roundTrip({'a':1, 'B':'d'}) + self.roundTrip({'a':1, 'B':'d'}, reprTest=False) def mixedNumericTypesHelper(self, cases): result = readPlistFromString(writePlistToString(cases)) - for i in range(0, len(cases)): + for i in xrange(0, len(cases)): self.assertTrue(cases[i] == result[i]) self.assertEqual(type(cases[i]), type(result[i]), "Type mismatch on %d: %s != %s" % (i, repr(cases[i]), repr(result[i]))) - def reprChecker(self, case): - result = readPlistFromString(writePlistToString(case)) - self.assertEqual(repr(case), repr(result)) - def testBoolsAndIntegersMixed(self): self.mixedNumericTypesHelper([0, 1, True, False, None]) self.mixedNumericTypesHelper([False, True, 0, 1, None]) - self.reprChecker({unicode('1'):[True, False, 1, 0], unicode('0'):[1, 2, 0, {unicode('2'):[1, 0, False]}]}) - self.reprChecker([1, 1, 1, 1, 1, True, True, True, True]) + self.roundTrip({'1':[True, False, 1, 0], '0':[1, 2, 0, {'2':[1, 0, False]}]}, reprTest=False) + self.roundTrip([1, 1, 1, 1, 1, True, True, True, True]) def testFloatsAndIntegersMixed(self): self.mixedNumericTypesHelper([0, 1, 1.0, 0.0, None]) self.mixedNumericTypesHelper([0.0, 1.0, 0, 1, None]) - self.reprChecker({unicode('1'):[1.0, 0.0, 1, 0], unicode('0'):[1, 2, 0, {unicode('2'):[1, 0, 0.0]}]}) - self.reprChecker([1, 1, 1, 1, 1, 1.0, 1.0, 1.0, 1.0]) + self.roundTrip({'1':[1.0, 0.0, 1, 0], '0':[1, 2, 0, {'2':[1, 0, 0.0]}]}, reprTest=False) + self.roundTrip([1, 1, 1, 1, 1, 1.0, 1.0, 1.0, 1.0]) def testSetRoot(self): self.roundTrip(set((1, 2, 3))) @@ -112,36 +151,102 @@ self.lintPlist(writePlistToString(root)) self.roundTrip(root) - def testString(self): + def testBytes(self): self.roundTrip(b'0') self.roundTrip(b'') - self.roundTrip({b'a':b''}) + + self.roundTrip([b'0']) + self.roundTrip([b'']) + + self.roundTrip({'a': b'0'}) + self.roundTrip({'a': b''}) - def testLargeDict(self): - d = {} - for i in range(0, 1000): - d['%d' % i] = '%d' % i - self.roundTrip(d) + def testString(self): + self.roundTrip('') + self.roundTrip('a') + self.roundTrip('1') + + self.roundTrip(['']) + self.roundTrip(['a']) + self.roundTrip(['1']) + + self.roundTrip({'a':''}) + self.roundTrip({'a':'a'}) + self.roundTrip({'1':'a'}) + self.roundTrip({'a':'a'}) + self.roundTrip({'a':'1'}) + + def testUnicode(self): + # defaulting to 1 byte strings + if str != unicode: + self.roundTrip(unicodeStr(r''), expected='') + self.roundTrip(unicodeStr(r'a'), expected='a') + + self.roundTrip([unicodeStr(r'a')], expected=['a']) + + self.roundTrip({'a':unicodeStr(r'a')}, expected={'a':'a'}) + self.roundTrip({unicodeStr(r'a'):'a'}, expected={'a':'a'}) + self.roundTrip({unicodeStr(r''):unicodeStr(r'')}, expected={'':''}) + + # TODO: need a 4-byte unicode character + self.roundTrip(unicodeStr(r'ü')) + self.roundTrip([unicodeStr(r'ü')]) + self.roundTrip({'a':unicodeStr(r'ü')}) + self.roundTrip({unicodeStr(r'ü'):'a'}) + + self.roundTrip(toUnicode('\u00b6')) + self.roundTrip([toUnicode('\u00b6')]) + self.roundTrip({toUnicode('\u00b6'):toUnicode('\u00b6')}) + + self.roundTrip(toUnicode('\u1D161')) + self.roundTrip([toUnicode('\u1D161')]) + self.roundTrip({toUnicode('\u1D161'):toUnicode('\u1D161')}) + + # Smiley face emoji + self.roundTrip(toUnicode('\U0001f604')) + self.roundTrip([toUnicode('\U0001f604'), toUnicode('\U0001f604')]) + self.roundTrip({toUnicode('\U0001f604'):toUnicode('\U0001f604')}) + + def testNone(self): + self.roundTrip(None) + self.roundTrip({'1':None}) + self.roundTrip([None, None, None]) + def testBools(self): + self.roundTrip(True) + self.roundTrip(False) + self.roundTrip([True, False]) + + self.roundTrip({'a':True, 'b':False}, reprTest=False) def testUniques(self): root = {'hi':'there', 'halloo':'there'} - self.roundTrip(root) + self.roundTrip(root, reprTest=False) + + def testAllEmpties(self): + '''Primarily testint that an empty unicode and bytes are not mixed up''' + self.roundTrip([unicodeStr(''), '', b'', [], {}], expected=['', '', b'', [], {}]) + def testLargeDict(self): + d = dict((str(x), str(x)) for x in xrange(0, 1000)) + self.roundTrip(d, reprTest=False) + def testWriteToFile(self): for is_binary in [True, False]: - path = '/var/tmp/test.plist' - writePlist([1, 2, 3], path, binary=is_binary) - self.assertTrue(os.path.exists(path)) - with open(path, 'rb') as f: - self.lintPlist(f.read()) - - def testNone(self): - self.roundTrip(None) - self.roundTrip({'1':None}) - self.roundTrip([None, None, None]) + with tempfile.NamedTemporaryFile(mode='w%s' % ('b' if is_binary else ''), suffix='.plist') as plistFile: + # clear out the created file + os.unlink(plistFile.name) + self.assertFalse(os.path.exists(plistFile.name)) + + # write to disk + writePlist([1, 2, 3], plistFile.name, binary=is_binary) + self.assertTrue(os.path.exists(plistFile.name)) + + with open(plistFile.name, 'r%s' % ('b' if is_binary else '')) as f: + fileContents = f.read() + self.lintPlist(fileContents) def testBadKeys(self): try: @@ -169,7 +274,7 @@ -pow(2, 15), pow(2, 15) - 1, -pow(2, 31), pow(2, 31) - 1, -pow(2, 63), pow(2, 64) - 1] - self.roundTrip(edges) + self.roundTrip(edges, reprTest=False) ioBytes = io.BytesIO() writer = PlistWriter(ioBytes) @@ -185,7 +290,7 @@ self.assertEqual(bytelen, got, "Byte size is wrong. Expected %d, got %d" % (bytelen, got)) bytes_lists = [list(x) for x in bytes] - self.roundTrip(bytes_lists) + self.roundTrip(bytes_lists, reprTest=False) try: self.roundTrip([0x10000000000000000, pow(2, 64)]) @@ -193,17 +298,23 @@ except InvalidPlistException as e: pass - def testWriteData(self): - self.roundTrip(Data(b"woohoo")) - - def testUnicode(self): - unicodeRoot = unicode("Mirror's Edge\u2122 for iPad") - writePlist(unicodeRoot, "/tmp/odd.plist") + def testUnicode2(self): + unicodeRoot = toUnicode("Mirror's Edge\u2122 for iPad") self.roundTrip(unicodeRoot) - unicodeStrings = [unicode("Mirror's Edge\u2122 for iPad"), unicode('Weightbot \u2014 Track your Weight in Style')] + unicodeStrings = [toUnicode("Mirror's Edge\u2122 for iPad"), toUnicode('Weightbot \u2014 Track your Weight in Style')] self.roundTrip(unicodeStrings) - self.roundTrip({unicode(""):unicode("")}, expected={b'':b''}) - self.roundTrip(unicode(""), expected=b'') + self.roundTrip({toUnicode(""):toUnicode("")}, expected={'':''}) + self.roundTrip(toUnicode(""), expected='') + + def testWriteData(self): + self.roundTrip(Data(b"woohoo")) + + def testEmptyData(self): + data = Data(b'') + binplist = writePlistToString(data) + plist = readPlistFromString(binplist) + self.assertEqual(plist, data) + self.assertEqual(type(plist), type(data)) def testUidWrite(self): self.roundTrip({'$version': 100000, @@ -213,7 +324,14 @@ 'object value as string', {'$classes': ['Archived', 'NSObject'], '$classname': 'Archived'} ], - '$top': {'root': Uid(1)}, '$archiver': 'NSKeyedArchiver'}) + '$top': {'root': Uid(1)}, '$archiver': 'NSKeyedArchiver'}, reprTest=False) + + def testUidRoundTrip(self): + # Per https://github.com/wooster/biplist/issues/9 + self.roundTrip(Uid(1)) + self.roundTrip([Uid(1), 1]) + self.roundTrip([1, Uid(1)]) + self.roundTrip([Uid(1), Uid(1)]) if __name__ == '__main__': unittest.main()