aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOlivier Tilloy <olivier@tilloy.net>2011-05-24 20:39:40 +0200
committerOlivier Tilloy <olivier@tilloy.net>2011-05-24 20:39:40 +0200
commit606382449ece9560f9fc7b7ed1dc64a028d68cba (patch)
tree3214bf4e1f0b379fe4ade930a6c43421c80e4e4a
parentfb0e651326f38481cf04908b4a3547bf36b5aefd (diff)
parentde244517501085dac5a20a47997fdbf77628fe99 (diff)
downloadpyexiv2-606382449ece9560f9fc7b7ed1dc64a028d68cba.tar.gz
Do not fail to parse rationals stored as '0/0'.
In the wonderful world of EXIF metadata, this means a null fraction, i.e. 0/1.
-rw-r--r--doc/api.rst1
-rw-r--r--src/pyexiv2/utils.py53
-rw-r--r--test/rational.py12
-rw-r--r--test/utils.py2
4 files changed, 58 insertions, 10 deletions
diff --git a/doc/api.rst b/doc/api.rst
index c48769d..a91122b 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -64,6 +64,7 @@ pyexiv2.utils
.. module:: pyexiv2.utils
.. autofunction:: undefined_to_string
.. autofunction:: string_to_undefined
+.. autofunction:: make_fraction
.. autoclass:: Rational
:members: numerator, denominator, from_string, to_float, __eq__
diff --git a/src/pyexiv2/utils.py b/src/pyexiv2/utils.py
index 4fcebf9..22a05f7 100644
--- a/src/pyexiv2/utils.py
+++ b/src/pyexiv2/utils.py
@@ -170,6 +170,9 @@ class Rational(object):
A class representing a rational number.
Its numerator and denominator are read-only properties.
+
+ Do not use this class directly to instantiate a rational number.
+ Instead, use :func:`make_fraction`.
"""
_format_re = re.compile(r'(?P<numerator>-?\d+)/(?P<denominator>\d+)')
@@ -200,6 +203,27 @@ class Rational(object):
return self._denominator
@staticmethod
+ def match_string(string):
+ """
+ Match a string against the expected format for a :class:`Rational`
+ (``[-]numerator/denominator``) and return the numerator and denominator
+ as a tuple.
+
+ :param string: a string representation of a rational number
+ :type string: string
+
+ :return: a tuple (numerator, denominator)
+ :rtype: tuple of long
+
+ :raise ValueError: if the format of the string is invalid
+ """
+ match = Rational._format_re.match(string)
+ if match is None:
+ raise ValueError('Invalid format for a rational: %s' % string)
+ gd = match.groupdict()
+ return (long(gd['numerator']), long(gd['denominator']))
+
+ @staticmethod
def from_string(string):
"""
Instantiate a :class:`Rational` from a string formatted as
@@ -213,11 +237,8 @@ class Rational(object):
:raise ValueError: if the format of the string is invalid
"""
- match = Rational._format_re.match(string)
- if match is None:
- raise ValueError('Invalid format for a rational: %s' % string)
- gd = match.groupdict()
- return Rational(long(gd['numerator']), long(gd['denominator']))
+ numerator, denominator = Rational.match_string(string)
+ return Rational(numerator, denominator)
def to_float(self):
"""
@@ -267,14 +288,26 @@ def make_fraction(*args):
The type of the returned object depends on the availability of the
fractions module in the standard library (Python ≥ 2.6).
+
+ :raise TypeError: if the arguments do not match the expected format for a
+ fraction
"""
+ if len(args) == 1:
+ numerator, denominator = Rational.match_string(args[0])
+ elif len(args) == 2:
+ numerator = args[0]
+ denominator = args[1]
+ else:
+ raise TypeError('Invalid format for a fraction: %s' % str(args))
+ if denominator == 0 and numerator == 0:
+ # Null rationals are often stored as '0/0'.
+ # We want to be fault-tolerant in this specific case
+ # (see https://bugs.launchpad.net/pyexiv2/+bug/786253).
+ denominator = 1
if Fraction is not None:
- return Fraction(*args)
+ return Fraction(numerator, denominator)
else:
- if len(args) == 1:
- return Rational.from_string(*args)
- else:
- return Rational(*args)
+ return Rational(numerator, denominator)
def fraction_to_string(fraction):
diff --git a/test/rational.py b/test/rational.py
index 1ee5670..bff8e5f 100644
--- a/test/rational.py
+++ b/test/rational.py
@@ -52,6 +52,16 @@ class TestRational(unittest.TestCase):
else:
self.fail('Denominator is not read-only.')
+ def test_match_string(self):
+ self.assertEqual(Rational.match_string('4/3'), (4, 3))
+ self.assertEqual(Rational.match_string('-4/3'), (-4, 3))
+ self.assertEqual(Rational.match_string('0/3'), (0, 3))
+ self.assertEqual(Rational.match_string('0/0'), (0, 0))
+ self.assertRaises(ValueError, Rational.match_string, '+3/5')
+ self.assertRaises(ValueError, Rational.match_string, '3 / 5')
+ self.assertRaises(ValueError, Rational.match_string, '3/-5')
+ self.assertRaises(ValueError, Rational.match_string, 'invalid')
+
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))
@@ -59,6 +69,8 @@ class TestRational(unittest.TestCase):
self.assertRaises(ValueError, Rational.from_string, '3 / 5')
self.assertRaises(ValueError, Rational.from_string, '3/-5')
self.assertRaises(ValueError, Rational.from_string, 'invalid')
+ self.assertRaises(ZeroDivisionError, Rational.from_string, '1/0')
+ self.assertRaises(ZeroDivisionError, Rational.from_string, '0/0')
def test_to_string(self):
self.assertEqual(str(Rational(3, 5)), '3/5')
diff --git a/test/utils.py b/test/utils.py
index 11b1766..0b55254 100644
--- a/test/utils.py
+++ b/test/utils.py
@@ -78,11 +78,13 @@ class TestFractions(unittest.TestCase):
self.assertEqual(make_fraction(-3, 5), Fraction(-3, 5))
self.assertEqual(make_fraction('3/2'), Fraction(3, 2))
self.assertEqual(make_fraction('-3/4'), Fraction(-3, 4))
+ self.assertEqual(make_fraction('0/0'), Fraction(0, 1))
else:
self.assertEqual(make_fraction(3, 5), Rational(3, 5))
self.assertEqual(make_fraction(-3, 5), Rational(-3, 5))
self.assertEqual(make_fraction('3/2'), Rational(3, 2))
self.assertEqual(make_fraction('-3/4'), Rational(-3, 4))
+ self.assertEqual(make_fraction('0/0'), Rational(0, 1))
self.assertRaises(ZeroDivisionError, make_fraction, 3, 0)
self.assertRaises(ZeroDivisionError, make_fraction, '3/0')