Skip to content

Commit e965299

Browse files
committed
Fix type interaction with other Rational classes.
See python/cpython#119236
1 parent 4a3e8a0 commit e965299

File tree

3 files changed

+297
-3
lines changed

3 files changed

+297
-3
lines changed

Diff for: CHANGES.rst

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ ChangeLog
1212
* Using ``complex`` numbers in division shows better tracebacks.
1313
https://github.com/python/cpython/pull/102842
1414

15+
* Mixed calculations with other ``Rational`` classes could return the wrong type.
16+
https://github.com/python/cpython/issues/119189
17+
1518

1619
1.18 (2024-04-03)
1720
-----------------

Diff for: src/quicktions.pyx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1487,7 +1487,7 @@ cdef forward(a, b, math_func monomorphic_operator, pyoperator, handle_complex=Tr
14871487
return monomorphic_operator(an, ad, (<Fraction>b)._numerator, (<Fraction>b)._denominator)
14881488
elif isinstance(b, int):
14891489
return monomorphic_operator(an, ad, b, 1)
1490-
elif isinstance(b, (Fraction, Rational)):
1490+
elif isinstance(b, Fraction):
14911491
return monomorphic_operator(an, ad, b.numerator, b.denominator)
14921492
elif isinstance(b, float):
14931493
return pyoperator(_as_float(an, ad), b)

Diff for: src/test_fractions.py

+293-2
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def __float__(self):
135135
assert False, "__float__ should not be invoked"
136136

137137

138-
class DummyFraction(fractions.Fraction):
138+
class DummyFraction(quicktions.Fraction):
139139
"""Dummy Fraction subclass for copy and deepcopy testing."""
140140

141141

@@ -200,6 +200,197 @@ def test_quicktions_limits(self):
200200
def _components(r):
201201
return (r.numerator, r.denominator)
202202

203+
def typed_approx_eq(a, b):
204+
return type(a) == type(b) and (a == b or math.isclose(a, b))
205+
206+
class Symbolic:
207+
"""Simple non-numeric class for testing mixed arithmetic.
208+
It is not Integral, Rational, Real or Complex, and cannot be conveted
209+
to int, float or complex. but it supports some arithmetic operations.
210+
"""
211+
def __init__(self, value):
212+
self.value = value
213+
def __mul__(self, other):
214+
if isinstance(other, F):
215+
return NotImplemented
216+
return self.__class__(f'{self} * {other}')
217+
def __rmul__(self, other):
218+
return self.__class__(f'{other} * {self}')
219+
def __truediv__(self, other):
220+
if isinstance(other, F):
221+
return NotImplemented
222+
return self.__class__(f'{self} / {other}')
223+
def __rtruediv__(self, other):
224+
return self.__class__(f'{other} / {self}')
225+
def __mod__(self, other):
226+
if isinstance(other, F):
227+
return NotImplemented
228+
return self.__class__(f'{self} % {other}')
229+
def __rmod__(self, other):
230+
return self.__class__(f'{other} % {self}')
231+
def __pow__(self, other):
232+
if isinstance(other, F):
233+
return NotImplemented
234+
return self.__class__(f'{self} ** {other}')
235+
def __rpow__(self, other):
236+
return self.__class__(f'{other} ** {self}')
237+
def __eq__(self, other):
238+
if other.__class__ != self.__class__:
239+
return NotImplemented
240+
return self.value == other.value
241+
def __str__(self):
242+
return f'{self.value}'
243+
def __repr__(self):
244+
return f'{self.__class__.__name__}({self.value!r})'
245+
246+
class SymbolicReal(Symbolic):
247+
pass
248+
numbers.Real.register(SymbolicReal)
249+
250+
class SymbolicComplex(Symbolic):
251+
pass
252+
numbers.Complex.register(SymbolicComplex)
253+
254+
class Rat:
255+
"""Simple Rational class for testing mixed arithmetic."""
256+
def __init__(self, n, d):
257+
self.numerator = n
258+
self.denominator = d
259+
def __mul__(self, other):
260+
if isinstance(other, F):
261+
return NotImplemented
262+
return self.__class__(self.numerator * other.numerator,
263+
self.denominator * other.denominator)
264+
def __rmul__(self, other):
265+
return self.__class__(other.numerator * self.numerator,
266+
other.denominator * self.denominator)
267+
def __truediv__(self, other):
268+
if isinstance(other, F):
269+
return NotImplemented
270+
return self.__class__(self.numerator * other.denominator,
271+
self.denominator * other.numerator)
272+
def __rtruediv__(self, other):
273+
return self.__class__(other.numerator * self.denominator,
274+
other.denominator * self.numerator)
275+
def __mod__(self, other):
276+
if isinstance(other, F):
277+
return NotImplemented
278+
d = self.denominator * other.numerator
279+
return self.__class__(self.numerator * other.denominator % d, d)
280+
def __rmod__(self, other):
281+
d = other.denominator * self.numerator
282+
return self.__class__(other.numerator * self.denominator % d, d)
283+
284+
return self.__class__(other.numerator / self.numerator,
285+
other.denominator / self.denominator)
286+
def __pow__(self, other):
287+
if isinstance(other, F):
288+
return NotImplemented
289+
return self.__class__(self.numerator ** other,
290+
self.denominator ** other)
291+
def __float__(self):
292+
return self.numerator / self.denominator
293+
def __eq__(self, other):
294+
if self.__class__ != other.__class__:
295+
return NotImplemented
296+
return (typed_approx_eq(self.numerator, other.numerator) and
297+
typed_approx_eq(self.denominator, other.denominator))
298+
def __repr__(self):
299+
return f'{self.__class__.__name__}({self.numerator!r}, {self.denominator!r})'
300+
numbers.Rational.register(Rat)
301+
302+
class Root:
303+
"""Simple Real class for testing mixed arithmetic."""
304+
def __init__(self, v, n=F(2)):
305+
self.base = v
306+
self.degree = n
307+
def __mul__(self, other):
308+
if isinstance(other, F):
309+
return NotImplemented
310+
return self.__class__(self.base * other**self.degree, self.degree)
311+
def __rmul__(self, other):
312+
return self.__class__(other**self.degree * self.base, self.degree)
313+
def __truediv__(self, other):
314+
if isinstance(other, F):
315+
return NotImplemented
316+
return self.__class__(self.base / other**self.degree, self.degree)
317+
def __rtruediv__(self, other):
318+
return self.__class__(other**self.degree / self.base, self.degree)
319+
def __pow__(self, other):
320+
if isinstance(other, F):
321+
return NotImplemented
322+
return self.__class__(self.base, self.degree / other)
323+
def __float__(self):
324+
return float(self.base) ** (1 / float(self.degree))
325+
def __eq__(self, other):
326+
if self.__class__ != other.__class__:
327+
return NotImplemented
328+
return typed_approx_eq(self.base, other.base) and typed_approx_eq(self.degree, other.degree)
329+
def __repr__(self):
330+
return f'{self.__class__.__name__}({self.base!r}, {self.degree!r})'
331+
numbers.Real.register(Root)
332+
333+
class Polar:
334+
"""Simple Complex class for testing mixed arithmetic."""
335+
def __init__(self, r, phi):
336+
self.r = r
337+
self.phi = phi
338+
def __mul__(self, other):
339+
if isinstance(other, F):
340+
return NotImplemented
341+
return self.__class__(self.r * other, self.phi)
342+
def __rmul__(self, other):
343+
return self.__class__(other * self.r, self.phi)
344+
def __truediv__(self, other):
345+
if isinstance(other, F):
346+
return NotImplemented
347+
return self.__class__(self.r / other, self.phi)
348+
def __rtruediv__(self, other):
349+
return self.__class__(other / self.r, -self.phi)
350+
def __pow__(self, other):
351+
if isinstance(other, F):
352+
return NotImplemented
353+
return self.__class__(self.r ** other, self.phi * other)
354+
def __eq__(self, other):
355+
if self.__class__ != other.__class__:
356+
return NotImplemented
357+
return typed_approx_eq(self.r, other.r) and typed_approx_eq(self.phi, other.phi)
358+
def __repr__(self):
359+
return f'{self.__class__.__name__}({self.r!r}, {self.phi!r})'
360+
numbers.Complex.register(Polar)
361+
362+
class Rect:
363+
"""Other simple Complex class for testing mixed arithmetic."""
364+
def __init__(self, x, y):
365+
self.x = x
366+
self.y = y
367+
def __mul__(self, other):
368+
if isinstance(other, F):
369+
return NotImplemented
370+
return self.__class__(self.x * other, self.y * other)
371+
def __rmul__(self, other):
372+
return self.__class__(other * self.x, other * self.y)
373+
def __truediv__(self, other):
374+
if isinstance(other, F):
375+
return NotImplemented
376+
return self.__class__(self.x / other, self.y / other)
377+
def __rtruediv__(self, other):
378+
r = self.x * self.x + self.y * self.y
379+
return self.__class__(other * (self.x / r), other * (self.y / r))
380+
def __rpow__(self, other):
381+
return Polar(other ** self.x, math.log(other) * self.y)
382+
def __complex__(self):
383+
return complex(self.x, self.y)
384+
def __eq__(self, other):
385+
if self.__class__ != other.__class__:
386+
return NotImplemented
387+
return typed_approx_eq(self.x, other.x) and typed_approx_eq(self.y, other.y)
388+
def __repr__(self):
389+
return f'{self.__class__.__name__}({self.x!r}, {self.y!r})'
390+
numbers.Complex.register(Rect)
391+
392+
class RectComplex(Rect, complex):
393+
pass
203394

204395
class FractionTest(unittest.TestCase):
205396

@@ -795,20 +986,66 @@ def testMixedArithmetic(self):
795986
self.assertTypedEquals(0.9, 1.0 - F(1, 10))
796987
self.assertTypedEquals(0.9 + 0j, (1.0 + 0j) - F(1, 10))
797988

989+
def testMixedMultiplication(self):
798990
self.assertTypedEquals(F(1, 10), F(1, 10) * 1)
799991
self.assertTypedEquals(0.1, F(1, 10) * 1.0)
800992
self.assertTypedEquals(0.1 + 0j, F(1, 10) * (1.0 + 0j))
801993
self.assertTypedEquals(F(1, 10), 1 * F(1, 10))
802994
self.assertTypedEquals(0.1, 1.0 * F(1, 10))
803995
self.assertTypedEquals(0.1 + 0j, (1.0 + 0j) * F(1, 10))
804996

997+
self.assertTypedEquals(F(3, 2) * DummyFraction(5, 3), F(5, 2))
998+
self.assertTypedEquals(DummyFraction(5, 3) * F(3, 2), F(5, 2))
999+
self.assertTypedEquals(F(3, 2) * Rat(5, 3), Rat(15, 6))
1000+
self.assertTypedEquals(Rat(5, 3) * F(3, 2), F(5, 2))
1001+
1002+
self.assertTypedEquals(F(3, 2) * Root(4), Root(F(9, 1)))
1003+
self.assertTypedEquals(Root(4) * F(3, 2), 3.0)
1004+
self.assertEqual(F(3, 2) * SymbolicReal('X'), SymbolicReal('3/2 * X'))
1005+
self.assertRaises(TypeError, operator.mul, SymbolicReal('X'), F(3, 2))
1006+
1007+
self.assertTypedEquals(F(3, 2) * Polar(4, 2), Polar(F(6, 1), 2))
1008+
self.assertTypedEquals(F(3, 2) * Polar(4.0, 2), Polar(6.0, 2))
1009+
self.assertTypedEquals(F(3, 2) * Rect(4, 3), Rect(F(6, 1), F(9, 2)))
1010+
self.assertTypedEquals(F(3, 2) * RectComplex(4, 3), RectComplex(6.0+0j, 4.5+0j))
1011+
self.assertRaises(TypeError, operator.mul, Polar(4, 2), F(3, 2))
1012+
self.assertTypedEquals(Rect(4, 3) * F(3, 2), 6.0 + 4.5j)
1013+
self.assertEqual(F(3, 2) * SymbolicComplex('X'), SymbolicComplex('3/2 * X'))
1014+
self.assertRaises(TypeError, operator.mul, SymbolicComplex('X'), F(3, 2))
1015+
1016+
self.assertEqual(F(3, 2) * Symbolic('X'), Symbolic('3/2 * X'))
1017+
self.assertRaises(TypeError, operator.mul, Symbolic('X'), F(3, 2))
1018+
1019+
def testMixedDivision(self):
8051020
self.assertTypedEquals(F(1, 10), F(1, 10) / 1)
8061021
self.assertTypedEquals(0.1, F(1, 10) / 1.0)
8071022
self.assertTypedEquals(0.1 + 0j, F(1, 10) / (1.0 + 0j))
8081023
self.assertTypedEquals(F(10, 1), 1 / F(1, 10))
8091024
self.assertTypedEquals(10.0, 1.0 / F(1, 10))
8101025
self.assertTypedEquals(10.0 + 0j, (1.0 + 0j) / F(1, 10))
8111026

1027+
self.assertTypedEquals(F(3, 2) / DummyFraction(3, 5), F(5, 2))
1028+
self.assertTypedEquals(DummyFraction(5, 3) / F(2, 3), F(5, 2))
1029+
self.assertTypedEquals(F(3, 2) / Rat(3, 5), Rat(15, 6))
1030+
self.assertTypedEquals(Rat(5, 3) / F(2, 3), F(5, 2))
1031+
1032+
self.assertTypedEquals(F(2, 3) / Root(4), Root(F(1, 9)))
1033+
self.assertTypedEquals(Root(4) / F(2, 3), 3.0)
1034+
self.assertEqual(F(3, 2) / SymbolicReal('X'), SymbolicReal('3/2 / X'))
1035+
self.assertRaises(TypeError, operator.truediv, SymbolicReal('X'), F(3, 2))
1036+
1037+
self.assertTypedEquals(F(3, 2) / Polar(4, 2), Polar(F(3, 8), -2))
1038+
self.assertTypedEquals(F(3, 2) / Polar(4.0, 2), Polar(0.375, -2))
1039+
self.assertTypedEquals(F(3, 2) / Rect(4, 3), Rect(0.24, 0.18))
1040+
self.assertRaises(TypeError, operator.truediv, Polar(4, 2), F(2, 3))
1041+
self.assertTypedEquals(Rect(4, 3) / F(2, 3), 6.0 + 4.5j)
1042+
self.assertEqual(F(3, 2) / SymbolicComplex('X'), SymbolicComplex('3/2 / X'))
1043+
self.assertRaises(TypeError, operator.truediv, SymbolicComplex('X'), F(3, 2))
1044+
1045+
self.assertEqual(F(3, 2) / Symbolic('X'), Symbolic('3/2 / X'))
1046+
self.assertRaises(TypeError, operator.truediv, Symbolic('X'), F(2, 3))
1047+
1048+
def testMixedIntegerDivision(self):
8121049
self.assertTypedEquals(0, F(1, 10) // 1)
8131050
self.assertTypedEquals(0.0, F(1, 10) // 1.0)
8141051
self.assertTypedEquals(10, 1 // F(1, 10))
@@ -835,6 +1072,26 @@ def testMixedArithmetic(self):
8351072
self.assertTypedTupleEquals(divmod(-0.1, float('inf')), divmod(F(-1, 10), float('inf')))
8361073
self.assertTypedTupleEquals(divmod(-0.1, float('-inf')), divmod(F(-1, 10), float('-inf')))
8371074

1075+
self.assertTypedEquals(F(3, 2) % DummyFraction(3, 5), F(3, 10))
1076+
self.assertTypedEquals(DummyFraction(5, 3) % F(2, 3), F(1, 3))
1077+
self.assertTypedEquals(F(3, 2) % Rat(3, 5), Rat(3, 6))
1078+
self.assertTypedEquals(Rat(5, 3) % F(2, 3), F(1, 3))
1079+
1080+
self.assertRaises(TypeError, operator.mod, F(2, 3), Root(4))
1081+
self.assertTypedEquals(Root(4) % F(3, 2), 0.5)
1082+
self.assertEqual(F(3, 2) % SymbolicReal('X'), SymbolicReal('3/2 % X'))
1083+
self.assertRaises(TypeError, operator.mod, SymbolicReal('X'), F(3, 2))
1084+
1085+
self.assertRaises(TypeError, operator.mod, F(3, 2), Polar(4, 2))
1086+
self.assertRaises(TypeError, operator.mod, F(3, 2), RectComplex(4, 3))
1087+
self.assertRaises(TypeError, operator.mod, Rect(4, 3), F(2, 3))
1088+
self.assertEqual(F(3, 2) % SymbolicComplex('X'), SymbolicComplex('3/2 % X'))
1089+
self.assertRaises(TypeError, operator.mod, SymbolicComplex('X'), F(3, 2))
1090+
1091+
self.assertEqual(F(3, 2) % Symbolic('X'), Symbolic('3/2 % X'))
1092+
self.assertRaises(TypeError, operator.mod, Symbolic('X'), F(2, 3))
1093+
1094+
def testMixedPower(self):
8381095
# ** has more interesting conversion rules.
8391096
self.assertTypedEquals(F(100, 1), F(1, 10) ** -2)
8401097
self.assertTypedEquals(F(100, 1), F(10, 1) ** 2)
@@ -855,6 +1112,40 @@ def testMixedArithmetic(self):
8551112
self.assertRaises(ZeroDivisionError, operator.pow,
8561113
F(0, 1), -2)
8571114

1115+
self.assertTypedEquals(F(3, 2) ** Rat(3, 1), F(27, 8))
1116+
self.assertTypedEquals(F(3, 2) ** Rat(-3, 1), F(8, 27))
1117+
self.assertTypedEquals(F(-3, 2) ** Rat(-3, 1), F(-8, 27))
1118+
self.assertTypedEquals(F(9, 4) ** Rat(3, 2), 3.375)
1119+
self.assertIsInstance(F(4, 9) ** Rat(-3, 2), float)
1120+
self.assertAlmostEqual(F(4, 9) ** Rat(-3, 2), 3.375)
1121+
self.assertAlmostEqual(F(-4, 9) ** Rat(-3, 2), 3.375j)
1122+
self.assertTypedEquals(Rat(9, 4) ** F(3, 2), 3.375)
1123+
self.assertTypedEquals(Rat(3, 2) ** F(3, 1), Rat(27, 8))
1124+
self.assertTypedEquals(Rat(3, 2) ** F(-3, 1), F(8, 27))
1125+
self.assertIsInstance(Rat(4, 9) ** F(-3, 2), float)
1126+
self.assertAlmostEqual(Rat(4, 9) ** F(-3, 2), 3.375)
1127+
1128+
self.assertTypedEquals(Root(4) ** F(2, 3), Root(4, 3.0))
1129+
self.assertTypedEquals(Root(4) ** F(2, 1), Root(4, F(1)))
1130+
self.assertTypedEquals(Root(4) ** F(-2, 1), Root(4, -F(1)))
1131+
self.assertTypedEquals(Root(4) ** F(-2, 3), Root(4, -3.0))
1132+
self.assertEqual(F(3, 2) ** SymbolicReal('X'), SymbolicReal('1.5 ** X'))
1133+
self.assertEqual(SymbolicReal('X') ** F(3, 2), SymbolicReal('X ** 1.5'))
1134+
1135+
self.assertTypedEquals(F(3, 2) ** Rect(2, 0), Polar(2.25, 0.0))
1136+
self.assertTypedEquals(F(1, 1) ** Rect(2, 3), Polar(1.0, 0.0))
1137+
self.assertTypedEquals(F(3, 2) ** RectComplex(2, 0), Polar(2.25, 0.0))
1138+
self.assertTypedEquals(F(1, 1) ** RectComplex(2, 3), Polar(1.0, 0.0))
1139+
self.assertTypedEquals(Polar(4, 2) ** F(3, 2), Polar(8.0, 3.0))
1140+
self.assertTypedEquals(Polar(4, 2) ** F(3, 1), Polar(64, 6))
1141+
self.assertTypedEquals(Polar(4, 2) ** F(-3, 1), Polar(0.015625, -6))
1142+
self.assertTypedEquals(Polar(4, 2) ** F(-3, 2), Polar(0.125, -3.0))
1143+
self.assertEqual(F(3, 2) ** SymbolicComplex('X'), SymbolicComplex('1.5 ** X'))
1144+
self.assertEqual(SymbolicComplex('X') ** F(3, 2), SymbolicComplex('X ** 1.5'))
1145+
1146+
self.assertEqual(F(3, 2) ** Symbolic('X'), Symbolic('1.5 ** X'))
1147+
self.assertEqual(Symbolic('X') ** F(3, 2), Symbolic('X ** 1.5'))
1148+
8581149
def testMixingWithDecimal(self):
8591150
# Decimal refuses mixed arithmetic (but not mixed comparisons)
8601151
self.assertRaises(TypeError, operator.add,
@@ -1087,7 +1378,7 @@ def numerator(self):
10871378
def denominator(self):
10881379
return type(self)(1)
10891380

1090-
f = fractions.Fraction(myint(1 * 3), myint(2 * 3))
1381+
f = F(myint(1 * 3), myint(2 * 3))
10911382
self.assertEqual(f.numerator, 1)
10921383
self.assertEqual(f.denominator, 2)
10931384
self.assertEqual(type(f.numerator), myint)

0 commit comments

Comments
 (0)