aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOlivier Tilloy <olivier@tilloy.net>2010-12-26 19:30:33 +0100
committerOlivier Tilloy <olivier@tilloy.net>2010-12-26 19:30:33 +0100
commit66afe34c75790fba2b8395ce781bd9d16868e0e3 (patch)
treed2109d2a2bd0a627ce06f27e99f7b5d04558073b
parentf178386b9ec255d7456977bb23b205802a0452e7 (diff)
downloadpyexiv2-66afe34c75790fba2b8395ce781bd9d16868e0e3.tar.gz
Handle fractions in a transparent manner,
using the convenience functions defined in module pyexiv2.utils.
-rw-r--r--src/pyexiv2/exif.py30
-rw-r--r--src/pyexiv2/utils.py9
-rw-r--r--src/pyexiv2/xmp.py10
-rw-r--r--test/ReadMetadataTestCase.py61
-rw-r--r--test/exif.py21
-rw-r--r--test/metadata.py4
-rw-r--r--test/pickling.py8
-rw-r--r--test/xmp.py13
8 files changed, 74 insertions, 82 deletions
diff --git a/src/pyexiv2/exif.py b/src/pyexiv2/exif.py
index 4f3fe03..8c58ede 100644
--- a/src/pyexiv2/exif.py
+++ b/src/pyexiv2/exif.py
@@ -30,7 +30,7 @@ EXIF specific code.
import libexiv2python
-from pyexiv2.utils import Rational, Fraction, \
+from pyexiv2.utils import is_fraction, make_fraction, \
NotifyingList, ListenerInterface, \
undefined_to_string, string_to_undefined
@@ -72,7 +72,8 @@ class ExifTag(ListenerInterface):
- Comment: string
- Long, SLong: [list of] long
- Short, SShort: [list of] int
- - Rational, SRational: [list of] :class:`pyexiv2.utils.Rational`
+ - Rational, SRational: [list of] :class:`fractions.Fraction` if available
+ (Python ≥ 2.6) or :class:`pyexiv2.utils.Rational`
- Undefined: string
"""
@@ -316,7 +317,7 @@ class ExifTag(ListenerInterface):
elif self.type in ('Rational', 'SRational'):
try:
- r = Rational.from_string(value)
+ r = make_fraction(value)
except (ValueError, ZeroDivisionError):
raise ExifValueError(value, self.type)
else:
@@ -424,17 +425,26 @@ class ExifTag(ListenerInterface):
raise ExifValueError(value, self.type)
elif self.type == 'Rational':
- if (isinstance(value, Rational) or \
- (Fraction is not None and isinstance(value, Fraction))) \
- and value.numerator >= 0:
- return str(value)
+ if is_fraction(value) and value.numerator >= 0:
+ r = str(value)
+ if r == '0':
+ # fractions.Fraction.__str__ returns '0' for a null
+ # numerator, but libexiv2 always expects a string in the
+ # form 'numerator/denominator'.
+ r = '0/1'
+ return r
else:
raise ExifValueError(value, self.type)
elif self.type == 'SRational':
- if isinstance(value, Rational) or \
- (Fraction is not None and isinstance(value, Fraction)):
- return str(value)
+ if is_fraction(value):
+ r = str(value)
+ if r == '0':
+ # fractions.Fraction.__str__ returns '0' for a null
+ # numerator, but libexiv2 always expects a string in the
+ # form 'numerator/denominator'.
+ r = '0/1'
+ return r
else:
raise ExifValueError(value, self.type)
diff --git a/src/pyexiv2/utils.py b/src/pyexiv2/utils.py
index 3c5aa95..473acdb 100644
--- a/src/pyexiv2/utils.py
+++ b/src/pyexiv2/utils.py
@@ -31,11 +31,10 @@ Utilitary classes and functions.
import datetime
import re
-# Support for fractions.Fraction as a replacement for the Rational class is not
-# implemented yet as we have to support versions of Python < 2.6
-# (see https://launchpad.net/bugs/514415).
-# However, it doesn’t hurt to accept Fraction objects as values when the module
-# is available (see https://launchpad.net/bugs/683232).
+# pyexiv2 uses fractions.Fraction when available (Python ≥ 2.6), or falls back
+# on the custom Rational class. This should be transparent to the application
+# developer as both classes have a similar API.
+# This module contains convenience functions to ease manipulation of fractions.
try:
from fractions import Fraction
except ImportError:
diff --git a/src/pyexiv2/xmp.py b/src/pyexiv2/xmp.py
index ab2edeb..469af98 100644
--- a/src/pyexiv2/xmp.py
+++ b/src/pyexiv2/xmp.py
@@ -30,7 +30,7 @@ XMP specific code.
import libexiv2python
-from pyexiv2.utils import FixedOffset, Rational, Fraction, GPSCoordinate
+from pyexiv2.utils import FixedOffset, is_fraction, make_fraction, GPSCoordinate
import datetime
import re
@@ -74,7 +74,8 @@ class XmpTag(object):
- Integer: int
- Locale: *[not implemented yet]*
- MIMEType: 2-tuple of strings
- - Rational: :class:`pyexiv2.utils.Rational`
+ - Rational: :class:`fractions.Fraction` if available (Python ≥ 2.6) or
+ :class:`pyexiv2.utils.Rational`
- Real: *[not implemented yet]*
- AgentName, ProperName, Text: unicode string
- Thumbnail: *[not implemented yet]*
@@ -338,7 +339,7 @@ class XmpTag(object):
elif type == 'Rational':
try:
- return Rational.from_string(value)
+ return make_fraction(value)
except (ValueError, ZeroDivisionError):
raise XmpValueError(value, type)
@@ -435,8 +436,7 @@ class XmpTag(object):
raise XmpValueError(value, type)
elif type == 'Rational':
- if isinstance(value, Rational) or \
- (Fraction is not None and isinstance(value, Fraction)):
+ if is_fraction(value):
return str(value)
else:
raise XmpValueError(value, type)
diff --git a/test/ReadMetadataTestCase.py b/test/ReadMetadataTestCase.py
index 40f0843..3bf8e09 100644
--- a/test/ReadMetadataTestCase.py
+++ b/test/ReadMetadataTestCase.py
@@ -25,12 +25,19 @@
#
# ******************************************************************************
+import pyexiv2
+from pyexiv2.utils import is_fraction, make_fraction
+
import unittest
-import testutils
import os.path
-import pyexiv2
import datetime
+import testutils
+
+
+FRACTION = 'fraction'
+
+
class ReadMetadataTestCase(unittest.TestCase):
"""
@@ -38,7 +45,10 @@ class ReadMetadataTestCase(unittest.TestCase):
"""
def check_type_and_value(self, tag, etype, evalue):
- self.assert_(isinstance(tag.value, etype))
+ if etype == FRACTION:
+ self.assert_(is_fraction(tag.value))
+ else:
+ self.assert_(isinstance(tag.value, etype))
self.assertEqual(tag.value, evalue)
def check_type_and_values(self, tag, etype, evalues):
@@ -68,8 +78,8 @@ class ReadMetadataTestCase(unittest.TestCase):
# Exhaustive tests on the values of EXIF metadata
exifTags = [('Exif.Image.ImageDescription', str, 'Well it is a smiley that happens to be green'),
- ('Exif.Image.XResolution', pyexiv2.Rational, pyexiv2.Rational(72, 1)),
- ('Exif.Image.YResolution', pyexiv2.Rational, pyexiv2.Rational(72, 1)),
+ ('Exif.Image.XResolution', FRACTION, make_fraction(72, 1)),
+ ('Exif.Image.YResolution', FRACTION, make_fraction(72, 1)),
('Exif.Image.ResolutionUnit', int, 2),
('Exif.Image.Software', str, 'ImageReady'),
('Exif.Image.DateTime', datetime.datetime, datetime.datetime(2004, 7, 13, 21, 23, 44)),
@@ -115,36 +125,22 @@ class ReadMetadataTestCase(unittest.TestCase):
('Xmp.dc.source', unicode, u'FreeFoto.com'),
('Xmp.dc.subject', list, [u'Communications']),
('Xmp.dc.title', dict, {u'x-default': u'Communications'}),
- ('Xmp.exif.ApertureValue',
- pyexiv2.utils.Rational,
- pyexiv2.utils.Rational(8, 1)),
- ('Xmp.exif.BrightnessValue',
- pyexiv2.utils.Rational,
- pyexiv2.utils.Rational(333, 1280)),
+ ('Xmp.exif.ApertureValue', FRACTION, make_fraction(8, 1)),
+ ('Xmp.exif.BrightnessValue', FRACTION, make_fraction(333, 1280)),
('Xmp.exif.ColorSpace', int, 1),
('Xmp.exif.DateTimeOriginal',
datetime.datetime,
datetime.datetime(2002, 7, 13, 15, 58, 28, tzinfo=pyexiv2.utils.FixedOffset())),
('Xmp.exif.ExifVersion', unicode, u'0200'),
- ('Xmp.exif.ExposureBiasValue',
- pyexiv2.utils.Rational,
- pyexiv2.utils.Rational(-13, 20)),
+ ('Xmp.exif.ExposureBiasValue', FRACTION, make_fraction(-13, 20)),
('Xmp.exif.ExposureProgram', int, 4),
- ('Xmp.exif.FNumber',
- pyexiv2.utils.Rational,
- pyexiv2.utils.Rational(3, 5)),
+ ('Xmp.exif.FNumber', FRACTION, make_fraction(3, 5)),
('Xmp.exif.FileSource', int, 0),
('Xmp.exif.FlashpixVersion', unicode, u'0100'),
- ('Xmp.exif.FocalLength',
- pyexiv2.utils.Rational,
- pyexiv2.utils.Rational(0, 1)),
+ ('Xmp.exif.FocalLength', FRACTION, make_fraction(0, 1)),
('Xmp.exif.FocalPlaneResolutionUnit', int, 2),
- ('Xmp.exif.FocalPlaneXResolution',
- pyexiv2.utils.Rational,
- pyexiv2.utils.Rational(3085, 256)),
- ('Xmp.exif.FocalPlaneYResolution',
- pyexiv2.utils.Rational,
- pyexiv2.utils.Rational(3085, 256)),
+ ('Xmp.exif.FocalPlaneXResolution', FRACTION, make_fraction(3085, 256)),
+ ('Xmp.exif.FocalPlaneYResolution', FRACTION, make_fraction(3085, 256)),
('Xmp.exif.GPSLatitude',
pyexiv2.utils.GPSCoordinate,
pyexiv2.utils.GPSCoordinate.from_string('54,59.380000N')),
@@ -162,9 +158,7 @@ class ReadMetadataTestCase(unittest.TestCase):
('Xmp.exif.PixelYDimension', int, 1600),
('Xmp.exif.SceneType', int, 0),
('Xmp.exif.SensingMethod', int, 2),
- ('Xmp.exif.ShutterSpeedValue',
- pyexiv2.utils.Rational,
- pyexiv2.utils.Rational(30827, 3245)),
+ ('Xmp.exif.ShutterSpeedValue', FRACTION, make_fraction(30827, 3245)),
('Xmp.pdf.Keywords', unicode, u'Communications'),
('Xmp.photoshop.AuthorsPosition', unicode, u'Photographer'),
('Xmp.photoshop.CaptionWriter', unicode, u'Ian Britton'),
@@ -191,13 +185,9 @@ class ReadMetadataTestCase(unittest.TestCase):
('Xmp.tiff.Orientation', int, 1),
('Xmp.tiff.ResolutionUnit', int, 2),
('Xmp.tiff.Software', unicode, u'Adobe Photoshop 7.0'),
- ('Xmp.tiff.XResolution',
- pyexiv2.utils.Rational,
- pyexiv2.utils.Rational(300, 1)),
+ ('Xmp.tiff.XResolution', FRACTION, make_fraction(300, 1)),
('Xmp.tiff.YCbCrPositioning', int, 2),
- ('Xmp.tiff.YResolution',
- pyexiv2.utils.Rational,
- pyexiv2.utils.Rational(300, 1)),
+ ('Xmp.tiff.YResolution', FRACTION, make_fraction(300, 1)),
('Xmp.xmp.CreateDate',
datetime.datetime,
datetime.datetime(2002, 7, 13, 15, 58, 28, tzinfo=pyexiv2.utils.FixedOffset())),
@@ -215,3 +205,4 @@ class ReadMetadataTestCase(unittest.TestCase):
self.assertEqual(image.xmp_keys, [tag[0] for tag in xmpTags])
for key, ktype, value in xmpTags:
self.check_type_and_value(image[key], ktype, value)
+
diff --git a/test/exif.py b/test/exif.py
index 1f929b7..38e19b7 100644
--- a/test/exif.py
+++ b/test/exif.py
@@ -27,7 +27,7 @@
import unittest
from pyexiv2.exif import ExifTag, ExifValueError
-from pyexiv2.utils import Rational, Fraction
+from pyexiv2.utils import make_fraction
import datetime
@@ -260,7 +260,7 @@ class TestExifTag(unittest.TestCase):
# Valid values
tag = ExifTag('Exif.Image.XResolution')
self.assertEqual(tag.type, 'Rational')
- self.assertEqual(tag._convert_to_python('5/3'), Rational(5, 3))
+ self.assertEqual(tag._convert_to_python('5/3'), make_fraction(5, 3))
# Invalid values
self.failUnlessRaises(ExifValueError, tag._convert_to_python, 'invalid')
@@ -272,21 +272,19 @@ class TestExifTag(unittest.TestCase):
# Valid values
tag = ExifTag('Exif.Image.XResolution')
self.assertEqual(tag.type, 'Rational')
- self.assertEqual(tag._convert_to_string(Rational(5, 3)), '5/3')
- if Fraction is not None:
- self.assertEqual(tag._convert_to_string(Fraction('1.6')), '8/5')
+ self.assertEqual(tag._convert_to_string(make_fraction(5, 3)), '5/3')
# Invalid values
self.failUnlessRaises(ExifValueError, tag._convert_to_string, 'invalid')
self.failUnlessRaises(ExifValueError,
- tag._convert_to_string, Rational(-5, 3))
+ tag._convert_to_string, make_fraction(-5, 3))
def test_convert_to_python_srational(self):
# Valid values
tag = ExifTag('Exif.Image.BaselineExposure')
self.assertEqual(tag.type, 'SRational')
- self.assertEqual(tag._convert_to_python('5/3'), Rational(5, 3))
- self.assertEqual(tag._convert_to_python('-5/3'), Rational(-5, 3))
+ self.assertEqual(tag._convert_to_python('5/3'), make_fraction(5, 3))
+ self.assertEqual(tag._convert_to_python('-5/3'), make_fraction(-5, 3))
# Invalid values
self.failUnlessRaises(ExifValueError, tag._convert_to_python, 'invalid')
@@ -297,11 +295,8 @@ class TestExifTag(unittest.TestCase):
# Valid values
tag = ExifTag('Exif.Image.BaselineExposure')
self.assertEqual(tag.type, 'SRational')
- self.assertEqual(tag._convert_to_string(Rational(5, 3)), '5/3')
- self.assertEqual(tag._convert_to_string(Rational(-5, 3)), '-5/3')
- if Fraction is not None:
- self.assertEqual(tag._convert_to_string(Fraction('1.6')), '8/5')
- self.assertEqual(tag._convert_to_string(Fraction('-1.6')), '-8/5')
+ self.assertEqual(tag._convert_to_string(make_fraction(5, 3)), '5/3')
+ self.assertEqual(tag._convert_to_string(make_fraction(-5, 3)), '-5/3')
# Invalid values
self.failUnlessRaises(ExifValueError, tag._convert_to_string, 'invalid')
diff --git a/test/metadata.py b/test/metadata.py
index bc4d1ff..177ec9d 100644
--- a/test/metadata.py
+++ b/test/metadata.py
@@ -28,7 +28,7 @@ from pyexiv2.metadata import ImageMetadata
from pyexiv2.exif import ExifTag
from pyexiv2.iptc import IptcTag
from pyexiv2.xmp import XmpTag
-from pyexiv2.utils import FixedOffset, Rational
+from pyexiv2.utils import FixedOffset, make_fraction
import datetime
import os
@@ -501,7 +501,7 @@ class TestImageMetadata(unittest.TestCase):
self.metadata.read()
# Set new tags
key = 'Exif.Photo.ExposureBiasValue'
- tag = ExifTag(key, Rational(0, 3))
+ tag = ExifTag(key, make_fraction(0, 3))
self.metadata[key] = tag
self.failUnless(key in self.metadata._tags['exif'])
self.failUnlessEqual(self.metadata._tags['exif'][key], tag)
diff --git a/test/pickling.py b/test/pickling.py
index d98c146..8d0101b 100644
--- a/test/pickling.py
+++ b/test/pickling.py
@@ -27,7 +27,7 @@
from pyexiv2.exif import ExifTag
from pyexiv2.iptc import IptcTag
from pyexiv2.xmp import XmpTag
-from pyexiv2.utils import Rational, FixedOffset
+from pyexiv2.utils import make_fraction, FixedOffset
import unittest
import pickle
@@ -49,8 +49,8 @@ class TestPicklingTags(unittest.TestCase):
tags.append(ExifTag('Exif.Image.TimeZoneOffset', 7))
tags.append(ExifTag('Exif.Image.ImageWidth', 7492))
tags.append(ExifTag('Exif.OlympusCs.ManometerReading', 29))
- tags.append(ExifTag('Exif.Image.XResolution', Rational(7, 3)))
- tags.append(ExifTag('Exif.Image.BaselineExposure', Rational(-7, 3)))
+ tags.append(ExifTag('Exif.Image.XResolution', make_fraction(7, 3)))
+ tags.append(ExifTag('Exif.Image.BaselineExposure', make_fraction(-7, 3)))
tags.append(ExifTag('Exif.Photo.ExifVersion', '0100'))
for tag in tags:
s = pickle.dumps(tag)
@@ -102,7 +102,7 @@ class TestPicklingTags(unittest.TestCase):
tags.append(XmpTag('Xmp.dc.source', 'bleh'))
tags.append(XmpTag('Xmp.xmpMM.DocumentID', 'http://example.com'))
tags.append(XmpTag('Xmp.xmp.BaseURL', 'http://example.com'))
- tags.append(XmpTag('Xmp.xmpDM.videoPixelAspectRatio', Rational(5, 3)))
+ tags.append(XmpTag('Xmp.xmpDM.videoPixelAspectRatio', make_fraction(5, 3)))
for tag in tags:
s = pickle.dumps(tag)
t = pickle.loads(s)
diff --git a/test/xmp.py b/test/xmp.py
index 3379851..f3fa4c3 100644
--- a/test/xmp.py
+++ b/test/xmp.py
@@ -28,7 +28,7 @@ import unittest
from pyexiv2.xmp import XmpTag, XmpValueError, register_namespace, \
unregister_namespace, unregister_namespaces
-from pyexiv2.utils import FixedOffset, Rational, Fraction
+from pyexiv2.utils import FixedOffset, make_fraction
from pyexiv2.metadata import ImageMetadata
import datetime
@@ -288,8 +288,8 @@ class TestXmpTag(unittest.TestCase):
# Valid values
tag = XmpTag('Xmp.xmpDM.videoPixelAspectRatio')
self.assertEqual(tag.type, 'Rational')
- self.assertEqual(tag._convert_to_python('5/3', 'Rational'), Rational(5, 3))
- self.assertEqual(tag._convert_to_python('-5/3', 'Rational'), Rational(-5, 3))
+ self.assertEqual(tag._convert_to_python('5/3', 'Rational'), make_fraction(5, 3))
+ self.assertEqual(tag._convert_to_python('-5/3', 'Rational'), make_fraction(-5, 3))
# Invalid values
self.failUnlessRaises(XmpValueError, tag._convert_to_python, 'invalid', 'Rational')
@@ -300,11 +300,8 @@ class TestXmpTag(unittest.TestCase):
# Valid values
tag = XmpTag('Xmp.xmpDM.videoPixelAspectRatio')
self.assertEqual(tag.type, 'Rational')
- self.assertEqual(tag._convert_to_string(Rational(5, 3), 'Rational'), '5/3')
- self.assertEqual(tag._convert_to_string(Rational(-5, 3), 'Rational'), '-5/3')
- if Fraction is not None:
- self.assertEqual(tag._convert_to_string(Fraction('1.6'), 'Rational'), '8/5')
- self.assertEqual(tag._convert_to_string(Fraction('-1.6'), 'Rational'), '-8/5')
+ self.assertEqual(tag._convert_to_string(make_fraction(5, 3), 'Rational'), '5/3')
+ self.assertEqual(tag._convert_to_string(make_fraction(-5, 3), 'Rational'), '-5/3')
# Invalid values
self.failUnlessRaises(XmpValueError, tag._convert_to_string, 'invalid', 'Rational')