diff options
author | Olivier Tilloy <olivier@tilloy.net> | 2011-05-24 20:39:40 +0200 |
---|---|---|
committer | Olivier Tilloy <olivier@tilloy.net> | 2011-05-24 20:39:40 +0200 |
commit | 606382449ece9560f9fc7b7ed1dc64a028d68cba (patch) | |
tree | 3214bf4e1f0b379fe4ade930a6c43421c80e4e4a | |
parent | fb0e651326f38481cf04908b4a3547bf36b5aefd (diff) | |
parent | de244517501085dac5a20a47997fdbf77628fe99 (diff) | |
download | pyexiv2-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.rst | 1 | ||||
-rw-r--r-- | src/pyexiv2/utils.py | 53 | ||||
-rw-r--r-- | test/rational.py | 12 | ||||
-rw-r--r-- | test/utils.py | 2 |
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') |