From 317c1c6271449169f6de5a4b6d31f4f3ae39df54 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 20 May 2024 18:28:26 +0300 Subject: [PATCH 1/2] gh-119189: Add more tests for mixed Fraction arithmetic --- Lib/test/test_fractions.py | 246 +++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index b45bd098a36684..ac8267d42aac5a 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -1,5 +1,6 @@ """Tests for Lib/fractions.py.""" +import cmath from decimal import Decimal from test.support import requires_IEEE_754 import math @@ -91,6 +92,170 @@ class DummyFraction(fractions.Fraction): def _components(r): return (r.numerator, r.denominator) +def is_eq(a, b): + return type(a) == type(b) and (a == b or math.isclose(a, b)) + +class One: + def __mul__(self, other): + if isinstance(other, F): + return NotImplemented + return other + def __rmul__(self, other): + return other + def __truediv__(self, other): + if isinstance(other, F): + return NotImplemented + return 1 / other + def __rtruediv__(self, other): + return other + def __mod__(self, other): + if isinstance(other, F): + return NotImplemented + return 1 % other + def __rmod__(self, other): + return other % 1 + def __pow__(self, other): + if isinstance(other, F): + return NotImplemented + return self + def __rpow__(self, other): + return other +One = One() + +class Rat: + def __init__(self, n, d): + self.numerator = n + self.denominator = d + def __mul__(self, other): + if isinstance(other, F): + return NotImplemented + return self.__class__(self.numerator * other.numerator, + self.denominator * other.denominator) + def __rmul__(self, other): + return self.__class__(other.numerator * self.numerator, + other.denominator * self.denominator) + def __truediv__(self, other): + if isinstance(other, F): + return NotImplemented + return self.__class__(self.numerator * other.denominator, + self.denominator * other.numerator) + def __rtruediv__(self, other): + return self.__class__(other.numerator * self.denominator, + other.denominator * self.numerator) + def __mod__(self, other): + if isinstance(other, F): + return NotImplemented + d = self.denominator * other.numerator + return self.__class__(self.numerator * other.denominator % d, d) + def __rmod__(self, other): + d = other.denominator * self.numerator + return self.__class__(other.numerator * self.denominator % d, d) + + return self.__class__(other.numerator / self.numerator, + other.denominator / self.denominator) + def __pow__(self, other): + if isinstance(other, F): + return NotImplemented + return self.__class__(self.numerator ** other, + self.denominator ** other) + def __float__(self): + return self.numerator / self.denominator + def __eq__(self, other): + if self.__class__ != other.__class__: + return NotImplemented + return (is_eq(self.numerator, other.numerator) and + is_eq(self.denominator, other.denominator)) + def __repr__(self): + return f'{self.__class__.__name__}({self.numerator!r}, {self.denominator!r})' +numbers.Rational.register(Rat) + +class Root: + def __init__(self, v, n=F(2)): + self.base = v + self.degree = n + def __mul__(self, other): + if isinstance(other, F): + return NotImplemented + return self.__class__(self.base * other**self.degree, self.degree) + def __rmul__(self, other): + return self.__class__(other**self.degree * self.base, self.degree) + def __truediv__(self, other): + if isinstance(other, F): + return NotImplemented + return self.__class__(self.base / other**self.degree, self.degree) + def __rtruediv__(self, other): + return self.__class__(other**self.degree / self.base, self.degree) + def __pow__(self, other): + if isinstance(other, F): + return NotImplemented + return self.__class__(self.base, self.degree / other) + def __float__(self): + return float(self.base) ** (1 / float(self.degree)) + def __eq__(self, other): + if self.__class__ != other.__class__: + return NotImplemented + return is_eq(self.base, other.base) and is_eq(self.degree, other.degree) + def __repr__(self): + return f'{self.__class__.__name__}({self.base!r}, {self.degree!r})' +numbers.Real.register(Root) + +class Polar: + def __init__(self, r, phi): + self.r = r + self.phi = phi + def __mul__(self, other): + if isinstance(other, F): + return NotImplemented + return self.__class__(self.r * other, self.phi) + def __rmul__(self, other): + return self.__class__(other * self.r, self.phi) + def __truediv__(self, other): + if isinstance(other, F): + return NotImplemented + return self.__class__(self.r / other, self.phi) + def __rtruediv__(self, other): + return self.__class__(other / self.r, -self.phi) + def __pow__(self, other): + if isinstance(other, F): + return NotImplemented + return self.__class__(self.r ** other, self.phi * other) + def __eq__(self, other): + if self.__class__ != other.__class__: + return NotImplemented + return is_eq(self.r, other.r) and is_eq(self.phi, other.phi) + def __repr__(self): + return f'{self.__class__.__name__}({self.r!r}, {self.phi!r})' +numbers.Complex.register(Polar) + +class Rect: + def __init__(self, x, y): + self.x = x + self.y = y + def __mul__(self, other): + if isinstance(other, F): + return NotImplemented + return self.__class__(self.x * other, self.y * other) + def __rmul__(self, other): + return self.__class__(other * self.x, other * self.y) + def __truediv__(self, other): + if isinstance(other, F): + return NotImplemented + return self.__class__(self.x / other, self.y / other) + def __rtruediv__(self, other): + r = self.x * self.x + self.y * self.y + return self.__class__(other * (self.x / r), other * (self.y / r)) + def __rpow__(self, other): + return Polar(other ** self.x, math.log(other) * self.y) + def __complex__(self): + return complex(self.x, self.y) + def __eq__(self, other): + if self.__class__ != other.__class__: + return NotImplemented + return is_eq(self.x, other.x) and is_eq(self.y, other.y) + def __repr__(self): + return f'{self.__class__.__name__}({self.x!r}, {self.y!r})' +numbers.Complex.register(Rect) + class FractionTest(unittest.TestCase): @@ -593,6 +758,7 @@ def testMixedArithmetic(self): self.assertTypedEquals(0.9, 1.0 - F(1, 10)) self.assertTypedEquals(0.9 + 0j, (1.0 + 0j) - F(1, 10)) + def testMixedMultiplication(self): self.assertTypedEquals(F(1, 10), F(1, 10) * 1) self.assertTypedEquals(0.1, F(1, 10) * 1.0) self.assertTypedEquals(0.1 + 0j, F(1, 10) * (1.0 + 0j)) @@ -600,6 +766,24 @@ def testMixedArithmetic(self): self.assertTypedEquals(0.1, 1.0 * F(1, 10)) self.assertTypedEquals(0.1 + 0j, (1.0 + 0j) * F(1, 10)) + self.assertTypedEquals(F(3, 2) * DummyFraction(5, 3), F(5, 2)) + self.assertTypedEquals(DummyFraction(5, 3) * F(3, 2), F(5, 2)) + self.assertTypedEquals(F(3, 2) * Rat(5, 3), Rat(15, 6)) + self.assertTypedEquals(Rat(5, 3) * F(3, 2), F(5, 2)) + + self.assertTypedEquals(F(3, 2) * Root(4), Root(F(9, 1))) + self.assertTypedEquals(Root(4) * F(3, 2), 3.0) + + self.assertTypedEquals(F(3, 2) * Polar(4, 2), Polar(F(6, 1), 2)) + self.assertTypedEquals(F(3, 2) * Polar(4.0, 2), Polar(6.0, 2)) + self.assertTypedEquals(F(3, 2) * Rect(4, 3), Rect(F(6, 1), F(9, 2))) + self.assertRaises(TypeError, operator.mul, Polar(4, 2), F(3, 2)) + self.assertTypedEquals(Rect(4, 3) * F(3, 2), 6.0 + 4.5j) + + self.assertTypedEquals(F(3, 2) * One, F(3, 2)) + self.assertRaises(TypeError, operator.mul, One, F(3, 2)) + + def testMixedDivision(self): self.assertTypedEquals(F(1, 10), F(1, 10) / 1) self.assertTypedEquals(0.1, F(1, 10) / 1.0) self.assertTypedEquals(0.1 + 0j, F(1, 10) / (1.0 + 0j)) @@ -607,6 +791,24 @@ def testMixedArithmetic(self): self.assertTypedEquals(10.0, 1.0 / F(1, 10)) self.assertTypedEquals(10.0 + 0j, (1.0 + 0j) / F(1, 10)) + self.assertTypedEquals(F(3, 2) / DummyFraction(3, 5), F(5, 2)) + self.assertTypedEquals(DummyFraction(5, 3) / F(2, 3), F(5, 2)) + self.assertTypedEquals(F(3, 2) / Rat(3, 5), Rat(15, 6)) + self.assertTypedEquals(Rat(5, 3) / F(2, 3), F(5, 2)) + + self.assertTypedEquals(F(2, 3) / Root(4), Root(F(1, 9))) + self.assertTypedEquals(Root(4) / F(2, 3), 3.0) + + self.assertTypedEquals(F(3, 2) / Polar(4, 2), Polar(F(3, 8), -2)) + self.assertTypedEquals(F(3, 2) / Polar(4.0, 2), Polar(0.375, -2)) + self.assertTypedEquals(F(3, 2) / Rect(4, 3), Rect(0.24, 0.18)) + self.assertRaises(TypeError, operator.truediv, Polar(4, 2), F(2, 3)) + self.assertTypedEquals(Rect(4, 3) / F(2, 3), 6.0 + 4.5j) + + self.assertTypedEquals(F(3, 2) / One, F(3, 2)) + self.assertRaises(TypeError, operator.truediv, One, F(2, 3)) + + def testMixedIntegerDivision(self): self.assertTypedEquals(0, F(1, 10) // 1) self.assertTypedEquals(0.0, F(1, 10) // 1.0) self.assertTypedEquals(10, 1 // F(1, 10)) @@ -631,6 +833,21 @@ def testMixedArithmetic(self): self.assertTypedTupleEquals(divmod(-0.1, float('inf')), divmod(F(-1, 10), float('inf'))) self.assertTypedTupleEquals(divmod(-0.1, float('-inf')), divmod(F(-1, 10), float('-inf'))) + self.assertTypedEquals(F(3, 2) % DummyFraction(3, 5), F(3, 10)) + self.assertTypedEquals(DummyFraction(5, 3) % F(2, 3), F(1, 3)) + self.assertTypedEquals(F(3, 2) % Rat(3, 5), Rat(3, 6)) + self.assertTypedEquals(Rat(5, 3) % F(2, 3), F(1, 3)) + + self.assertRaises(TypeError, operator.mod, F(2, 3), Root(4)) + self.assertTypedEquals(Root(4) % F(3, 2), 0.5) + + self.assertRaises(TypeError, operator.mod, F(3, 2), Polar(4, 2)) + self.assertRaises(TypeError, operator.mod, Rect(4, 3), F(2, 3)) + + self.assertTypedEquals(F(3, 2) % One, F(1, 2)) + self.assertRaises(TypeError, operator.mod, One, F(2, 3)) + + def testMixedPower(self): # ** has more interesting conversion rules. self.assertTypedEquals(F(100, 1), F(1, 10) ** -2) self.assertTypedEquals(F(100, 1), F(10, 1) ** 2) @@ -647,6 +864,35 @@ def testMixedArithmetic(self): self.assertRaises(ZeroDivisionError, operator.pow, F(0, 1), -2) + self.assertTypedEquals(F(3, 2) ** Rat(3, 1), F(27, 8)) + self.assertTypedEquals(F(3, 2) ** Rat(-3, 1), F(8, 27)) + self.assertTypedEquals(F(-3, 2) ** Rat(-3, 1), F(-8, 27)) + self.assertTypedEquals(F(9, 4) ** Rat(3, 2), 3.375) + self.assertIsInstance(F(4, 9) ** Rat(-3, 2), float) + self.assertAlmostEqual(F(4, 9) ** Rat(-3, 2), 3.375) + self.assertAlmostEqual(F(-4, 9) ** Rat(-3, 2), 3.375j) + + self.assertTypedEquals(Rat(9, 4) ** F(3, 2), 3.375) + self.assertTypedEquals(Rat(3, 2) ** F(3, 1), Rat(27, 8)) + self.assertTypedEquals(Rat(3, 2) ** F(-3, 1), F(8, 27)) + self.assertIsInstance(Rat(4, 9) ** F(-3, 2), float) + self.assertAlmostEqual(Rat(4, 9) ** F(-3, 2), 3.375) + + self.assertTypedEquals(Root(4) ** F(2, 3), Root(4, 3.0)) + self.assertTypedEquals(Root(4) ** F(2, 1), Root(4, F(1))) + self.assertTypedEquals(Root(4) ** F(-2, 1), Root(4, -F(1))) + self.assertTypedEquals(Root(4) ** F(-2, 3), Root(4, -3.0)) + + self.assertTypedEquals(F(3, 2) ** Rect(2, 0), Polar(2.25, 0.0)) + self.assertTypedEquals(F(1, 1) ** Rect(2, 3), Polar(1.0, 0.0)) + self.assertTypedEquals(Polar(4, 2) ** F(3, 2), Polar(8.0, 3.0)) + self.assertTypedEquals(Polar(4, 2) ** F(3, 1), Polar(64, 6)) + self.assertTypedEquals(Polar(4, 2) ** F(-3, 1), Polar(0.015625, -6)) + self.assertTypedEquals(Polar(4, 2) ** F(-3, 2), Polar(0.125, -3.0)) + + self.assertTypedEquals(F(3, 2) ** One, 1.5) + self.assertTypedEquals(One ** F(3, 2), One) + def testMixingWithDecimal(self): # Decimal refuses mixed arithmetic (but not mixed comparisons) self.assertRaises(TypeError, operator.add, From c1f6081ddaf7e65335812e1b0fc69aa69c93a26c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 20 May 2024 21:33:54 +0300 Subject: [PATCH 2/2] Address reviwe comments. Replace class One with class Symbolic. --- Lib/test/test_fractions.py | 65 ++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index ac8267d42aac5a..3a9a86fe7a8b67 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -92,37 +92,51 @@ class DummyFraction(fractions.Fraction): def _components(r): return (r.numerator, r.denominator) -def is_eq(a, b): +def typed_approx_eq(a, b): return type(a) == type(b) and (a == b or math.isclose(a, b)) -class One: +class Symbolic: + """Simple non-numeric class for testing mixed arithmetic. + It is not Integral, Rational, Real or Complex, and cannot be conveted + to int, float or complex. but it supports some arithmetic operations. + """ + def __init__(self, value): + self.value = value def __mul__(self, other): if isinstance(other, F): return NotImplemented - return other + return self.__class__(f'{self} * {other}') def __rmul__(self, other): - return other + return self.__class__(f'{other} * {self}') def __truediv__(self, other): if isinstance(other, F): return NotImplemented - return 1 / other + return self.__class__(f'{self} / {other}') def __rtruediv__(self, other): - return other + return self.__class__(f'{other} / {self}') def __mod__(self, other): if isinstance(other, F): return NotImplemented - return 1 % other + return self.__class__(f'{self} % {other}') def __rmod__(self, other): - return other % 1 + return self.__class__(f'{other} % {self}') def __pow__(self, other): if isinstance(other, F): return NotImplemented - return self + return self.__class__(f'{self} ** {other}') def __rpow__(self, other): - return other -One = One() + return self.__class__(f'{other} ** {self}') + def __eq__(self, other): + if other.__class__ != self.__class__: + return NotImplemented + return self.value == other.value + def __str__(self): + return f'{self.value}' + def __repr__(self): + return f'{self.__class__.__name__}({self.value!r})' class Rat: + """Simple Rational class for testing mixed arithmetic.""" def __init__(self, n, d): self.numerator = n self.denominator = d @@ -163,13 +177,14 @@ def __float__(self): def __eq__(self, other): if self.__class__ != other.__class__: return NotImplemented - return (is_eq(self.numerator, other.numerator) and - is_eq(self.denominator, other.denominator)) + return (typed_approx_eq(self.numerator, other.numerator) and + typed_approx_eq(self.denominator, other.denominator)) def __repr__(self): return f'{self.__class__.__name__}({self.numerator!r}, {self.denominator!r})' numbers.Rational.register(Rat) class Root: + """Simple Real class for testing mixed arithmetic.""" def __init__(self, v, n=F(2)): self.base = v self.degree = n @@ -194,12 +209,13 @@ def __float__(self): def __eq__(self, other): if self.__class__ != other.__class__: return NotImplemented - return is_eq(self.base, other.base) and is_eq(self.degree, other.degree) + return typed_approx_eq(self.base, other.base) and typed_approx_eq(self.degree, other.degree) def __repr__(self): return f'{self.__class__.__name__}({self.base!r}, {self.degree!r})' numbers.Real.register(Root) class Polar: + """Simple Complex class for testing mixed arithmetic.""" def __init__(self, r, phi): self.r = r self.phi = phi @@ -222,12 +238,13 @@ def __pow__(self, other): def __eq__(self, other): if self.__class__ != other.__class__: return NotImplemented - return is_eq(self.r, other.r) and is_eq(self.phi, other.phi) + return typed_approx_eq(self.r, other.r) and typed_approx_eq(self.phi, other.phi) def __repr__(self): return f'{self.__class__.__name__}({self.r!r}, {self.phi!r})' numbers.Complex.register(Polar) class Rect: + """Other simple Complex class for testing mixed arithmetic.""" def __init__(self, x, y): self.x = x self.y = y @@ -251,7 +268,7 @@ def __complex__(self): def __eq__(self, other): if self.__class__ != other.__class__: return NotImplemented - return is_eq(self.x, other.x) and is_eq(self.y, other.y) + return typed_approx_eq(self.x, other.x) and typed_approx_eq(self.y, other.y) def __repr__(self): return f'{self.__class__.__name__}({self.x!r}, {self.y!r})' numbers.Complex.register(Rect) @@ -780,8 +797,8 @@ def testMixedMultiplication(self): self.assertRaises(TypeError, operator.mul, Polar(4, 2), F(3, 2)) self.assertTypedEquals(Rect(4, 3) * F(3, 2), 6.0 + 4.5j) - self.assertTypedEquals(F(3, 2) * One, F(3, 2)) - self.assertRaises(TypeError, operator.mul, One, F(3, 2)) + self.assertEqual(F(3, 2) * Symbolic('X'), Symbolic('3/2 * X')) + self.assertRaises(TypeError, operator.mul, Symbolic('X'), F(3, 2)) def testMixedDivision(self): self.assertTypedEquals(F(1, 10), F(1, 10) / 1) @@ -805,8 +822,8 @@ def testMixedDivision(self): self.assertRaises(TypeError, operator.truediv, Polar(4, 2), F(2, 3)) self.assertTypedEquals(Rect(4, 3) / F(2, 3), 6.0 + 4.5j) - self.assertTypedEquals(F(3, 2) / One, F(3, 2)) - self.assertRaises(TypeError, operator.truediv, One, F(2, 3)) + self.assertEqual(F(3, 2) / Symbolic('X'), Symbolic('3/2 / X')) + self.assertRaises(TypeError, operator.truediv, Symbolic('X'), F(2, 3)) def testMixedIntegerDivision(self): self.assertTypedEquals(0, F(1, 10) // 1) @@ -844,8 +861,8 @@ def testMixedIntegerDivision(self): self.assertRaises(TypeError, operator.mod, F(3, 2), Polar(4, 2)) self.assertRaises(TypeError, operator.mod, Rect(4, 3), F(2, 3)) - self.assertTypedEquals(F(3, 2) % One, F(1, 2)) - self.assertRaises(TypeError, operator.mod, One, F(2, 3)) + self.assertEqual(F(3, 2) % Symbolic('X'), Symbolic('3/2 % X')) + self.assertRaises(TypeError, operator.mod, Symbolic('X'), F(2, 3)) def testMixedPower(self): # ** has more interesting conversion rules. @@ -890,8 +907,8 @@ def testMixedPower(self): self.assertTypedEquals(Polar(4, 2) ** F(-3, 1), Polar(0.015625, -6)) self.assertTypedEquals(Polar(4, 2) ** F(-3, 2), Polar(0.125, -3.0)) - self.assertTypedEquals(F(3, 2) ** One, 1.5) - self.assertTypedEquals(One ** F(3, 2), One) + self.assertTypedEquals(F(3, 2) ** Symbolic('X'), Symbolic('1.5 ** X')) + self.assertTypedEquals(Symbolic('X') ** F(3, 2), Symbolic('X ** 1.5')) def testMixingWithDecimal(self): # Decimal refuses mixed arithmetic (but not mixed comparisons)