Repository: mpmath/mpmath (python)

Issue:
Unsupported format string passed to mpf.__format__
```
$ python
Python 3.12.3 (main, Feb  4 2025, 14:48:35) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import mpmath
>>> mpmath.__version__
'1.3.0'
>>> val = mpmath.mpf('0.1')
>>> f"{val:.016f}"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported format string passed to mpf.__format__
>>> 
```
Can you fix this bug?

Pull request description:
Misc fixes
* Fix exception type for ``int(mp.inf)`` to ``OverflowError``
* Fix ``mpf.__round__()`` to return int type per default (``ndigits=None``)
* Revert "Revert 849e185e and (partially) f4cef9c8 (same for with field)", closes #915 
* Clarify deprecation of direct access to `_mpf_` tuples
* Amend 4582b137


Patch:
diff --git a/CHANGES b/CHANGES
index 736c51f..2dedde7 100644
--- a/CHANGES
+++ b/CHANGES
@@ -30,8 +30,8 @@ Features:
 * Add signed option to to_man_exp(), see #783 (Sergey B Kirpichev)
 * Add fp.hypot, see #798 (Sergey B Kirpichev)
 * Support inf/nan's in ctx.almosteq(), #802 (Sergey B Kirpichev)
-* Implement mpf.__format__(), see #819, #831, #850, #859, #857, #862, #881 and
-  #944 (Javier Garcia, Sergey B Kirpichev)
+* Implement mpf.__format__(), see #819, #831, #850, #859, #857, #862, #881,
+  #944 and #966 (Javier Garcia, Sergey B Kirpichev)
 * Support conversion from scalar ndarray's, see #821 (Sergey B Kirpichev)
 * Support rounding modes in mpf.__format__, see #823, #831 and #834 (Javier
   Garcia, Sergey B Kirpichev)
@@ -40,7 +40,7 @@ Features:
 * Implement 'a'/'A' formating types for mpf.__format__, see #841 and #870
   (Sergey B Kirpichev)
 * Add mpc.__format__(), see #855 (Sergey B Kirpichev)
-* Now mpf.__round__() returns mpf, see #826 (Sergey B Kirpichev)
+* Now mpf.__round__() returns mpf, see #826 and #966 (Sergey B Kirpichev)
 * Support 'b' (binary) format type for mpf/mpc, see #867 (Sergey B Kirpichev)
 * Implement mpf.__floordiv__() and mpf.__divmod__(), see #873 (Sergey B
   Kirpichev)
@@ -70,7 +70,8 @@ Compatibility:
 * Drop to_pickable()/from_pickable() helpers, see #667 and #769 (Sergey B
   Kirpichev)
 * Direct access to _mpf_ tuples now deprecated, please use from/to_man_exp()
-  functions, see #783 (Sergey B Kirpichev)
+  functions and special constants (finf, fninf and fnan), see
+  #783 (Sergey B Kirpichev)
 * Removed sage backend, see #732 (Sergey B Kirpichev)
 * Drop MPMATH_STRICT environment variable, see #759 (Sergey B Kirpichev)
 * Deprecate current (descending) order of coefficients in polyval(), etc, see
@@ -152,6 +153,7 @@ Bug fixes:
 * Use mpf_log1p in acos_asin() helper (implementing Hull et al algorithm), see
   #948 (Sergey B Kirpichev)
 * Fix kwargs passing in the nstr() for mpc, see #964 (David Walker)
+* Fix exception type for int(inf), see #966 (Sergey B Kirpichev)
 
 Maintenance:
 
diff --git a/mpmath/ctx_mp_python.py b/mpmath/ctx_mp_python.py
index 255322a..8376658 100644
--- a/mpmath/ctx_mp_python.py
+++ b/mpmath/ctx_mp_python.py
@@ -130,14 +130,14 @@ def as_integer_ratio(self):
     def __reduce__(self): return _make_mpf, (self._mpf_,)
 
     def __repr__(s):
-        rounding = s.context._rounding
+        rounding = s.context._prec_rounding[1]
         if s.context.pretty:
             ndigits = (s.context._repr_digits
                        if s.context._pretty_repr_dps else s.context._str_digits)
             return to_str(s._mpf_, ndigits, rounding=rounding)
         return "mpf('%s')" % to_str(s._mpf_, s.context._repr_digits, rounding=rounding)
 
-    def __str__(s): return to_str(s._mpf_, s.context._str_digits, rounding=s.context._rounding)
+    def __str__(s): return to_str(s._mpf_, s.context._str_digits, rounding=s.context._prec_rounding[1])
     def __hash__(s): return mpf_hash(s._mpf_)
     def __int__(s): return int(to_int(s._mpf_))
     def __float__(s): return to_float(s._mpf_, rnd=s.context._prec_rounding[1])
@@ -441,13 +441,17 @@ def ae(s, t, rel_eps=None, abs_eps=None):
     def to_fixed(self, prec):
         return to_fixed(self._mpf_, prec)
 
-    def __round__(self, ndigits=0):
+    def __round__(self, ndigits=None):
         ctx = self.context
         if ctx.isfinite(self):
             frac = MPQ(*self.as_integer_ratio())
             res = round(frac, ndigits)
-            return ctx.convert(res)
-        return self
+            res = ctx.convert(res)
+        else:
+            res = self
+        if ndigits is None:
+            res = int(res)
+        return res
 
 
 class _constant(_mpf):
@@ -731,7 +735,6 @@ def make_mpc(ctx, v):
 
     def default(ctx):
         ctx._prec = ctx._prec_rounding[0] = sys.float_info.mant_dig
-        ctx._rounding = ctx._prec_rounding[1]
         ctx._dps = sys.float_info.dig
         ctx.trap_complex = False
 
@@ -746,13 +749,12 @@ def _set_dps(ctx, n):
     def _set_rounding(ctx, r):
         try:
             ctx._prec_rounding[1] = ctx._parse_prec({'rounding': r})[1]
-            ctx._rounding = ctx._prec_rounding[1]
         except KeyError:
             raise ValueError('invalid rounding mode')
 
     prec = property(lambda ctx: ctx._prec, _set_prec)
     dps = property(lambda ctx: ctx._dps, _set_dps)
-    rounding = property(lambda ctx: ctx._rounding, _set_rounding)
+    rounding = property(lambda ctx: ctx._prec_rounding[1], _set_rounding)
 
     def _set_pretty_dps(ctx, v):
         ctx._pretty_repr_dps = True if v == 'repr' else False
diff --git a/mpmath/libmp/libmpf.py b/mpmath/libmp/libmpf.py
index b1f5f0a..432ae94 100644
--- a/mpmath/libmp/libmpf.py
+++ b/mpmath/libmp/libmpf.py
@@ -267,7 +267,9 @@ def to_int(s, rnd=round_fast):
     input is inf/nan, an exception is raised."""
     sign, man, exp, bc = s
     if (not man) and exp:
-        raise ValueError("cannot convert inf or nan to int")
+        if s == fnan:
+            raise ValueError("cannot convert nan to int")
+        raise OverflowError("cannot convert infinity to int")
     if exp >= 0:
         if sign:
             return (-man) << exp
@@ -1397,12 +1399,12 @@ def from_str(x, prec=0, rnd=round_fast, base=0):
     (?P<sign>[-+ ]?)
     (?P<no_neg_0>z)?
     (?P<alternate>\#)?
-    (?P<zeropad>0(?=[0-9]))?
-    (?P<width>0|[1-9][0-9]*)?
+    (?P<zeropad>0(?=0*[1-9]))?
+    (?P<width>[0-9]+)?
     (?P<thousands_separators>[,_])?
     (?:\.
         (?=[,_0-9])  # lookahead for digit or separator
-        (?P<precision>0|[1-9][0-9]*)?
+        (?P<precision>[0-9]+)?
         (?P<frac_separators>[,_])?
     )?
     (?P<rounding>[UDYZN])?
@@ -1478,22 +1480,17 @@ def read_format_spec(format_spec):
             format_dict['rounding'] = _GMPY_ROUND_CHAR_DICT[rounding_char]
 
         if match['zeropad']:
-            if match['fill_char']:
-                raise ValueError("Fill character conflicts with '0'"
-                                 f" in format specifier: '{format_spec}'")
-            if match['align']:
-                raise ValueError("Alignment conflicts with '0'"
-                                 f" in format specifier: '{format_spec}'")
-            format_dict['align'] = '='
-            format_dict['fill_char'] = '0'
-
-        if format_dict['precision'] < 0 and (format_dict['type'].lower()
-                                             not in ['', 'a', 'b']):
-            format_dict['precision'] = 6
+            if not match['align']:
+                format_dict['align'] = '='
+            if not match['fill_char']:
+                format_dict['fill_char'] = '0'
 
-        return format_dict
+        if format_dict['precision'] < 0 and format_dict['type'].lower() not in ['', 'a', 'b']:
+            format_dict['precision'] = 6
+    else:
+        raise ValueError("Invalid format specifier '{}'".format(format_spec))
 
-    raise ValueError(f"Invalid format specifier '{format_spec}'")
+    return format_dict
 
 
 def format_fixed(s, precision=6, rounding=round_nearest):


Test patch:
diff --git a/mpmath/tests/test_basic_ops.py b/mpmath/tests/test_basic_ops.py
index ca91476..1409e23 100644
--- a/mpmath/tests/test_basic_ops.py
+++ b/mpmath/tests/test_basic_ops.py
@@ -665,6 +665,16 @@ def test_round_bulk(x, n):
             return
         assert nstr(mr, n=14, base=16, strip_zeros=False,
                     show_zero_exponent=True, binary_exp=True) == xr.hex()
+    try:
+        xr = round(x)
+    except ValueError:
+        pytest.raises(ValueError, lambda: round(m))
+    except OverflowError:
+        pytest.raises(OverflowError, lambda: round(m))
+    else:
+        mr = round(m)
+        assert type(mr) is int
+        assert mr == xr
 
 
 def test_rounding_prop():
diff --git a/mpmath/tests/test_format.py b/mpmath/tests/test_format.py
index f07ae6a..22822d6 100644
--- a/mpmath/tests/test_format.py
+++ b/mpmath/tests/test_format.py
@@ -57,7 +57,9 @@ def fmt_str(draw, types='fFeE', for_complex=False):
         skip_thousand_separators = True
 
     # Width
-    res += draw(st.sampled_from(['']*7 + list(map(str, range(1, 40)))))
+    res += draw(st.sampled_from(['']*7 + list(map(str, range(1, 40)))
+                                + ([] if for_complex else ['0' + str(_)
+                                                           for _ in range(40)])))
 
     # grouping character (thousand_separators)
     gchar = draw(st.sampled_from([''] + list(',_')))
@@ -65,7 +67,8 @@ def fmt_str(draw, types='fFeE', for_complex=False):
         res += gchar
 
     # Precision
-    prec = draw(st.sampled_from(['']*7 + list(map(str, range(40)))))
+    prec = draw(st.sampled_from(['']*7 + list(map(str, range(40)))
+                + ['0' + str(_) for _ in range(40)]))
     if prec:
         res += '.' + prec
         if vinfo >= (3, 14):
@@ -481,6 +484,8 @@ def test_mpf_fmt_cpython():
                  allow_infinity=True,
                  allow_subnormal=True))
 @example(fmt='.0g', x=9.995074823339339e-05)  # issue 880
+@example(fmt='.016f', x=0.1)  # issue 915
+@example(fmt='0030f', x=0.3)
 @example(fmt='0=13,f', x=1.1)  # issue 917
 @example(fmt='013,f', x=1.1)
 @example(fmt='013,.0%', x=1.1)
@@ -628,8 +633,8 @@ def test_mpf_fmt():
 
         # Tests for = alignment
         assert f"{mp.mpf('0.24'):=+20.2f}" == '+               0.24'
-        assert f"{mp.mpf('0.24'):0=+20.2e}" == '+000000000002.40e-01'
-        assert f"{mp.mpf('0.24'):0=+20.2g}" == '+0000000000000000.24'
+        assert f"{mp.mpf('0.24'):=+020.2e}" == '+000000000002.40e-01'
+        assert f"{mp.mpf('0.24'):=+020.2g}" == '+0000000000000000.24'
 
         # Tests for different kinds of rounding
         num = mp.mpf('-1.23456789999901234567')
@@ -826,13 +831,6 @@ def test_errors():
     with pytest.raises(ValueError, match="Invalid format specifier '12.3 E '"):
         f"{mp.mpf('4'):12.3 E }"
 
-    with pytest.raises(ValueError, match="Fill character conflicts"):
-        f"{mp.mpf('4'):q<03f}"
-    with pytest.raises(ValueError, match="Alignment conflicts"):
-        f"{mp.mpf('4'):=03f}"
-    with pytest.raises(ValueError, match="Fill character conflicts"):
-        f"{mp.mpf('4'): =03f}"
-
     with pytest.raises(ValueError):
         f"{mp.mpf(1):.f}"
     with pytest.raises(ValueError):