aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--test/exif.py5
-rw-r--r--test/iptc.py5
-rw-r--r--test/metadata.py692
-rw-r--r--test/notifying_list.py338
-rw-r--r--test/rational.py80
-rw-r--r--test/xmp.py370
6 files changed, 1488 insertions, 2 deletions
diff --git a/test/exif.py b/test/exif.py
index d32e30d..44ae394 100644
--- a/test/exif.py
+++ b/test/exif.py
@@ -25,7 +25,10 @@
# ******************************************************************************
import unittest
-from pyexiv2 import ExifTag, ExifValueError, Rational
+
+from pyexiv2.exif import ExifTag, ExifValueError
+from pyexiv2.utils import Rational
+
import datetime
diff --git a/test/iptc.py b/test/iptc.py
index ba02599..5473090 100644
--- a/test/iptc.py
+++ b/test/iptc.py
@@ -25,7 +25,10 @@
# ******************************************************************************
import unittest
-from pyexiv2 import IptcTag, IptcValueError, FixedOffset
+
+from pyexiv2.iptc import IptcTag, IptcValueError
+from pyexiv2.utils import FixedOffset
+
import datetime
diff --git a/test/metadata.py b/test/metadata.py
new file mode 100644
index 0000000..af8646f
--- /dev/null
+++ b/test/metadata.py
@@ -0,0 +1,692 @@
+# -*- coding: utf-8 -*-
+
+# ******************************************************************************
+#
+# Copyright (C) 2009 Olivier Tilloy <olivier@tilloy.net>
+#
+# This file is part of the pyexiv2 distribution.
+#
+# pyexiv2 is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# pyexiv2 is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with pyexiv2; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA.
+#
+# Author: Olivier Tilloy <olivier@tilloy.net>
+#
+# ******************************************************************************
+
+import unittest
+
+from pyexiv2.metadata import ImageMetadata
+from pyexiv2.exif import ExifTag
+from pyexiv2.iptc import IptcTag
+from pyexiv2.xmp import XmpTag
+
+import datetime
+
+
+class ImageMock(object):
+
+ def __init__(self, filename):
+ self.filename = filename
+ self.read = False
+ self.written = False
+ self.tags = {'exif': {}, 'iptc': {}, 'xmp': {}}
+
+ def readMetadata(self):
+ self.read = True
+
+ def writeMetadata(self):
+ self.written = True
+
+ def exifKeys(self):
+ return self.tags['exif'].keys()
+
+ def getExifTag(self, key):
+ return self.tags['exif'][key]
+
+ def setExifTagValue(self, key, value):
+ self.tags['exif'][key] = value
+
+ def deleteExifTag(self, key):
+ try:
+ del self.tags['exif'][key]
+ except KeyError:
+ pass
+
+ def iptcKeys(self):
+ return self.tags['iptc'].keys()
+
+ def getIptcTag(self, key):
+ return self.tags['iptc'][key]
+
+ def setIptcTagValues(self, key, values):
+ self.tags['iptc'][key] = values
+
+ def deleteIptcTag(self, key):
+ try:
+ del self.tags['iptc'][key]
+ except KeyError:
+ pass
+
+ def xmpKeys(self):
+ return self.tags['xmp'].keys()
+
+ def getXmpTag(self, key):
+ return self.tags['xmp'][key]
+
+ def setXmpTagValue(self, key, value):
+ self.tags['xmp'][key] = value
+
+ def deleteXmpTag(self, key):
+ try:
+ del self.tags['xmp'][key]
+ except KeyError:
+ pass
+
+
+class TestImageMetadata(unittest.TestCase):
+
+ def setUp(self):
+ self.metadata = ImageMetadata('nofile')
+ self.metadata._instantiate_image = lambda filename: ImageMock(filename)
+
+ def _set_exif_tags(self):
+ tags = {}
+ tags['Exif.Image.Make'] = \
+ ('Exif.Image.Make', 'Make', 'Manufacturer', 'blabla', 'Ascii',
+ 'EASTMAN KODAK COMPANY', 'EASTMAN KODAK COMPANY')
+ tags['Exif.Image.DateTime'] = \
+ ('Exif.Image.DateTime', 'DateTime', 'Date and Time', 'blabla',
+ 'Ascii', '2009:02:09 13:33:20', '2009:02:09 13:33:20')
+ tags['Exif.Photo.ExifVersion'] = \
+ ('Exif.Photo.ExifVersion', 'ExifVersion', 'Exif Version', 'blabla',
+ 'Undefined', '48 50 50 49 ', '2.21')
+ self.metadata._image.tags['exif'] = tags
+
+ def _set_iptc_tags(self):
+ tags = {}
+ tags['Iptc.Application2.Caption'] = \
+ ('Iptc.Application2.Caption', 'Caption', 'Caption',
+ 'A textual description of the object data.', 'String',
+ ['blabla'])
+ tags['Iptc.Application2.DateCreated'] = \
+ ('Iptc.Application2.DateCreated', 'DateCreated', 'Date Created',
+ 'Represented in the form CCYYMMDD to designate the date the ' \
+ 'intellectual content of the object data was created rather ' \
+ 'than the date of the creation of the physical representation. ' \
+ 'Follows ISO 8601 standard.', 'Date', ['2004-07-13'])
+ self.metadata._image.tags['iptc'] = tags
+
+ def _set_xmp_tags(self):
+ tags = {}
+ tags['Xmp.dc.format'] = \
+ ('Xmp.dc.format', 'format', 'Format', 'The file format used when ' \
+ 'saving the resource. Tools and applications should set this ' \
+ 'property to the save format of the data. It may include ' \
+ 'appropriate qualifiers.', 'MIMEType', 'image/jpeg')
+ tags['Xmp.dc.subject'] = \
+ ('Xmp.dc.subject', 'subject', 'Subject', 'An unordered array of ' \
+ 'descriptive phrases or keywords that specify the topic of the ' \
+ 'content of the resource.', 'bag Text', 'image, test, pyexiv2')
+ tags['Xmp.xmp.CreateDate'] = \
+ ('Xmp.xmp.CreateDate', 'CreateDate', 'Create Date',
+ 'The date and time the resource was originally created.', 'Date',
+ '2005-09-07T15:07:40-07:00')
+ tags['Xmp.xmpMM.DocumentID'] = \
+ ('Xmp.xmpMM.DocumentID', 'DocumentID', 'Document ID',
+ 'The common identifier for all versions and renditions of a ' \
+ 'document. It should be based on a UUID; see Document and ' \
+ 'Instance IDs below.', 'URI',
+ 'uuid:9A3B7F52214211DAB6308A7391270C13')
+ self.metadata._image.tags['xmp'] = tags
+
+ ######################
+ # Test general methods
+ ######################
+
+ def test_read(self):
+ self.assertEqual(self.metadata._image, None)
+ self.metadata.read()
+ self.failIfEqual(self.metadata._image, None)
+ self.failUnless(self.metadata._image.read)
+
+ def test_write(self):
+ self.metadata.read()
+ self.failIf(self.metadata._image.written)
+ self.metadata.write()
+ self.failUnless(self.metadata._image.written)
+
+ ###########################
+ # Test EXIF-related methods
+ ###########################
+
+ def test_exif_keys(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ self.assertEqual(self.metadata._keys['exif'], None)
+ keys = self.metadata.exif_keys
+ self.assertEqual(len(keys), 3)
+ self.assertEqual(self.metadata._keys['exif'], keys)
+
+ def test_get_exif_tag(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ self.assertEqual(self.metadata._tags['exif'], {})
+ # Get an existing tag
+ key = 'Exif.Image.Make'
+ tag = self.metadata._get_exif_tag(key)
+ self.assertEqual(type(tag), ExifTag)
+ self.assertEqual(tag.metadata, self.metadata)
+ self.assertEqual(self.metadata._tags['exif'][key], tag)
+ # Try to get an nonexistent tag
+ key = 'Exif.Photo.Sharpness'
+ self.failUnlessRaises(KeyError, self.metadata._get_exif_tag, key)
+
+ def test_set_exif_tag_wrong(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ self.assertEqual(self.metadata._tags['exif'], {})
+ # Try to set a tag with wrong type
+ tag = 'Not an exif tag'
+ self.failUnlessRaises(TypeError, self.metadata._set_exif_tag, tag)
+ self.assertEqual(self.metadata._tags['exif'], {})
+
+ def test_set_exif_tag_create(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ self.assertEqual(self.metadata._tags['exif'], {})
+ # Create a new tag
+ tag = ExifTag('Exif.Thumbnail.Orientation', 'Orientation',
+ 'Orientation', 'The image orientation viewed in terms ' \
+ 'of rows and columns.', 'Short', '1', 'top, left')
+ self.assertEqual(tag.metadata, None)
+ self.metadata._set_exif_tag(tag)
+ self.assertEqual(tag.metadata, self.metadata)
+ self.assertEqual(self.metadata._tags['exif'], {tag.key: tag})
+ self.assert_(self.metadata._image.tags['exif'].has_key(tag.key))
+ self.assertEqual(self.metadata._image.tags['exif'][tag.key],
+ tag.raw_value)
+
+ def test_set_exif_tag_overwrite(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ self.assertEqual(self.metadata._tags['exif'], {})
+ # Overwrite an existing tag
+ tag = ExifTag('Exif.Image.DateTime', 'DateTime', 'Date and Time',
+ 'blabla', 'Ascii', '2009:03:20 20:32:00',
+ '2009:03:20 20:32:00')
+ self.assertEqual(tag.metadata, None)
+ self.metadata._set_exif_tag(tag)
+ self.assertEqual(tag.metadata, self.metadata)
+ self.assertEqual(self.metadata._tags['exif'], {tag.key: tag})
+ self.assert_(self.metadata._image.tags['exif'].has_key(tag.key))
+ self.assertEqual(self.metadata._image.tags['exif'][tag.key],
+ tag.raw_value)
+
+ def test_set_exif_tag_overwrite_already_cached(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ self.assertEqual(self.metadata._tags['exif'], {})
+ # Overwrite an existing tag already cached
+ key = 'Exif.Photo.ExifVersion'
+ tag = self.metadata._get_exif_tag(key)
+ self.assertEqual(self.metadata._tags['exif'][key], tag)
+ new_tag = ExifTag(key, 'ExifVersion', 'Exif Version', 'blabla',
+ 'Undefined', '48 50 50 48 ', '2.20')
+ self.assertEqual(new_tag.metadata, None)
+ self.metadata._set_exif_tag(new_tag)
+ self.assertEqual(new_tag.metadata, self.metadata)
+ self.assertEqual(self.metadata._tags['exif'], {key: new_tag})
+ self.assert_(self.metadata._image.tags['exif'].has_key(key))
+ # Special case where the formatted value is used instead of the raw
+ # value.
+ self.assertEqual(self.metadata._image.tags['exif'][key], new_tag.fvalue)
+
+ def test_set_exif_tag_value_inexistent(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ key = 'Exif.Photo.ExposureTime'
+ value = '1/500'
+ self.failUnlessRaises(KeyError, self.metadata._set_exif_tag_value,
+ key, value)
+
+ def test_set_exif_tag_value_wrong_type(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ key = 'Exif.Image.DateTime'
+ value = datetime.datetime(2009, 3, 24, 9, 37, 36)
+ self.failUnlessRaises(TypeError, self.metadata._set_exif_tag_value,
+ key, value)
+
+ def test_set_exif_tag_value(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ key = 'Exif.Image.DateTime'
+ tag = self.metadata._get_exif_tag(key)
+ value = '2009:03:24 09:37:36'
+ self.failIfEqual(self.metadata._image.tags['exif'][key], value)
+ self.metadata._set_exif_tag_value(key, value)
+ self.assertEqual(self.metadata._image.tags['exif'][key], value)
+
+ def test_delete_exif_tag_inexistent(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ key = 'Exif.Image.Artist'
+ self.failUnlessRaises(KeyError, self.metadata._delete_exif_tag, key)
+
+ def test_delete_exif_tag_not_cached(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ key = 'Exif.Image.DateTime'
+ self.assertEqual(self.metadata._tags['exif'], {})
+ self.assert_(self.metadata._image.tags['exif'].has_key(key))
+ self.metadata._delete_exif_tag(key)
+ self.assertEqual(self.metadata._tags['exif'], {})
+ self.failIf(self.metadata._image.tags['exif'].has_key(key))
+
+ def test_delete_exif_tag_cached(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ key = 'Exif.Image.DateTime'
+ self.assert_(self.metadata._image.tags['exif'].has_key(key))
+ tag = self.metadata._get_exif_tag(key)
+ self.assertEqual(self.metadata._tags['exif'][key], tag)
+ self.metadata._delete_exif_tag(key)
+ self.assertEqual(self.metadata._tags['exif'], {})
+ self.failIf(self.metadata._image.tags['exif'].has_key(key))
+
+ ###########################
+ # Test IPTC-related methods
+ ###########################
+
+ def test_iptc_keys(self):
+ self.metadata.read()
+ self._set_iptc_tags()
+ self.assertEqual(self.metadata._keys['iptc'], None)
+ keys = self.metadata.iptc_keys
+ self.assertEqual(len(keys), 2)
+ self.assertEqual(self.metadata._keys['iptc'], keys)
+
+ def test_get_iptc_tag(self):
+ self.metadata.read()
+ self._set_iptc_tags()
+ self.assertEqual(self.metadata._tags['iptc'], {})
+ # Get an existing tag
+ key = 'Iptc.Application2.DateCreated'
+ tag = self.metadata._get_iptc_tag(key)
+ self.assertEqual(type(tag), IptcTag)
+ self.assertEqual(tag.metadata, self.metadata)
+ self.assertEqual(self.metadata._tags['iptc'][key], tag)
+ # Try to get an nonexistent tag
+ key = 'Iptc.Application2.Copyright'
+ self.failUnlessRaises(KeyError, self.metadata._get_iptc_tag, key)
+
+ def test_set_iptc_tag_wrong(self):
+ self.metadata.read()
+ self._set_iptc_tags()
+ self.assertEqual(self.metadata._tags['iptc'], {})
+ # Try to set a tag with wrong type
+ tag = 'Not an iptc tag'
+ self.failUnlessRaises(TypeError, self.metadata._set_iptc_tag, tag)
+ self.assertEqual(self.metadata._tags['iptc'], {})
+
+ def test_set_iptc_tag_create(self):
+ self.metadata.read()
+ self._set_iptc_tags()
+ self.assertEqual(self.metadata._tags['iptc'], {})
+ # Create a new tag
+ tag = IptcTag('Iptc.Application2.Writer', 'Writer', 'Writer',
+ 'Identification of the name of the person involved in ' \
+ 'the writing, editing or correcting the object data or ' \
+ 'caption/abstract.', 'String', ['Nobody'])
+ self.assertEqual(tag.metadata, None)
+ self.metadata._set_iptc_tag(tag)
+ self.assertEqual(tag.metadata, self.metadata)
+ self.assertEqual(self.metadata._tags['iptc'], {tag.key: tag})
+ self.assert_(self.metadata._image.tags['iptc'].has_key(tag.key))
+ self.assertEqual(self.metadata._image.tags['iptc'][tag.key],
+ tag.raw_value)
+
+ def test_set_iptc_tag_overwrite(self):
+ self.metadata.read()
+ self._set_iptc_tags()
+ self.assertEqual(self.metadata._tags['iptc'], {})
+ # Overwrite an existing tag
+ tag = IptcTag('Iptc.Application2.Caption', 'Caption', 'Caption',
+ 'A textual description of the object data.', 'String',
+ ['A picture.'])
+ self.assertEqual(tag.metadata, None)
+ self.metadata._set_iptc_tag(tag)
+ self.assertEqual(tag.metadata, self.metadata)
+ self.assertEqual(self.metadata._tags['iptc'], {tag.key: tag})
+ self.assert_(self.metadata._image.tags['iptc'].has_key(tag.key))
+ self.assertEqual(self.metadata._image.tags['iptc'][tag.key],
+ tag.raw_value)
+
+ def test_set_iptc_tag_overwrite_already_cached(self):
+ self.metadata.read()
+ self._set_iptc_tags()
+ self.assertEqual(self.metadata._tags['iptc'], {})
+ # Overwrite an existing tag already cached
+ key = 'Iptc.Application2.Caption'
+ tag = self.metadata._get_iptc_tag(key)
+ self.assertEqual(self.metadata._tags['iptc'][key], tag)
+ new_tag = IptcTag(key, 'Caption', 'Caption',
+ 'A textual description of the object data.', 'String',
+ ['A picture.'])
+ self.assertEqual(new_tag.metadata, None)
+ self.metadata._set_iptc_tag(new_tag)
+ self.assertEqual(new_tag.metadata, self.metadata)
+ self.assertEqual(self.metadata._tags['iptc'], {key: new_tag})
+ self.assert_(self.metadata._image.tags['iptc'].has_key(key))
+ self.assertEqual(self.metadata._image.tags['iptc'][key],
+ new_tag.raw_value)
+
+ def test_set_iptc_tag_values_inexistent(self):
+ self.metadata.read()
+ self._set_iptc_tags()
+ key = 'Iptc.Application2.Urgency'
+ values = ['1']
+ self.failUnlessRaises(KeyError, self.metadata._set_iptc_tag_values,
+ key, values)
+
+ def test_set_iptc_tag_values_wrong_type(self):
+ self.metadata.read()
+ self._set_iptc_tags()
+ key = 'Iptc.Application2.DateCreated'
+ value = '20090324'
+ self.failUnlessRaises(TypeError, self.metadata._set_iptc_tag_values,
+ key, value)
+ values = [datetime.date(2009, 3, 24)]
+ self.failUnlessRaises(TypeError, self.metadata._set_iptc_tag_values,
+ key, values)
+
+ def test_set_iptc_tag_values(self):
+ self.metadata.read()
+ self._set_iptc_tags()
+ key = 'Iptc.Application2.DateCreated'
+ tag = self.metadata._get_iptc_tag(key)
+ values = ['2009-04-07']
+ self.failIfEqual(self.metadata._image.tags['iptc'][key], values)
+ self.metadata._set_iptc_tag_values(key, values)
+ self.assertEqual(self.metadata._image.tags['iptc'][key], values)
+
+ def test_delete_iptc_tag_inexistent(self):
+ self.metadata.read()
+ self._set_iptc_tags()
+ key = 'Iptc.Application2.LocationCode'
+ self.failUnlessRaises(KeyError, self.metadata._delete_iptc_tag, key)
+
+ def test_delete_iptc_tag_not_cached(self):
+ self.metadata.read()
+ self._set_iptc_tags()
+ key = 'Iptc.Application2.Caption'
+ self.assertEqual(self.metadata._tags['iptc'], {})
+ self.assert_(self.metadata._image.tags['iptc'].has_key(key))
+ self.metadata._delete_iptc_tag(key)
+ self.assertEqual(self.metadata._tags['iptc'], {})
+ self.failIf(self.metadata._image.tags['iptc'].has_key(key))
+
+ def test_delete_iptc_tag_cached(self):
+ self.metadata.read()
+ self._set_iptc_tags()
+ key = 'Iptc.Application2.Caption'
+ self.assert_(self.metadata._image.tags['iptc'].has_key(key))
+ tag = self.metadata._get_iptc_tag(key)
+ self.assertEqual(self.metadata._tags['iptc'][key], tag)
+ self.metadata._delete_iptc_tag(key)
+ self.assertEqual(self.metadata._tags['iptc'], {})
+ self.failIf(self.metadata._image.tags['iptc'].has_key(key))
+
+ ##########################
+ # Test XMP-related methods
+ ##########################
+
+ def test_xmp_keys(self):
+ self.metadata.read()
+ self._set_xmp_tags()
+ self.assertEqual(self.metadata._keys['xmp'], None)
+ keys = self.metadata.xmp_keys
+ self.assertEqual(len(keys), 4)
+ self.assertEqual(self.metadata._keys['xmp'], keys)
+
+ def test_get_xmp_tag(self):
+ self.metadata.read()
+ self._set_xmp_tags()
+ self.assertEqual(self.metadata._tags['xmp'], {})
+ # Get an existing tag
+ key = 'Xmp.dc.subject'
+ tag = self.metadata._get_xmp_tag(key)
+ self.assertEqual(type(tag), XmpTag)
+ self.assertEqual(tag.metadata, self.metadata)
+ self.assertEqual(self.metadata._tags['xmp'][key], tag)
+ # Try to get an nonexistent tag
+ key = 'Xmp.xmp.Label'
+ self.failUnlessRaises(KeyError, self.metadata._get_xmp_tag, key)
+
+ def test_set_xmp_tag_wrong(self):
+ self.metadata.read()
+ self._set_xmp_tags()
+ self.assertEqual(self.metadata._tags['xmp'], {})
+ # Try to set a tag with wrong type
+ tag = 'Not an xmp tag'
+ self.failUnlessRaises(TypeError, self.metadata._set_xmp_tag, tag)
+ self.assertEqual(self.metadata._tags['xmp'], {})
+
+ def test_set_xmp_tag_create(self):
+ self.metadata.read()
+ self._set_xmp_tags()
+ self.assertEqual(self.metadata._tags['xmp'], {})
+ # Create a new tag
+ tag = XmpTag('Xmp.dc.title', 'title', 'Title', 'The title of the ' \
+ 'document, or the name given to the resource. Typically,' \
+ ' it will be a name by which the resource is formally ' \
+ 'known.', 'Lang Alt',
+ 'lang="x-default" This is not a title, ' \
+ 'lang="fr-FR" Ceci n\'est pas un titre')
+ self.assertEqual(tag.metadata, None)
+ self.metadata._set_xmp_tag(tag)
+ self.assertEqual(tag.metadata, self.metadata)
+ self.assertEqual(self.metadata._tags['xmp'], {tag.key: tag})
+ self.assert_(self.metadata._image.tags['xmp'].has_key(tag.key))
+ self.assertEqual(self.metadata._image.tags['xmp'][tag.key],
+ tag.raw_value)
+
+ def test_set_xmp_tag_overwrite(self):
+ self.metadata.read()
+ self._set_xmp_tags()
+ self.assertEqual(self.metadata._tags['xmp'], {})
+ # Overwrite an existing tag
+ tag = XmpTag('Xmp.dc.format', 'format', 'Format', 'The file format ' \
+ 'used when saving the resource. Tools and applications ' \
+ 'should set this property to the save format of the ' \
+ 'data. It may include appropriate qualifiers.',
+ 'MIMEType', 'image/png')
+ self.assertEqual(tag.metadata, None)
+ self.metadata._set_xmp_tag(tag)
+ self.assertEqual(tag.metadata, self.metadata)
+ self.assertEqual(self.metadata._tags['xmp'], {tag.key: tag})
+ self.assert_(self.metadata._image.tags['xmp'].has_key(tag.key))
+ self.assertEqual(self.metadata._image.tags['xmp'][tag.key],
+ tag.raw_value)
+
+ def test_set_xmp_tag_overwrite_already_cached(self):
+ self.metadata.read()
+ self._set_xmp_tags()
+ self.assertEqual(self.metadata._tags['xmp'], {})
+ # Overwrite an existing tag already cached
+ key = 'Xmp.xmp.CreateDate'
+ tag = self.metadata._get_xmp_tag(key)
+ self.assertEqual(self.metadata._tags['xmp'][key], tag)
+ new_tag = XmpTag(key, 'CreateDate', 'Create Date',
+ 'The date and time the resource was originally ' \
+ 'created.', 'Date', '2009-04-21T20:07+01:00')
+ self.assertEqual(new_tag.metadata, None)
+ self.metadata._set_xmp_tag(new_tag)
+ self.assertEqual(new_tag.metadata, self.metadata)
+ self.assertEqual(self.metadata._tags['xmp'], {key: new_tag})
+ self.assert_(self.metadata._image.tags['xmp'].has_key(key))
+ self.assertEqual(self.metadata._image.tags['xmp'][key],
+ new_tag.raw_value)
+
+ def test_set_xmp_tag_value_inexistent(self):
+ self.metadata.read()
+ self._set_xmp_tags()
+ key = 'Xmp.xmp.Nickname'
+ value = 'oSoMoN'
+ self.failUnlessRaises(KeyError, self.metadata._set_xmp_tag_value,
+ key, value)
+
+ def test_set_xmp_tag_value_wrong_type(self):
+ self.metadata.read()
+ self._set_xmp_tags()
+ key = 'Xmp.xmp.CreateDate'
+ value = datetime.datetime(2009, 4, 21, 20, 11, 0)
+ self.failUnlessRaises(TypeError, self.metadata._set_xmp_tag_value,
+ key, value)
+
+ def test_set_xmp_tag_value(self):
+ self.metadata.read()
+ self._set_xmp_tags()
+ key = 'Xmp.xmp.CreateDate'
+ tag = self.metadata._get_xmp_tag(key)
+ value = '2009-04-21T20:12:47+01:00'
+ self.failIfEqual(self.metadata._image.tags['xmp'][key], value)
+ self.metadata._set_xmp_tag_value(key, value)
+ self.assertEqual(self.metadata._image.tags['xmp'][key], value)
+
+ def test_delete_xmp_tag_inexistent(self):
+ self.metadata.read()
+ self._set_xmp_tags()
+ key = 'Xmp.xmp.CreatorTool'
+ self.failUnlessRaises(KeyError, self.metadata._delete_xmp_tag, key)
+
+ def test_delete_xmp_tag_not_cached(self):
+ self.metadata.read()
+ self._set_xmp_tags()
+ key = 'Xmp.dc.subject'
+ self.assertEqual(self.metadata._tags['xmp'], {})
+ self.assert_(self.metadata._image.tags['xmp'].has_key(key))
+ self.metadata._delete_xmp_tag(key)
+ self.assertEqual(self.metadata._tags['xmp'], {})
+ self.failIf(self.metadata._image.tags['xmp'].has_key(key))
+
+ def test_delete_xmp_tag_cached(self):
+ self.metadata.read()
+ self._set_xmp_tags()
+ key = 'Xmp.dc.subject'
+ self.assert_(self.metadata._image.tags['xmp'].has_key(key))
+ tag = self.metadata._get_xmp_tag(key)
+ self.assertEqual(self.metadata._tags['xmp'][key], tag)
+ self.metadata._delete_xmp_tag(key)
+ self.assertEqual(self.metadata._tags['xmp'], {})
+ self.failIf(self.metadata._image.tags['xmp'].has_key(key))
+
+ ###########################
+ # Test dictionary interface
+ ###########################
+
+ def test_getitem(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ self._set_iptc_tags()
+ self._set_xmp_tags()
+ # Get existing tags
+ key = 'Exif.Photo.ExifVersion'
+ tag = self.metadata[key]
+ self.assertEqual(type(tag), ExifTag)
+ key = 'Iptc.Application2.Caption'
+ tag = self.metadata[key]
+ self.assertEqual(type(tag), IptcTag)
+ key = 'Xmp.xmp.CreateDate'
+ tag = self.metadata[key]
+ self.assertEqual(type(tag), XmpTag)
+ # Try to get nonexistent tags
+ keys = ('Exif.Image.SamplesPerPixel', 'Iptc.Application2.FixtureId',
+ 'Xmp.xmp.Rating', 'Wrong.Noluck.Raise')
+ for key in keys:
+ self.failUnlessRaises(KeyError, self.metadata.__getitem__, key)
+
+ def test_setitem(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ self._set_iptc_tags()
+ self._set_xmp_tags()
+ # Set new tags
+ key = 'Exif.Photo.ExposureBiasValue'
+ tag = ExifTag(key, 'ExposureBiasValue', 'Exposure Bias',
+ 'The exposure bias. The units is the APEX value. ' \
+ 'Ordinarily it is given in the range of -99.99 to 99.99.',
+ 'SRational', '0/3', '0')
+ self.metadata[key] = tag
+ self.failUnless(key in self.metadata._tags['exif'])
+ self.failUnlessEqual(self.metadata._tags['exif'][key], tag)
+ key = 'Iptc.Application2.City'
+ tag = IptcTag(key, 'City', 'City', 'Identifies city of object data ' \
+ 'origin according to guidelines established by the ' \
+ 'provider.', 'String', ['Barcelona'])
+ self.metadata[key] = tag
+ self.failUnless(key in self.metadata._tags['iptc'])
+ self.failUnlessEqual(self.metadata._tags['iptc'][key], tag)
+ key = 'Xmp.dc.description'
+ tag = XmpTag(key, 'description', 'Description', 'A textual ' \
+ 'description of the content of the resource. Multiple ' \
+ 'values may be present for different languages.',
+ 'Lang Alt', 'lang="x-default" Sunset picture.')
+ self.metadata[key] = tag
+ self.failUnless(key in self.metadata._tags['xmp'])
+ self.failUnlessEqual(self.metadata._tags['xmp'][key], tag)
+ # Replace existing tags
+ key = 'Exif.Photo.ExifVersion'
+ tag = ExifTag(key, 'ExifVersion', 'Exif Version', 'The version of ' \
+ 'this standard supported. Nonexistence of this field is' \
+ ' taken to mean nonconformance to the standard.',
+ 'Undefined', '48 50 50 48 ', '2.20')
+ self.metadata[key] = tag
+ self.failUnless(key in self.metadata._tags['exif'])
+ self.failUnlessEqual(self.metadata._tags['exif'][key], tag)
+ key = 'Iptc.Application2.Caption'
+ tag = IptcTag(key, 'Caption', 'Caption', 'A textual description of ' \
+ 'the object data.', 'String', ['Sunset on Barcelona.'])
+ self.metadata[key] = tag
+ self.failUnless(key in self.metadata._tags['iptc'])
+ self.failUnlessEqual(self.metadata._tags['iptc'][key], tag)
+ key = 'Xmp.dc.subject'
+ tag = XmpTag(key, 'subject', 'Subject', 'An unordered array of ' \
+ 'descriptive phrases or keywords that specify the topic ' \
+ 'of the content of the resource.', 'bag Text',
+ 'sunset, Barcelona, beautiful, beach')
+ self.metadata[key] = tag
+ self.failUnless(key in self.metadata._tags['xmp'])
+ self.failUnlessEqual(self.metadata._tags['xmp'][key], tag)
+
+ def test_delitem(self):
+ self.metadata.read()
+ self._set_exif_tags()
+ self._set_iptc_tags()
+ self._set_xmp_tags()
+ # Delete existing tags
+ key = 'Exif.Photo.ExifVersion'
+ del self.metadata[key]
+ self.failIf(key in self.metadata._tags['exif'])
+ key = 'Iptc.Application2.Caption'
+ del self.metadata[key]
+ self.failIf(key in self.metadata._tags['iptc'])
+ key = 'Xmp.xmp.CreateDate'
+ del self.metadata[key]
+ self.failIf(key in self.metadata._tags['xmp'])
+ # Try to delete nonexistent tags
+ keys = ('Exif.Image.SamplesPerPixel', 'Iptc.Application2.FixtureId',
+ 'Xmp.xmp.Rating', 'Wrong.Noluck.Raise')
+ for key in keys:
+ self.failUnlessRaises(KeyError, self.metadata.__delitem__, key)
diff --git a/test/notifying_list.py b/test/notifying_list.py
new file mode 100644
index 0000000..e80ec9c
--- /dev/null
+++ b/test/notifying_list.py
@@ -0,0 +1,338 @@
+# -*- coding: utf-8 -*-
+
+# ******************************************************************************
+#
+# Copyright (C) 2009 Olivier Tilloy <olivier@tilloy.net>
+#
+# This file is part of the pyexiv2 distribution.
+#
+# pyexiv2 is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# pyexiv2 is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with pyexiv2; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA.
+#
+# Author: Olivier Tilloy <olivier@tilloy.net>
+#
+# ******************************************************************************
+
+import unittest
+from pyexiv2.utils import ListenerInterface, NotifyingList
+import random
+
+
+class SimpleListener(ListenerInterface):
+
+ def __init__(self):
+ self.changes = 0
+
+ def contents_changed(self):
+ self.changes += 1
+
+
+class TestNotifyingList(unittest.TestCase):
+
+ def setUp(self):
+ self.values = NotifyingList([5, 7, 9, 14, 57, 3, 2])
+
+ def test_no_listener(self):
+ # No listener is registered, nothing should happen.
+ self.values[3] = 13
+ del self.values[5]
+ self.values.append(17)
+ self.values.extend([11, 22])
+ self.values.insert(4, 24)
+ self.values.pop()
+ self.values.remove(9)
+ self.values.reverse()
+ self.values.sort()
+ self.values += [8, 4]
+ self.values *= 3
+ self.values[3:4] = [8, 4]
+ del self.values[3:5]
+
+ def test_listener_interface(self):
+ self.values.register_listener(ListenerInterface())
+ self.failUnlessRaises(NotImplementedError,
+ self.values.__setitem__, 3, 13)
+ self.failUnlessRaises(NotImplementedError, self.values.__delitem__, 5)
+ self.failUnlessRaises(NotImplementedError, self.values.append, 17)
+ self.failUnlessRaises(NotImplementedError, self.values.extend, [11, 22])
+ self.failUnlessRaises(NotImplementedError, self.values.insert, 4, 24)
+ self.failUnlessRaises(NotImplementedError, self.values.pop)
+ self.failUnlessRaises(NotImplementedError, self.values.remove, 9)
+ self.failUnlessRaises(NotImplementedError, self.values.reverse)
+ self.failUnlessRaises(NotImplementedError, self.values.sort)
+ self.failUnlessRaises(NotImplementedError, self.values.__iadd__, [8, 4])
+ self.failUnlessRaises(NotImplementedError, self.values.__imul__, 3)
+ self.failUnlessRaises(NotImplementedError, self.values.__setslice__,
+ 3, 4, [8, 4])
+ self.failUnlessRaises(NotImplementedError, self.values.__delslice__,
+ 3, 5)
+
+ def _register_listeners(self):
+ # Register a random number of listeners
+ listeners = [SimpleListener() for i in xrange(random.randint(3, 20))]
+ for listener in listeners:
+ self.values.register_listener(listener)
+ return listeners
+
+ def test_setitem(self):
+ listeners = self._register_listeners()
+
+ self.values[3] = 13
+ self.failUnlessEqual(self.values, [5, 7, 9, 13, 57, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ self.failUnlessRaises(IndexError, self.values.__setitem__, 9, 27)
+ self.failUnlessEqual(self.values, [5, 7, 9, 13, 57, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ def test_delitem(self):
+ listeners = self._register_listeners()
+
+ del self.values[5]
+ self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ self.failUnlessRaises(IndexError, self.values.__delitem__, 9)
+ self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ def test_append(self):
+ listeners = self._register_listeners()
+
+ self.values.append(17)
+ self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2, 17])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ def test_extend(self):
+ listeners = self._register_listeners()
+
+ self.values.extend([11, 22])
+ self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2, 11, 22])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ self.failUnlessRaises(TypeError, self.values.extend, 26)
+ self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2, 11, 22])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ def test_insert(self):
+ listeners = self._register_listeners()
+
+ self.values.insert(4, 24)
+ self.failUnlessEqual(self.values, [5, 7, 9, 14, 24, 57, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ def test_pop(self):
+ listeners = self._register_listeners()
+
+ self.values.pop()
+ self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ self.values.pop(4)
+ self.failUnlessEqual(self.values, [5, 7, 9, 14, 3])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 2)
+
+ self.values.pop(-2)
+ self.failUnlessEqual(self.values, [5, 7, 9, 3])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 3)
+
+ self.failUnlessRaises(IndexError, self.values.pop, 33)
+ self.failUnlessEqual(self.values, [5, 7, 9, 3])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 3)
+
+ def test_remove(self):
+ listeners = self._register_listeners()
+
+ self.values.remove(9)
+ self.failUnlessEqual(self.values, [5, 7, 14, 57, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ self.failUnlessRaises(ValueError, self.values.remove, 33)
+ self.failUnlessEqual(self.values, [5, 7, 14, 57, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ def test_reverse(self):
+ listeners = self._register_listeners()
+
+ self.values.reverse()
+ self.failUnlessEqual(self.values, [2, 3, 57, 14, 9, 7, 5])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ def test_sort(self):
+ listeners = self._register_listeners()
+
+ self.values.sort()
+ self.failUnlessEqual(self.values, [2, 3, 5, 7, 9, 14, 57])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ self.values.sort(cmp=lambda x, y: y - x)
+ self.failUnlessEqual(self.values, [57, 14, 9, 7, 5, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 2)
+
+ self.values.sort(key=lambda x: x * x)
+ self.failUnlessEqual(self.values, [2, 3, 5, 7, 9, 14, 57])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 3)
+
+ self.values.sort(reverse=True)
+ self.failUnlessEqual(self.values, [57, 14, 9, 7, 5, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 4)
+
+ def test_iadd(self):
+ listeners = self._register_listeners()
+
+ self.values += [44, 31, 19]
+ self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2, 44, 31, 19])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ def test_imul(self):
+ listeners = self._register_listeners()
+
+ self.values *= 3
+ self.failUnlessEqual(self.values,
+ [5, 7, 9, 14, 57, 3, 2,
+ 5, 7, 9, 14, 57, 3, 2,
+ 5, 7, 9, 14, 57, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ def test_setslice(self):
+ listeners = self._register_listeners()
+
+ # Basic slicing (of the form [i:j]): implemented as __setslice__.
+
+ self.values[2:4] = [3, 4]
+ self.failUnlessEqual(self.values, [5, 7, 3, 4, 57, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ self.values[3:5] = [77, 8, 12]
+ self.failUnlessEqual(self.values, [5, 7, 3, 77, 8, 12, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 2)
+
+ self.values[2:5] = [1, 0]
+ self.failUnlessEqual(self.values, [5, 7, 1, 0, 12, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 3)
+
+ self.values[0:2] = []
+ self.failUnlessEqual(self.values, [1, 0, 12, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 4)
+
+ self.values[2:2] = [7, 5]
+ self.failUnlessEqual(self.values, [1, 0, 7, 5, 12, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 5)
+
+ # With negatives indexes
+
+ self.values[4:-2] = [9]
+ self.failUnlessEqual(self.values, [1, 0, 7, 5, 9, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 6)
+
+ self.values[-2:1] = [6, 4]
+ self.failUnlessEqual(self.values, [1, 0, 7, 5, 9, 6, 4, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 7)
+
+ self.values[-5:-2] = [8]
+ self.failUnlessEqual(self.values, [1, 0, 7, 5, 8, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 8)
+
+ # With missing (implicit) indexes
+
+ self.values[:2] = [4]
+ self.failUnlessEqual(self.values, [4, 7, 5, 8, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 9)
+
+ self.values[4:] = [1]
+ self.failUnlessEqual(self.values, [4, 7, 5, 8, 1])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 10)
+
+ self.values[:] = [5, 7, 9, 14, 57, 3, 2]
+ self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 11)
+
+ def test_delslice(self):
+ listeners = self._register_listeners()
+
+ del self.values[2:3]
+ self.failUnlessEqual(self.values, [5, 7, 14, 57, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ del self.values[2:2]
+ self.failUnlessEqual(self.values, [5, 7, 14, 57, 3, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 1)
+
+ # With negatives indexes
+
+ del self.values[4:-1]
+ self.failUnlessEqual(self.values, [5, 7, 14, 57, 2])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 2)
+
+ del self.values[-1:5]
+ self.failUnlessEqual(self.values, [5, 7, 14, 57])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 3)
+
+ del self.values[-2:-1]
+ self.failUnlessEqual(self.values, [5, 7, 57])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 4)
+
+ # With missing (implicit) indexes
+
+ del self.values[:1]
+ self.failUnlessEqual(self.values, [7, 57])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 5)
+
+ del self.values[1:]
+ self.failUnlessEqual(self.values, [7])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 6)
+
+ del self.values[:]
+ self.failUnlessEqual(self.values, [])
+ for listener in listeners:
+ self.failUnlessEqual(listener.changes, 7)
diff --git a/test/rational.py b/test/rational.py
new file mode 100644
index 0000000..070bf6c
--- /dev/null
+++ b/test/rational.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+
+# ******************************************************************************
+#
+# Copyright (C) 2008-2009 Olivier Tilloy <olivier@tilloy.net>
+#
+# This file is part of the pyexiv2 distribution.
+#
+# pyexiv2 is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# pyexiv2 is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with pyexiv2; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA.
+#
+# Author: Olivier Tilloy <olivier@tilloy.net>
+#
+# ******************************************************************************
+
+import unittest
+
+from pyexiv2.utils import Rational
+
+
+class TestRational(unittest.TestCase):
+
+ def test_constructor(self):
+ r = Rational(2, 1)
+ self.assertEqual(r.numerator, 2)
+ self.assertEqual(r.denominator, 1)
+ self.assertRaises(ZeroDivisionError, Rational, 1, 0)
+
+ def test_read_only(self):
+ r = Rational(3, 4)
+ try:
+ r.numerator = 5
+ except AttributeError:
+ pass
+ else:
+ self.fail('Numerator is not read-only.')
+ try:
+ r.denominator = 5
+ except AttributeError:
+ pass
+ else:
+ self.fail('Denominator is not read-only.')
+
+ def test_from_string(self):
+ self.assertEqual(Rational.from_string('4/3'), Rational(4, 3))
+ self.assertEqual(Rational.from_string('-4/3'), Rational(-4, 3))
+ self.assertRaises(ValueError, Rational.from_string, '+3/5')
+ self.assertRaises(ValueError, Rational.from_string, '3 / 5')
+ self.assertRaises(ValueError, Rational.from_string, '3/-5')
+ self.assertRaises(ValueError, Rational.from_string, 'invalid')
+
+ def test_to_string(self):
+ self.assertEqual(str(Rational(3, 5)), '3/5')
+ self.assertEqual(str(Rational(-3, 5)), '-3/5')
+
+ def test_to_float(self):
+ self.assertEqual(Rational(3, 6).to_float(), 0.5)
+ self.assertEqual(Rational(11, 11).to_float(), 1.0)
+ self.assertEqual(Rational(-2, 8).to_float(), -0.25)
+ self.assertEqual(Rational(0, 3).to_float(), 0.0)
+
+ def test_equality(self):
+ r1 = Rational(2, 1)
+ r2 = Rational(2, 1)
+ r3 = Rational(8, 4)
+ r4 = Rational(3, 2)
+ self.assertEqual(r1, r2)
+ self.assertEqual(r1, r3)
+ self.assertNotEqual(r1, r4)
diff --git a/test/xmp.py b/test/xmp.py
new file mode 100644
index 0000000..a61e375
--- /dev/null
+++ b/test/xmp.py
@@ -0,0 +1,370 @@
+# -*- coding: utf-8 -*-
+
+# ******************************************************************************
+#
+# Copyright (C) 2009 Olivier Tilloy <olivier@tilloy.net>
+#
+# This file is part of the pyexiv2 distribution.
+#
+# pyexiv2 is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# pyexiv2 is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with pyexiv2; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA.
+#
+# Author: Olivier Tilloy <olivier@tilloy.net>
+#
+# ******************************************************************************
+
+import unittest
+
+from pyexiv2.xmp import XmpTag, XmpValueError
+from pyexiv2.utils import FixedOffset
+
+import datetime
+
+
+class ImageMetadataMock(object):
+
+ tags = {}
+
+ def _set_xmp_tag_value(self, key, value):
+ self.tags[key] = value
+
+ def _delete_xmp_tag(self, key):
+ try:
+ del self.tags[key]
+ except KeyError:
+ pass
+
+
+class TestXmpTag(unittest.TestCase):
+
+ def test_convert_to_python_bag(self):
+ xtype = 'bag Text'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_python('', xtype), [])
+ self.assertEqual(XmpTag._convert_to_python('One value only', xtype), [u'One value only'])
+ self.assertEqual(XmpTag._convert_to_python('Some, text, keyword, this is a test', xtype),
+ [u'Some', u'text', u'keyword', u'this is a test'])
+
+ def test_convert_to_string_bag(self):
+ xtype = 'bag Text'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_string([], xtype), '')
+ self.assertEqual(XmpTag._convert_to_string(['One value only'], xtype), 'One value only')
+ self.assertEqual(XmpTag._convert_to_string([u'One value only'], xtype), 'One value only')
+ self.assertEqual(XmpTag._convert_to_string([u'Some', u'text', u'keyword', u'this is a test'], xtype),
+ 'Some, text, keyword, this is a test')
+ self.assertEqual(XmpTag._convert_to_string(['Some ', ' text '], xtype),
+ 'Some , text ')
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, 'invalid', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, [1, 2, 3], xtype)
+
+ def test_convert_to_python_boolean(self):
+ xtype = 'Boolean'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_python('True', xtype), True)
+ self.assertEqual(XmpTag._convert_to_python('False', xtype), False)
+ # Invalid values: not converted
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, 'invalid', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, None, xtype)
+
+ def test_convert_to_string_boolean(self):
+ xtype = 'Boolean'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_string(True, xtype), 'True')
+ self.assertEqual(XmpTag._convert_to_string(False, xtype), 'False')
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, 'invalid', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, None, xtype)
+
+ def test_convert_to_python_date(self):
+ xtype = 'Date'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_python('1999', xtype),
+ datetime.date(1999, 1, 1))
+ self.assertEqual(XmpTag._convert_to_python('1999-10', xtype),
+ datetime.date(1999, 10, 1))
+ self.assertEqual(XmpTag._convert_to_python('1999-10-13', xtype),
+ datetime.date(1999, 10, 13))
+ self.assertEqual(XmpTag._convert_to_python('1999-10-13T05:03Z', xtype) - \
+ datetime.datetime(1999, 10, 13, 5, 3, tzinfo=FixedOffset()),
+ datetime.timedelta(0))
+ self.assertEqual(XmpTag._convert_to_python('1999-10-13T05:03+06:00', xtype) - \
+ datetime.datetime(1999, 10, 13, 5, 3, tzinfo=FixedOffset('+', 6, 0)),
+ datetime.timedelta(0))
+ self.assertEqual(XmpTag._convert_to_python('1999-10-13T05:03-06:00', xtype) - \
+ datetime.datetime(1999, 10, 13, 5, 3, tzinfo=FixedOffset('-', 6, 0)),
+ datetime.timedelta(0))
+ self.assertEqual(XmpTag._convert_to_python('1999-10-13T05:03:54Z', xtype) - \
+ datetime.datetime(1999, 10, 13, 5, 3, 54, tzinfo=FixedOffset()),
+ datetime.timedelta(0))
+ self.assertEqual(XmpTag._convert_to_python('1999-10-13T05:03:54+06:00', xtype) - \
+ datetime.datetime(1999, 10, 13, 5, 3, 54, tzinfo=FixedOffset('+', 6, 0)),
+ datetime.timedelta(0))
+ self.assertEqual(XmpTag._convert_to_python('1999-10-13T05:03:54-06:00', xtype) - \
+ datetime.datetime(1999, 10, 13, 5, 3, 54, tzinfo=FixedOffset('-', 6, 0)),
+ datetime.timedelta(0))
+ self.assertEqual(XmpTag._convert_to_python('1999-10-13T05:03:54.721Z', xtype) - \
+ datetime.datetime(1999, 10, 13, 5, 3, 54, 721000, tzinfo=FixedOffset()),
+ datetime.timedelta(0))
+ self.assertEqual(XmpTag._convert_to_python('1999-10-13T05:03:54.721+06:00', xtype) - \
+ datetime.datetime(1999, 10, 13, 5, 3, 54, 721000, tzinfo=FixedOffset('+', 6, 0)),
+ datetime.timedelta(0))
+ self.assertEqual(XmpTag._convert_to_python('1999-10-13T05:03:54.721-06:00', xtype) - \
+ datetime.datetime(1999, 10, 13, 5, 3, 54, 721000, tzinfo=FixedOffset('-', 6, 0)),
+ datetime.timedelta(0))
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, 'invalid', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, '11/10/1983', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, '-1000', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, '2009-13', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, '2009-10-32', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, '2009-10-30T25:12Z', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, '2009-10-30T23:67Z', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, '2009-01-22T21', xtype)
+
+ def test_convert_to_string_date(self):
+ xtype = 'Date'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_string(datetime.date(2009, 2, 4), xtype),
+ '2009-02-04')
+ self.assertEqual(XmpTag._convert_to_string(datetime.datetime(1999, 10, 13), xtype),
+ '1999-10-13')
+ self.assertEqual(XmpTag._convert_to_string(datetime.datetime(1999, 10, 13, 5, 3, tzinfo=FixedOffset()), xtype),
+ '1999-10-13T05:03Z')
+ self.assertEqual(XmpTag._convert_to_string(datetime.datetime(1999, 10, 13, 5, 3, tzinfo=FixedOffset('+', 5, 30)), xtype),
+ '1999-10-13T05:03+05:30')
+ self.assertEqual(XmpTag._convert_to_string(datetime.datetime(1999, 10, 13, 5, 3, tzinfo=FixedOffset('-', 11, 30)), xtype),
+ '1999-10-13T05:03-11:30')
+ self.assertEqual(XmpTag._convert_to_string(datetime.datetime(1999, 10, 13, 5, 3, 27, tzinfo=FixedOffset()), xtype),
+ '1999-10-13T05:03:27Z')
+ self.assertEqual(XmpTag._convert_to_string(datetime.datetime(1999, 10, 13, 5, 3, 27, tzinfo=FixedOffset('+', 5, 30)), xtype),
+ '1999-10-13T05:03:27+05:30')
+ self.assertEqual(XmpTag._convert_to_string(datetime.datetime(1999, 10, 13, 5, 3, 27, tzinfo=FixedOffset('-', 11, 30)), xtype),
+ '1999-10-13T05:03:27-11:30')
+ self.assertEqual(XmpTag._convert_to_string(datetime.datetime(1999, 10, 13, 5, 3, 27, 124300, tzinfo=FixedOffset()), xtype),
+ '1999-10-13T05:03:27.1243Z')
+ self.assertEqual(XmpTag._convert_to_string(datetime.datetime(1999, 10, 13, 5, 3, 27, 124300, tzinfo=FixedOffset('+', 5, 30)), xtype),
+ '1999-10-13T05:03:27.1243+05:30')
+ self.assertEqual(XmpTag._convert_to_string(datetime.datetime(1999, 10, 13, 5, 3, 27, 124300, tzinfo=FixedOffset('-', 11, 30)), xtype),
+ '1999-10-13T05:03:27.1243-11:30')
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, 'invalid', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, None, xtype)
+
+ def test_convert_to_python_integer(self):
+ xtype = 'Integer'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_python('23', xtype), 23)
+ self.assertEqual(XmpTag._convert_to_python('+5628', xtype), 5628)
+ self.assertEqual(XmpTag._convert_to_python('-4', xtype), -4)
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, 'abc', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, '5,64', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, '47.0001', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, '1E3', xtype)
+
+ def test_convert_to_string_integer(self):
+ xtype = 'Integer'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_string(123, xtype), '123')
+ self.assertEqual(XmpTag._convert_to_string(-57, xtype), '-57')
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, 'invalid', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, 3.14, xtype)
+
+ def test_convert_to_python_langalt(self):
+ xtype = 'Lang Alt'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_python('lang="x-default" some text', xtype),
+ {'x-default': u'some text'})
+ self.assertEqual(XmpTag._convert_to_python('lang="x-default" some text, lang="fr-FR" du texte', xtype),
+ {'x-default': u'some text', 'fr-FR': u'du texte'})
+ self.assertEqual(XmpTag._convert_to_python('lang="x-default" some text , lang="fr-FR" du texte ', xtype),
+ {'x-default': u'some text ', 'fr-FR': u' du texte '})
+ self.assertEqual(XmpTag._convert_to_python('lang="x-default" some text, lang="fr-FR" du texte, lang="es-ES" un texto', xtype),
+ {'x-default': u'some text', 'fr-FR': u'du texte', 'es-ES': u'un texto'})
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, 'invalid', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, 'lang="malformed', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, 'xlang="x-default" some text', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, 'lang="x-default" some text, xlang="fr-FR" du texte', xtype)
+
+ def test_convert_to_string_langalt(self):
+ xtype = 'Lang Alt'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_string({'x-default': 'some text'}, xtype),
+ 'lang="x-default" some text')
+ self.assertEqual(XmpTag._convert_to_string({'x-default': u'some text'}, xtype),
+ 'lang="x-default" some text')
+ self.assertEqual(XmpTag._convert_to_string({'x-default': 'some text', 'fr-FR': 'du texte'}, xtype),
+ 'lang="x-default" some text, lang="fr-FR" du texte')
+ self.assertEqual(XmpTag._convert_to_string({'x-default': u'some text', 'fr-FR': 'du texte'}, xtype),
+ 'lang="x-default" some text, lang="fr-FR" du texte')
+ self.assertEqual(XmpTag._convert_to_string({'x-default': u'some text', 'fr-FR': 'du texte', 'es-ES': 'un texto'}, xtype),
+ 'lang="x-default" some text, lang="es-ES" un texto, lang="fr-FR" du texte')
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, 'invalid', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, {}, xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, {'x-default': 25}, xtype)
+
+ def test_convert_to_python_mimetype(self):
+ xtype = 'MIMEType'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_python('image/jpeg', xtype),
+ {'type': 'image', 'subtype': 'jpeg'})
+ self.assertEqual(XmpTag._convert_to_python('video/ogg', xtype),
+ {'type': 'video', 'subtype': 'ogg'})
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, 'invalid', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, 'image-jpeg', xtype)
+
+ def test_convert_to_string_mimetype(self):
+ xtype = 'MIMEType'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_string({'type': 'image', 'subtype': 'jpeg'}, xtype), 'image/jpeg')
+ self.assertEqual(XmpTag._convert_to_string({'type': 'video', 'subtype': 'ogg'}, xtype), 'video/ogg')
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, 'invalid', xtype)
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, {'type': 'image'}, xtype)
+
+ def test_convert_to_python_propername(self):
+ xtype = 'ProperName'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_python('Gérard', xtype), u'Gérard')
+ self.assertEqual(XmpTag._convert_to_python('Python Software Foundation', xtype), u'Python Software Foundation')
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, None, xtype)
+
+ def test_convert_to_string_propername(self):
+ xtype = 'ProperName'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_string('Gérard', xtype), 'Gérard')
+ self.assertEqual(XmpTag._convert_to_string(u'Gérard', xtype), 'Gérard')
+ self.assertEqual(XmpTag._convert_to_string(u'Python Software Foundation', xtype), 'Python Software Foundation')
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, None, xtype)
+
+ def test_convert_to_python_text(self):
+ xtype = 'Text'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_python('Some text.', xtype), u'Some text.')
+ self.assertEqual(XmpTag._convert_to_python('Some text with exotic chàräctérʐ.', xtype),
+ u'Some text with exotic chàräctérʐ.')
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_python, None, xtype)
+
+ def test_convert_to_string_text(self):
+ xtype = 'Text'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_string(u'Some text', xtype), 'Some text')
+ self.assertEqual(XmpTag._convert_to_string(u'Some text with exotic chàräctérʐ.', xtype),
+ 'Some text with exotic chàräctérʐ.')
+ self.assertEqual(XmpTag._convert_to_string('Some text with exotic chàräctérʐ.', xtype),
+ 'Some text with exotic chàräctérʐ.')
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, None, xtype)
+
+ def test_convert_to_python_uri(self):
+ xtype = 'URI'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_python('http://example.com', xtype), 'http://example.com')
+ self.assertEqual(XmpTag._convert_to_python('https://example.com', xtype), 'https://example.com')
+ self.assertEqual(XmpTag._convert_to_python('http://localhost:8000/resource', xtype),
+ 'http://localhost:8000/resource')
+ self.assertEqual(XmpTag._convert_to_python('uuid:9A3B7F52214211DAB6308A7391270C13', xtype),
+ 'uuid:9A3B7F52214211DAB6308A7391270C13')
+
+ def test_convert_to_string_uri(self):
+ xtype = 'URI'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_string('http://example.com', xtype), 'http://example.com')
+ self.assertEqual(XmpTag._convert_to_string(u'http://example.com', xtype), 'http://example.com')
+ self.assertEqual(XmpTag._convert_to_string('https://example.com', xtype), 'https://example.com')
+ self.assertEqual(XmpTag._convert_to_string('http://localhost:8000/resource', xtype),
+ 'http://localhost:8000/resource')
+ self.assertEqual(XmpTag._convert_to_string('uuid:9A3B7F52214211DAB6308A7391270C13', xtype),
+ 'uuid:9A3B7F52214211DAB6308A7391270C13')
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, None, xtype)
+
+ def test_convert_to_python_url(self):
+ xtype = 'URL'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_python('http://example.com', xtype), 'http://example.com')
+ self.assertEqual(XmpTag._convert_to_python('https://example.com', xtype), 'https://example.com')
+ self.assertEqual(XmpTag._convert_to_python('http://localhost:8000/resource', xtype),
+ 'http://localhost:8000/resource')
+
+ def test_convert_to_string_url(self):
+ xtype = 'URL'
+ # Valid values
+ self.assertEqual(XmpTag._convert_to_string('http://example.com', xtype), 'http://example.com')
+ self.assertEqual(XmpTag._convert_to_string(u'http://example.com', xtype), 'http://example.com')
+ self.assertEqual(XmpTag._convert_to_string('https://example.com', xtype), 'https://example.com')
+ self.assertEqual(XmpTag._convert_to_string('http://localhost:8000/resource', xtype),
+ 'http://localhost:8000/resource')
+ # Invalid values
+ self.failUnlessRaises(XmpValueError, XmpTag._convert_to_string, None, xtype)
+
+ # TODO: other types
+
+
+ def test_set_value_no_metadata(self):
+ tag = XmpTag('Xmp.xmp.ModifyDate', 'ModifyDate', 'Modify Date',
+ 'The date and time the resource was last modified. Note:' \
+ ' The value of this property is not necessarily the same' \
+ "as the file's system modification date because it is " \
+ 'set before the file is saved.', 'Date',
+ '2005-09-07T15:09:51-07:00')
+ old_value = tag.value
+ tag.value = datetime.datetime(2009, 4, 22, 8, 30, 27, tzinfo=FixedOffset())
+ self.failIfEqual(tag.value, old_value)
+
+ def test_set_value_with_metadata(self):
+ tag = XmpTag('Xmp.xmp.ModifyDate', 'ModifyDate', 'Modify Date',
+ 'The date and time the resource was last modified. Note:' \
+ ' The value of this property is not necessarily the same' \
+ "as the file's system modification date because it is " \
+ 'set before the file is saved.', 'Date',
+ '2005-09-07T15:09:51-07:00')
+ tag.metadata = ImageMetadataMock()
+ old_value = tag.value
+ tag.value = datetime.datetime(2009, 4, 22, 8, 30, 27, tzinfo=FixedOffset())
+ self.failIfEqual(tag.value, old_value)
+ self.assertEqual(tag.metadata.tags[tag.key], '2009-04-22T08:30:27Z')
+
+ def test_del_value_no_metadata(self):
+ tag = XmpTag('Xmp.xmp.ModifyDate', 'ModifyDate', 'Modify Date',
+ 'The date and time the resource was last modified. Note:' \
+ ' The value of this property is not necessarily the same' \
+ "as the file's system modification date because it is " \
+ 'set before the file is saved.', 'Date',
+ '2005-09-07T15:09:51-07:00')
+ del tag.value
+ self.failIf(hasattr(tag, 'value'))
+
+ def test_del_value_with_metadata(self):
+ tag = XmpTag('Xmp.xmp.ModifyDate', 'ModifyDate', 'Modify Date',
+ 'The date and time the resource was last modified. Note:' \
+ ' The value of this property is not necessarily the same' \
+ "as the file's system modification date because it is " \
+ 'set before the file is saved.', 'Date',
+ '2005-09-07T15:09:51-07:00')
+ tag.metadata = ImageMetadataMock()
+ tag.metadata._set_xmp_tag_value(tag.key, tag.to_string())
+ self.assertEqual(tag.metadata.tags, {tag.key: '2005-09-07T15:09:51-07:00'})
+ del tag.value
+ self.failIf(hasattr(tag, 'value'))
+ self.failIf(tag.metadata.tags.has_key(tag.key))