From ee617bdf035443d18fcde0abae2a0cb167415d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:43:16 +0200 Subject: [PATCH 01/10] _pydecimal: avoid slow exponentiation in floor division --- Lib/_pydecimal.py | 10 +++++++--- .../2025-10-13-15-43-09.gh-issue-140036.b_59uN.rst | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-10-13-15-43-09.gh-issue-140036.b_59uN.rst diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index 9b8e42a2342536..aebda52fed4812 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -92,6 +92,8 @@ MIN_ETINY = MIN_EMIN - (MAX_PREC-1) +_LOG_10_BASE_2 = float.fromhex('0x1.a934f0979a371p+1') # log2(10) + # Errors class DecimalException(ArithmeticError): @@ -1355,9 +1357,11 @@ def _divide(self, other, context): else: op2.int *= 10**(op2.exp - op1.exp) q, r = divmod(op1.int, op2.int) - if q < 10**context.prec: - return (_dec_from_triple(sign, str(q), 0), - _dec_from_triple(self._sign, str(r), ideal_exp)) + if q.bit_length() < 1 + context.prec * _LOG_10_BASE_2: + # ensure that the previous check was sufficient + if len(str_q := str(q)) <= context.prec: + return (_dec_from_triple(sign, str_q, 0), + _dec_from_triple(self._sign, str(r), ideal_exp)) # Here the quotient is too large to be representable ans = context._raise_error(DivisionImpossible, diff --git a/Misc/NEWS.d/next/Library/2025-10-13-15-43-09.gh-issue-140036.b_59uN.rst b/Misc/NEWS.d/next/Library/2025-10-13-15-43-09.gh-issue-140036.b_59uN.rst new file mode 100644 index 00000000000000..4bd92b4d735225 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-13-15-43-09.gh-issue-140036.b_59uN.rst @@ -0,0 +1,2 @@ +Avoid hanging in floor division of pure Python :class:`decimal.Decimal` +instances when the context precision is very large. Patch by Bénédikt Tran. From 329c31a7609ec67ec7860cb571843aa2cef9f72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 14 Oct 2025 12:22:26 +0200 Subject: [PATCH 02/10] refine `q < 10**context.prec` checks --- Lib/_pydecimal.py | 70 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index aebda52fed4812..5b170c5043948b 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -92,8 +92,6 @@ MIN_ETINY = MIN_EMIN - (MAX_PREC-1) -_LOG_10_BASE_2 = float.fromhex('0x1.a934f0979a371p+1') # log2(10) - # Errors class DecimalException(ArithmeticError): @@ -444,6 +442,27 @@ def IEEEContext(bits, /): ##### Decimal class ####################################################### +# Observation: For all q >= 0 and a >= 1, q < 10**a iff len(str(q)) <= a. +# +# The constants below are used to speed-up "q < 10 ** a" checks to avoid +# computing len(str(q)) as much as possible. Those speed-ups are based on +# the following claims. +# +# See https://github.com/python/cpython/issues/140036 for details. + +# Claim: If 0 < z <= log2(10) and q.bit_length() < a*z, then q < 10**a. +# Proof: By contradiction, q >= 10**a. By definition, +# log2(q) >= a*log2(10) >= a*z > q.bit_length(). +# In particular, q > 2**q.bit_length(), which is impossible. +_LOG_10_BASE_2_LO = float.fromhex('0x1.a934f0979a371p+1') +assert pow(2, _LOG_10_BASE_2_LO) < 10 + +# Claim: If z > log2(10) and q.bit_length() >= 1 + a*z, then q > 10**a. +# Proof: Since q >= 2**(q.bit_length()-1), we have +# q >= 2**(q.bit_length()-1) >= 2**(a*z) > 2**(a*log2(10)) = 10**a. +_LOG_10_BASE_2_HI = float.fromhex('0x1.a934f0979a372p+1') +assert pow(2, _LOG_10_BASE_2_HI) > 10 + # Do not subclass Decimal from numbers.Real and do not register it as such # (because Decimals are not interoperable with floats). See the notes in # numbers.py for more detail. @@ -1357,11 +1376,27 @@ def _divide(self, other, context): else: op2.int *= 10**(op2.exp - op1.exp) q, r = divmod(op1.int, op2.int) - if q.bit_length() < 1 + context.prec * _LOG_10_BASE_2: - # ensure that the previous check was sufficient - if len(str_q := str(q)) <= context.prec: - return (_dec_from_triple(sign, str_q, 0), - _dec_from_triple(self._sign, str(r), ideal_exp)) + # See notes for _LOG_10_BASE_2_LO and _LOG_10_BASE_2_HI. + str_q = None # to cache str(q) when possible + if q.bit_length() < context.prec * _LOG_10_BASE_2_LO: + # assert q < 10 ** context.prec + is_valid = True + elif q.bit_length() >= 1 + context.prec * _LOG_10_BASE_2_HI: + # assert q > 10 ** context.prec + is_valid = False + else: + # Handles other cases due to floating point precision loss + # when computing _LOG_10_BASE_2_LO and _LOG_10_BASE_2_HI. + # Computation of str(q) may fail! + str_q = str(q) # we need to compute this in case of success + is_valid = len(str_q) <= context.prec + if is_valid: + if str_q is None: + str_q = str(q) + # assert q < 10 ** context.prec + # assert len(str(q)) <= context.prec + return (_dec_from_triple(sign, str_q, 0), + _dec_from_triple(self._sign, str(r), ideal_exp)) # Here the quotient is too large to be representable ans = context._raise_error(DivisionImpossible, @@ -1515,7 +1550,26 @@ def remainder_near(self, other, context=None): r -= op2.int q += 1 - if q >= 10**context.prec: + # See notes for _LOG_10_BASE_2_LO and _LOG_10_BASE_2_HI. + if q.bit_length() < context.prec * _LOG_10_BASE_2_LO: + # assert q < 10 ** context.prec + is_valid = True + elif q.bit_length() >= 1 + context.prec * _LOG_10_BASE_2_HI: + # assert q > 10 ** context.prec + is_valid = False + else: + # Handles other cases due to floating point precision loss + # when computing _LOG_10_BASE_2_LO and _LOG_10_BASE_2_HI. + # Computation of str(q) or 10 ** context.prec may be slow! + try: + str_q = str(q) + except ValueError: + is_valid = q < 10 ** context.prec + else: + is_valid = len(str_q) <= context.prec + if not is_valid: + # assert q >= 10 ** context.prec + # assert len(str(q)) > context.prec return context._raise_error(DivisionImpossible) # result has same sign as self unless r is negative From 103fe1e0a6c92d860ea63fa05c3f220b39cba86b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 26 Oct 2025 13:15:49 +0100 Subject: [PATCH 03/10] _pydecimal: add helpers for computing len(str(q)) < a --- Lib/_pydecimal.py | 79 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index 5b170c5043948b..2678d29d32fd2a 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -450,19 +450,84 @@ def IEEEContext(bits, /): # # See https://github.com/python/cpython/issues/140036 for details. -# Claim: If 0 < z <= log2(10) and q.bit_length() < a*z, then q < 10**a. -# Proof: By contradiction, q >= 10**a. By definition, -# log2(q) >= a*log2(10) >= a*z > q.bit_length(). -# In particular, q > 2**q.bit_length(), which is impossible. _LOG_10_BASE_2_LO = float.fromhex('0x1.a934f0979a371p+1') assert pow(2, _LOG_10_BASE_2_LO) < 10 -# Claim: If z > log2(10) and q.bit_length() >= 1 + a*z, then q > 10**a. -# Proof: Since q >= 2**(q.bit_length()-1), we have -# q >= 2**(q.bit_length()-1) >= 2**(a*z) > 2**(a*log2(10)) = 10**a. _LOG_10_BASE_2_HI = float.fromhex('0x1.a934f0979a372p+1') assert pow(2, _LOG_10_BASE_2_HI) > 10 + +def _tento(n): + """Compute 10 ** n with 1 base-5 exponentiation and 1 bit-shift.""" + return (5 ** n) << n + + +def _is_leq_than_pow10a_use_str(q, a): + """Try to efficiently check len(str(q)) <= a, or equivalently q < 10**a. + + If it is not possible to efficiently compute len(str(q)), + this explicitly compute str(q) instead. + + Return (len(str(q)) <= a, None) or (len(str(q)) <= a, str(q)). + """ + if q.bit_length() < a * _LOG_10_BASE_2_LO: + # Claim: If 0 < z <= log2(10) and q.bit_length() < a*z, then q < 10**a. + # Proof: By contradiction, q >= 10**a. By definition, + # log2(q) >= a*log2(10) >= a*z > q.bit_length(). + # In particular, q > 2**q.bit_length(), which is impossible. + + # assert q < 10 ** context.prec + return True, None + elif q.bit_length() >= 1 + a * _LOG_10_BASE_2_HI: + # Claim: If z > log2(10) and q.bit_length() >= 1 + a*z, then q > 10**a. + # Proof: Since q >= 2**(q.bit_length()-1), we have + # q >= 2**(q.bit_length()-1) >= 2**(a*z) > 2**(a*log2(10)) = 10**a. + + # assert q > 10 ** context.prec + return False, None + # Handles other cases due to floating point precision loss + # when computing _LOG_10_BASE_2_LO and _LOG_10_BASE_2_HI. + str_q = str(q) # can raise a ValueError + is_valid = len(str_q) <= a + return is_valid, str_q + + +def _is_leq_than_pow10a(q, a, *, exact=True, ulp_order=20): + """Check that len(str(q)) <= a without computing str(q). + + When *exact* is false, computing len(str(q)) is replaced by f(q): + + f(q) = floor(log10(q) + ulp(log10(q)) * ulp_order + 1.0) + + Most of the time, f(q) = len(str(q)) but in some cases, it may + happen that f(q) > len(str(q)). + + When *exact* is true, computing len(str(q)) requires one bigint + exponentiation that only depends on q. + """ + + if q < 10: + return a >= 1 + + z = _math.log10(q) + t = _math.ulp(z) * ulp_order + + if exact: + intlo = int(z - t) + inthi = int(z + t) + diff = inthi - intlo + assert diff in (0, 1) + if diff == 1: + lo = _tento(inthi) # may be slow + if q < lo: + inthi -= 1 + assert q >= (lo // 10) + ndigits = inthi + 1 + else: + ndigits = int(z + t + 1.0) + return ndigits <= a + + # Do not subclass Decimal from numbers.Real and do not register it as such # (because Decimals are not interoperable with floats). See the notes in # numbers.py for more detail. From efbdc0a788567807eaa945f705c67967c460a43b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 26 Oct 2025 13:25:02 +0100 Subject: [PATCH 04/10] _pydecimal: use helpers for computing len(str(q)) < a --- Lib/_pydecimal.py | 34 ++-------------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index 2678d29d32fd2a..534b1b42e365fe 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -1441,20 +1441,7 @@ def _divide(self, other, context): else: op2.int *= 10**(op2.exp - op1.exp) q, r = divmod(op1.int, op2.int) - # See notes for _LOG_10_BASE_2_LO and _LOG_10_BASE_2_HI. - str_q = None # to cache str(q) when possible - if q.bit_length() < context.prec * _LOG_10_BASE_2_LO: - # assert q < 10 ** context.prec - is_valid = True - elif q.bit_length() >= 1 + context.prec * _LOG_10_BASE_2_HI: - # assert q > 10 ** context.prec - is_valid = False - else: - # Handles other cases due to floating point precision loss - # when computing _LOG_10_BASE_2_LO and _LOG_10_BASE_2_HI. - # Computation of str(q) may fail! - str_q = str(q) # we need to compute this in case of success - is_valid = len(str_q) <= context.prec + is_valid, str_q = _is_leq_than_pow10a_use_str(q, context.prec) if is_valid: if str_q is None: str_q = str(q) @@ -1615,24 +1602,7 @@ def remainder_near(self, other, context=None): r -= op2.int q += 1 - # See notes for _LOG_10_BASE_2_LO and _LOG_10_BASE_2_HI. - if q.bit_length() < context.prec * _LOG_10_BASE_2_LO: - # assert q < 10 ** context.prec - is_valid = True - elif q.bit_length() >= 1 + context.prec * _LOG_10_BASE_2_HI: - # assert q > 10 ** context.prec - is_valid = False - else: - # Handles other cases due to floating point precision loss - # when computing _LOG_10_BASE_2_LO and _LOG_10_BASE_2_HI. - # Computation of str(q) or 10 ** context.prec may be slow! - try: - str_q = str(q) - except ValueError: - is_valid = q < 10 ** context.prec - else: - is_valid = len(str_q) <= context.prec - if not is_valid: + if not _is_leq_than_pow10a(q, context.prec): # assert q >= 10 ** context.prec # assert len(str(q)) > context.prec return context._raise_error(DivisionImpossible) From 9377ab6240bcc07e97ee122b1a35370a25cc1709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 26 Oct 2025 13:37:11 +0100 Subject: [PATCH 05/10] _pydecimal: add tests for unbounded contexts --- Lib/test/test_decimal.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 08a8f4c3b36bd6..84fa4ad4b99687 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -24,6 +24,7 @@ with the corresponding argument. """ +import contextlib import logging import math import os, sys @@ -4493,6 +4494,15 @@ def test_decimal_attributes(self): class Coverage: + @contextlib.contextmanager + def unbound_context(self, prec=None, Emax=None, Emin=None): + with self.decimal.localcontext() as c: + c.prec = self.decimal.MAX_PREC if prec is None else prec + c.Emax = self.decimal.MAX_EMAX if Emax is None else Emax + c.Emin = self.decimal.MIN_EMIN if Emin is None else Emin + c.traps[self.decimal.Inexact] = 1 + yield c + def test_adjusted(self): Decimal = self.decimal.Decimal @@ -4660,6 +4670,22 @@ def test_divmod(self): self.assertTrue(c.flags[InvalidOperation] and c.flags[DivisionByZero]) + def test_divide_unbound_context(self): + with self.unbound_context() as c: + x = self.decimal.Decimal('1') + y = x // 1 # should be fast + + def test_remainder_near(self): + L = 1000 + limit = sys.get_int_max_str_digits() + sys.set_int_max_str_digits(L) + self.addCleanup(sys.set_int_max_str_digits, limit) + + with self.unbound_context(prec=2 * L) as c: + self.assertEqual(c.prec, 2 * L) + x = self.decimal.Decimal(f'1e{L}') + y = x.remainder_near(1) # must not raise a ValueError + def test_power(self): Decimal = self.decimal.Decimal localcontext = self.decimal.localcontext From 23494fb540bcda70e720af71615adedd7c2844c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 7 Nov 2025 10:39:27 +0100 Subject: [PATCH 06/10] Apply suggestions from code review --- Lib/_pydecimal.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index 534b1b42e365fe..fc900adfb22b00 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -462,7 +462,7 @@ def _tento(n): return (5 ** n) << n -def _is_leq_than_pow10a_use_str(q, a): +def _is_less_than_pow10a_use_str(q, a): """Try to efficiently check len(str(q)) <= a, or equivalently q < 10**a. If it is not possible to efficiently compute len(str(q)), @@ -476,14 +476,14 @@ def _is_leq_than_pow10a_use_str(q, a): # log2(q) >= a*log2(10) >= a*z > q.bit_length(). # In particular, q > 2**q.bit_length(), which is impossible. - # assert q < 10 ** context.prec + # assert q < 10 ** a return True, None elif q.bit_length() >= 1 + a * _LOG_10_BASE_2_HI: # Claim: If z > log2(10) and q.bit_length() >= 1 + a*z, then q > 10**a. # Proof: Since q >= 2**(q.bit_length()-1), we have # q >= 2**(q.bit_length()-1) >= 2**(a*z) > 2**(a*log2(10)) = 10**a. - # assert q > 10 ** context.prec + # assert q > 10 ** a return False, None # Handles other cases due to floating point precision loss # when computing _LOG_10_BASE_2_LO and _LOG_10_BASE_2_HI. @@ -492,7 +492,7 @@ def _is_leq_than_pow10a_use_str(q, a): return is_valid, str_q -def _is_leq_than_pow10a(q, a, *, exact=True, ulp_order=20): +def _is_less_than_pow10a(q, a, *, exact=True, ulp_order=20): """Check that len(str(q)) <= a without computing str(q). When *exact* is false, computing len(str(q)) is replaced by f(q): @@ -505,7 +505,6 @@ def _is_leq_than_pow10a(q, a, *, exact=True, ulp_order=20): When *exact* is true, computing len(str(q)) requires one bigint exponentiation that only depends on q. """ - if q < 10: return a >= 1 @@ -1441,7 +1440,7 @@ def _divide(self, other, context): else: op2.int *= 10**(op2.exp - op1.exp) q, r = divmod(op1.int, op2.int) - is_valid, str_q = _is_leq_than_pow10a_use_str(q, context.prec) + is_valid, str_q = _is_less_than_pow10a_use_str(q, context.prec) if is_valid: if str_q is None: str_q = str(q) @@ -1602,7 +1601,7 @@ def remainder_near(self, other, context=None): r -= op2.int q += 1 - if not _is_leq_than_pow10a(q, context.prec): + if not _is_less_than_pow10a(q, context.prec): # assert q >= 10 ** context.prec # assert len(str(q)) > context.prec return context._raise_error(DivisionImpossible) From 6b55711a08cec60925cf208fb12742e6ac31fd62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 9 Nov 2025 14:42:22 +0100 Subject: [PATCH 07/10] address review --- Lib/_pydecimal.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index fc900adfb22b00..1867294f04aaa9 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -465,8 +465,9 @@ def _tento(n): def _is_less_than_pow10a_use_str(q, a): """Try to efficiently check len(str(q)) <= a, or equivalently q < 10**a. - If it is not possible to efficiently compute len(str(q)), - this explicitly compute str(q) instead. + If it is not possible to efficiently compute the comparison, str(q) is + explicitly computed. str(q) may also be computed for cases that cannot + be optimized. Return (len(str(q)) <= a, None) or (len(str(q)) <= a, str(q)). """ @@ -485,8 +486,12 @@ def _is_less_than_pow10a_use_str(q, a): # assert q > 10 ** a return False, None - # Handles other cases due to floating point precision loss - # when computing _LOG_10_BASE_2_LO and _LOG_10_BASE_2_HI. + # Handle cases that fail due to floating point precision loss + # when computing _LOG_10_BASE_2_LO and _LOG_10_BASE_2_HI, or + # that cannot be distinguished with (q.bit_length(), a) only. + # + # For instance, (q1, a) = (95, 2) and (q2, a) = (105, a) produce + # different results but q1.bit_length() == q2.bit_length() == 7. str_q = str(q) # can raise a ValueError is_valid = len(str_q) <= a return is_valid, str_q From 49d5b0d98da7c033842db01ce4c233cd5455282d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 9 Nov 2025 14:51:28 +0100 Subject: [PATCH 08/10] reformulate comment --- Lib/_pydecimal.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index 1867294f04aaa9..3dc87e553c88b2 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -465,9 +465,8 @@ def _tento(n): def _is_less_than_pow10a_use_str(q, a): """Try to efficiently check len(str(q)) <= a, or equivalently q < 10**a. - If it is not possible to efficiently compute the comparison, str(q) is - explicitly computed. str(q) may also be computed for cases that cannot - be optimized. + If the comparison cannot be obtained from q.bit_length(), + then str(q) is explicitly computed and may raise ValueError. Return (len(str(q)) <= a, None) or (len(str(q)) <= a, str(q)). """ From ea3e49945bf59b6b5ad20b8ff7d0e79cd1140642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 9 Nov 2025 18:14:51 +0100 Subject: [PATCH 09/10] Update Lib/_pydecimal.py Co-authored-by: Mikhail Efimov --- Lib/_pydecimal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index 3dc87e553c88b2..9fdecf68e89c5b 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -489,7 +489,7 @@ def _is_less_than_pow10a_use_str(q, a): # when computing _LOG_10_BASE_2_LO and _LOG_10_BASE_2_HI, or # that cannot be distinguished with (q.bit_length(), a) only. # - # For instance, (q1, a) = (95, 2) and (q2, a) = (105, a) produce + # For instance, (q1, a) = (95, 2) and (q2, a) = (105, 2) produce # different results but q1.bit_length() == q2.bit_length() == 7. str_q = str(q) # can raise a ValueError is_valid = len(str_q) <= a From b1c24b7e0778e1576b4374082e30e612d9b27ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 9 Nov 2025 18:30:36 +0100 Subject: [PATCH 10/10] add test for _is_less_than_pow10a_use_str() slow path --- Lib/test/test_decimal.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 84fa4ad4b99687..7eeb77dec78e0c 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -2612,6 +2612,37 @@ def tearDown(self): sys.set_int_max_str_digits(self._previous_int_limit) super().tearDown() + def test_helper__is_less_than_pow10a_use_str_slow_path(self): + # Test the "slow" path of _is_less_than_pow10a_use_str(). + a, b = 2, 7 + + # Choose q1, q2 such that len(str(q1)) <= a < len(str(q2)) + # and q1.bit_length() == q2.bit_length() == b to check that + # we cover the "slow" path correctly even for small values. + q1, q2 = 95, 105 + b1, b2 = q1.bit_length(), q2.bit_length() + + self.assertEqual(b1, b) + self.assertEqual(b2, b) + + # ensure that the first "fast" check doesn't hold + self.assertGreaterEqual(b, a * self.decimal._LOG_10_BASE_2_LO) + # ensure that the second "fast" check doesn't hold + self.assertLess(b, 1 + a * self.decimal._LOG_10_BASE_2_HI) + + cond_q1, str_q1 = self.decimal._is_less_than_pow10a_use_str(q1, a) + self.assertTrue(cond_q1) + self.assertIsNotNone(str_q1) + + cond_q2, str_q2 = self.decimal._is_less_than_pow10a_use_str(q2, a) + self.assertFalse(cond_q2) + self.assertIsNotNone(str_q2) + + def test_helper__is_less_than_pow10a(self): + # TODO(picnixz): find a simple test case with custom ulp_order. + pass + + class PythonAPItests: def test_abc(self):