Repository: mpmath/mpmath
Pull request #783 (2024-04-10 08:04:38)

Misc fixes
* don't unpack mpf tuples directly in ctx_mp*.py
* add signed option to to_man_exp()

skirpichev: Bugfix for tan/tanh moved to #785

Patch:
diff --git a/mpmath/ctx_mp.py b/mpmath/ctx_mp.py
index d382a02..ee05350 100644
--- a/mpmath/ctx_mp.py
+++ b/mpmath/ctx_mp.py
@@ -16,7 +16,7 @@
                     mpf_apery, mpf_catalan, mpf_degree, mpf_div, mpf_e,
                     mpf_euler, mpf_glaisher, mpf_khinchin, mpf_ln2, mpf_ln10,
                     mpf_mertens, mpf_mul, mpf_neg, mpf_phi, mpf_pi, mpf_rand,
-                    mpf_sub, mpf_twinprime, repr_dps, to_str)
+                    mpf_sub, mpf_twinprime, repr_dps, to_man_exp, to_str)
 
 
 get_complex = re.compile(r"""
@@ -337,8 +337,10 @@ def isnpint(ctx, x):
         if not x:
             return True
         if hasattr(x, '_mpf_'):
-            sign, man, exp, bc = x._mpf_
-            return sign and exp >= 0
+            if ctx.isfinite(x):
+                man, exp = to_man_exp(x._mpf_, signed=True)
+                return man < 0 and exp >= 0
+            return False
         if hasattr(x, '_mpc_'):
             return not x.imag and ctx.isnpint(x.real)
         if type(x) in int_types:
@@ -1113,26 +1115,26 @@ def nint_distance(ctx, x):
             im_dist = ctx.ninf
         elif hasattr(x, "_mpc_"):
             re, im = x._mpc_
-            isign, iman, iexp, ibc = im
+            iman, iexp = to_man_exp(im, signed=True)
             if iman:
-                im_dist = iexp + ibc
-            elif im == fzero:
-                im_dist = ctx.ninf
+                im_dist = iexp + iman.bit_length()
             else:
-                raise ValueError("requires a finite number")
+                im_dist = ctx.ninf
         else:
             x = ctx.convert(x)
             if hasattr(x, "_mpf_") or hasattr(x, "_mpc_"):
                 return ctx.nint_distance(x)
             else:
                 raise TypeError("requires an mpf/mpc")
-        sign, man, exp, bc = re
-        mag = exp+bc
+        man, exp = to_man_exp(re, signed=True)
+        mag = exp+man.bit_length()
         # |x| < 0.5
         if mag < 0:
             n = 0
             re_dist = mag
         elif man:
+            sign = man < 0
+            man = abs(man)
             # exact integer
             if exp >= 0:
                 n = man << exp
@@ -1153,11 +1155,9 @@ def nint_distance(ctx, x):
                 re_dist = exp+man.bit_length()
             if sign:
                 n = -n
-        elif re == fzero:
+        else:
             re_dist = ctx.ninf
             n = 0
-        else:
-            raise ValueError("requires a finite number")
         return n, max(re_dist, im_dist)
 
     def fprod(ctx, factors):
diff --git a/mpmath/ctx_mp_python.py b/mpmath/ctx_mp_python.py
index 4fdcbb7..3738d67 100644
--- a/mpmath/ctx_mp_python.py
+++ b/mpmath/ctx_mp_python.py
@@ -116,10 +116,10 @@ def mpf_convert_lhs(cls, x):
             return cls.context.make_mpf(x)
         return x
 
-    man_exp = property(lambda self: to_man_exp(self._mpf_))
-    man = property(lambda self: self._mpf_[1])
-    exp = property(lambda self: self._mpf_[2])
-    bc = property(lambda self: self._mpf_[3])
+    man_exp = property(lambda self: to_man_exp(self._mpf_, signed=False))
+    man = property(lambda self: self.man_exp[0])
+    exp = property(lambda self: self.man_exp[1])
+    bc = property(lambda self: self.man.bit_length())
 
     real = property(lambda self: self)
     imag = property(lambda self: self.context.zero)
@@ -897,7 +897,9 @@ def isnormal(ctx, x):
             False
         """
         if hasattr(x, "_mpf_"):
-            return bool(x._mpf_[1])
+            if ctx.isfinite(x):
+                return bool(to_man_exp(x._mpf_, signed=True)[0])
+            return False
         if hasattr(x, "_mpc_"):
             re, im = x._mpc_
             re_normal = bool(re[1])
@@ -938,15 +940,20 @@ def isint(ctx, x, gaussian=False):
         if isinstance(x, int_types):
             return True
         if hasattr(x, "_mpf_"):
-            sign, man, exp, bc = xval = x._mpf_
-            return bool((man and exp >= 0) or xval == fzero)
+            if ctx.isfinite(x):
+                man, exp = to_man_exp(x._mpf_, signed=True)
+                return bool((man and exp >= 0) or x._mpf_ == fzero)
+            return False
         if hasattr(x, "_mpc_"):
             re, im = x._mpc_
-            rsign, rman, rexp, rbc = re
-            isign, iman, iexp, ibc = im
-            re_isint = (rman and rexp >= 0) or re == fzero
+            if ctx.isfinite(x):
+                man, exp = to_man_exp(re, signed=True)
+                re_isint = bool((man and exp >= 0) or re == fzero)
+                man, exp = to_man_exp(im, signed=True)
+                im_isint = bool((man and exp >= 0) or im == fzero)
+            else:
+                return False
             if gaussian:
-                im_isint = (iman and iexp >= 0) or im == fzero
                 return re_isint and im_isint
             return re_isint and im == fzero
         if isinstance(x, MPQ):
@@ -1183,11 +1190,9 @@ def _convert_param(ctx, x):
                 v = x._mpf_
             else:
                 raise NotImplementedError
-        sign, man, exp, bc = v
+        man, exp = to_man_exp(v, signed=True)
         if man:
             if exp >= -4:
-                if sign:
-                    man = -man
                 if exp >= 0:
                     return int(man) << exp, 'Z'
                 p, q = int(man), (1<<(-exp))
@@ -1199,14 +1204,12 @@ def _convert_param(ctx, x):
         raise NotImplementedError
 
     def _mpf_mag(ctx, x):
-        sign, man, exp, bc = x
-        if man:
-            return exp+bc
         if x == fzero:
             return ctx.ninf
-        if x == finf or x == fninf:
-            return ctx.inf
-        return ctx.nan
+        if x in (finf, fninf, fnan):
+            return ctx.make_mpf(mpf_abs(x))
+        man, exp = to_man_exp(x, signed=True)
+        return exp+man.bit_length()
 
     def mag(ctx, x):
         """
diff --git a/mpmath/libmp/libmpf.py b/mpmath/libmp/libmpf.py
index 04a12a6..2d3faf9 100644
--- a/mpmath/libmp/libmpf.py
+++ b/mpmath/libmp/libmpf.py
@@ -4,6 +4,7 @@
 
 import math
 import sys
+import warnings
 
 
 # Importing random is slow
@@ -235,11 +236,20 @@ def from_int(n, prec=0, rnd=round_fast):
             return int_cache[n]
     return from_man_exp(n, 0, prec, rnd)
 
-def to_man_exp(s):
+def to_man_exp(s, signed=None):
     """Return (man, exp) of a raw mpf. Raise an error if inf/nan."""
+    if signed is None:
+        warnings.warn("Returning unsigned mantissa value per default "
+                      "is deprecated.  Please adapt your code to use "
+                      "signed=True (return a signed mantissa).",
+                      DeprecationWarning)
+        signed = False
     sign, man, exp, bc = s
     if (not man) and exp:
-        raise ValueError("mantissa and exponent are undefined for %s" % man)
+        raise ValueError("mantissa and exponent are defined "
+                         "for finite numbers only")
+    if signed and sign:
+        man = -man
     return man, exp
 
 def to_int(s, rnd=round_fast):


Test patch:
diff --git a/mpmath/tests/test_basic_ops.py b/mpmath/tests/test_basic_ops.py
index c5675cd..d66764e 100644
--- a/mpmath/tests/test_basic_ops.py
+++ b/mpmath/tests/test_basic_ops.py
@@ -7,9 +7,10 @@
 from mpmath import (ceil, fadd, fdiv, floor, fmul, fneg, fp, frac, fsub, inf,
                     isinf, isint, isnan, isnormal, iv, monitor, mp, mpc, mpf,
                     mpi, nan, ninf, nint, nint_distance, pi, workprec)
-from mpmath.libmp import (MPQ, finf, fnan, fninf, fone, from_float, from_int,
-                          from_str, mpf_add, mpf_mul, mpf_sub, round_down,
-                          round_nearest, round_up, to_int)
+from mpmath.libmp import (MPQ, finf, fnan, fninf, fnone, fone, from_float,
+                          from_int, from_str, mpf_add, mpf_mul, mpf_sub,
+                          round_down, round_nearest, round_up, to_int,
+                          to_man_exp)
 
 
 def test_type_compare():
@@ -332,6 +333,8 @@ def test_nint_distance():
     assert nint_distance(mpf(0)) == (0, -inf)
     assert nint_distance(mpf(0.01)) == (0, -6)
     assert nint_distance(mpf('1e-100')) == (0, -332)
+    pytest.raises(ValueError, lambda: nint_distance(mpc(1, inf)))
+    pytest.raises(ValueError, lambda: nint_distance(mpc(inf, 1)))
 
 def test_floor_ceil_nint_frac():
     for n in range(-10,10):
@@ -514,6 +517,7 @@ def test_isnan_etc():
     assert mp.isnpint(-1.1 + 0j) is False
     assert mp.isnpint(-1 + 0.1j) is False
     assert mp.isnpint(0 + 0.1j) is False
+    assert mp.isnpint(inf) is False
 
 
 def test_issue_438():
@@ -532,6 +536,10 @@ def test_ctx_mp_mpnumeric():
     with pytest.deprecated_call():
         from mpmath.ctx_mp import mpnumeric
 
+def test_to_man_exp_deprecation():
+    with pytest.deprecated_call():
+        to_man_exp(fnone)
+
 def test_rand_precision():
     """
     Test precision of rand()