Repository: mpmath/mpmath (python)

Issue:
hang in findroot
running the following code snippet will hang at the line of nsolve in the function _calc_abcd_with_sp.

```
from sympy import symbols, nsolve, sqrt

def _calc_abcd_with_sp(phi, abcd0):
    a0, b0, c0, d0 = abcd0
    N0 = sum(abcd0)
    S0 = b0 / d0
    T0 = c0 / d0

    a, b, c, d = symbols("a b c d", Positive=True, Real=True)

    eq1 = N0 - (a + b + c + d)
    eq2 = b - S0 * d
    eq3 = c - T0 * d
    eq4 = phi - (a * d - b * c) / sqrt((a + b) * (c + d) * (a + c) * (b + d))

    print(phi, abcd0, flush=True)
    try:
        solutions = nsolve(
            (eq1, eq2, eq3, eq4), (a, b, c, d), (a0, b0, c0, d0), maxsteps=2, tol=1e-6, verbose=True
        )
    except (
        ValueError,
        ZeroDivisionError,
    ):
        return ()
    # print(phi, abcd0, flush=True)

    a_, b_, c_, d_ = map(float, [sol[0] for sol in solutions.tolist()])

    if a_ < 0 or b_ < 0 or c_ < 0 or d_ < 0:
        return ()
    abcd = round_numbers((a_, b_, c_, d_))
    if not is_valid_abcd(abcd):
        return ()
    return abcd


def round_numbers(numbers):
    rounded = np.floor(numbers).astype(int)

    original_sum = sum(numbers)
    rounded_sum = sum(rounded)
    difference = round(original_sum - rounded_sum)

    for i in np.argsort([n - r for n, r in zip(numbers, rounded)])[::-1][:difference]:
        rounded[i] += 1
    return tuple(rounded.tolist())


def is_valid_abcd(abcd):
    a, b, c, d = abcd
    return a >= 0 and b >= 0 and c >= 0 and d > 0 and a + b > 0 and a + c > 0

phi = -0.44
abcd0 = (38, 34, 0, 49)
_calc_abcd_with_sp(phi, abcd0)
```

Pull request description:
Misc fixes
* properly support 'A' format option, amends 40c16e9
* optimize format_hexadecimal() like format_binary()
* don't use normalize() in format_* helpers
* enable trap_complex for MDNewton, closes #869 
* put coverage report to build/coverage/html/

Patch:
diff --git a/mpmath/calculus/optimization.py b/mpmath/calculus/optimization.py
index 51b5738..7f8c844 100644
--- a/mpmath/calculus/optimization.py
+++ b/mpmath/calculus/optimization.py
@@ -900,6 +900,7 @@ def findroot(ctx, f, x0, solver='secant', tol=None, verbose=False, verify=True,
 
     """
     prec = ctx.prec
+    trap_complex = getattr(ctx, 'trap_complex', None)
     try:
         ctx.prec += 20
 
@@ -948,6 +949,7 @@ def tmp(*args):
                 kwargs['norm'] = norm
             else:
                 norm = kwargs['norm']
+            ctx.trap_complex = True  # MDNewton assume real input
         else:
             norm = abs
 
@@ -985,6 +987,8 @@ def tmp(*args):
         return x
     finally:
         ctx.prec = prec
+        if trap_complex is not None:
+            ctx.trap_complex = trap_complex
 
 
 def multiplicity(ctx, f, root, tol=None, maxsteps=10, **kwargs):
diff --git a/mpmath/libmp/libmpf.py b/mpmath/libmp/libmpf.py
index 87a56ce..fe2d3c8 100644
--- a/mpmath/libmp/libmpf.py
+++ b/mpmath/libmp/libmpf.py
@@ -1623,49 +1623,40 @@ def format_hexadecimal(s,
                        capitalize=False,
                        alternate=False,
                        rounding=round_nearest):
-    sep = 'P' if capitalize else 'p'
-
-    if precision < 0:
-        precision = s[1].bit_length()//4 + 1
-
-    # First, get the exponent to know how many digits we will need
-    dps = precision+1
-    sign, digits, exponent = to_digits_exp(
-            s, max(dps+10, int(s[3]/4)+10), 16)
-    exponent *= 4
-
+    sign = '-' if s[0] else ''
     if sign != '-' and sign_spec != '-':
         sign = sign_spec
 
-    # normalization
-    if int(digits[0], 16) > 1:
-        shift = math.floor(math.log2(int(digits[0], 16)))
-        exponent += shift
-        n = int(digits, 16) >> shift
-        digits = hex(n)[2:]
+    prec = 4*precision + 1 if precision >= 0 else s[1].bit_length()
 
-    if digits != "0":
-        digits, exp_add = round_digits(s[0], digits, dps, 16, rounding)
-        exponent += exp_add*4
+    if s[1]:
+        s = mpf_pos(s, prec, rounding)
 
-    # normalization
-    if digits[0] == "2":
-        exponent += 1
-        n = int(digits, 16) >> 1
-        digits = hex(n)[2:]
+        exponent = s[2] + s[3] - 1
+        man = s[1] | (1 << s[3] + 2)  # set leading digit (ignored) to 0x9
+        man <<= 1 + 4*((s[3] + 3)//4) - s[3]
 
-    if s[1]:
-        # Clean up trailing zeros
-        digits = digits.rstrip('0')
-        precision = len(digits)
+        frac_digits = hex(man)[3:]
+        digits = "1"
+    else:
+        exponent = 0
+        frac_digits = ""
+        digits = "0"
 
-    if precision >= 1 and len(digits) > 1:
-        return sign, digits[0] + '.' + digits[1:] + sep + f'{exponent:+01d}'
+    if precision >= 0:
+        frac_digits = frac_digits[:precision]
+        frac_digits += "0"*(precision - len(frac_digits))
+    else:
+        # Clean up trailing zeros
+        frac_digits = frac_digits.rstrip('0')
 
-    if alternate:
-        return sign, digits + '.' + sep + f'{exponent:+01d}'
+    if frac_digits:
+        digits += "." + frac_digits
+    elif alternate:
+        digits += "."
+    digits += f'p{exponent:+01d}'
 
-    return sign, digits + sep + f'{exponent:+01d}'
+    return sign, digits.upper() if capitalize else digits
 
 
 def format_binary(s,
@@ -1677,7 +1668,7 @@ def format_binary(s,
         sign = sign_spec
 
     prec = precision + 1 if precision >= 0 else s[1].bit_length()
-    s = normalize(*s, prec, rounding)
+    s = mpf_pos(s, prec, rounding)
 
     digits = bin(s[1])[2:]
     digits = digits + '0'*(precision + 1 - len(digits))
diff --git a/pyproject.toml b/pyproject.toml
index e1176d2..b0496c4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -65,6 +65,8 @@ timeout = 300
 [tool.coverage.run]
 branch = true
 omit = ['mpmath/tests/*']
+[tool.coverage.html]
+directory = 'build/coverage/html'
 [tool.coverage.report]
 exclude_lines = ['pragma: no cover',
                  'raise NotImplementedError',


Test patch:
diff --git a/mpmath/tests/test_format.py b/mpmath/tests/test_format.py
index 7bc406c..9d96d48 100644
--- a/mpmath/tests/test_format.py
+++ b/mpmath/tests/test_format.py
@@ -868,10 +868,11 @@ def test_binary_fmt():
     assert f'{x:b}' == '0p+0'
 
 
-def test_hexadecimal():
+def test_hexadecimal_fmt():
     with workdps(1000):
         x = mp.mpf('1.234567890123456789')
         assert f'{x:.20a}' == '0x1.3c0ca428c59fb71a4194p+0'
+        assert f'{x:.20A}' == '0X1.3C0CA428C59FB71A4194P+0'
         assert f'{x:.0a}' == '0x1p+0'
         assert f'{x:#.0a}' == '0x1.p+0'
         assert f"{mp.mpf('1.234567890123456789'):+.0a}" == '+0x1p+0'
diff --git a/mpmath/tests/test_rootfinding.py b/mpmath/tests/test_rootfinding.py
index b92d98b..65efa58 100644
--- a/mpmath/tests/test_rootfinding.py
+++ b/mpmath/tests/test_rootfinding.py
@@ -99,3 +99,7 @@ def test_trivial():
     assert findroot(lambda x: 0, 1) == 1
     assert findroot(lambda x: x, 0) == 0
     #assert findroot(lambda x, y: x + y, (1, -1)) == (1, -1)
+
+def test_issue_869():
+    f = [lambda x: sqrt(x) + 1]
+    pytest.raises(mp.ComplexResult, lambda: findroot(f, [-1]))