Repository: mpmath/mpmath (python)

Issue:
Formatting mpf's is incompatible with Python if formatting type is missing
Documentation [says](https://docs.python.org/3/library/string.html#format-specification-mini-language): "For [float](https://docs.python.org/3/library/functions.html#float) this is the same as 'g', except that when fixed-point notation is used to format the result, it always includes at least one digit past the decimal point. The precision used is as large as needed to represent the given value faithfully."

While we have:
```pycon
>>> f'{mp.mpf(1.23456789):}'  # fixed precision=6
'1.23457'
>>> f'{mp.mpf(1):}'  # no trailing 0
'1'
```

Note, that it might be non-trivial to match exactly Python's output, because e.g. CPython and mpmath use different decimal approximations for some floats.  E.g.:
```pycon
>>> mp.mpf(0.3976135751111121)
mpf('0.39761357511111212')
>>> 0.3976135751111121
0.3976135751111121
>>> 0.39761357511111212
0.3976135751111121
```

Pull request description:
Fix formatting of mpf's when format type is missing
In this case it was incorrectly assumed, that this is an equivalent of 'g' formatting type.  It's not:
1) when no precision is specified - it should be equal to repr_deps(prec) to represent the given value faithfully.
2) when fixed-point is used to format the result, it always includes at least one digit past the decimal point.

Closes #856

Patch:
diff --git a/mpmath/libmp/libmpf.py b/mpmath/libmp/libmpf.py
index 09d24d0..48394fe 100644
--- a/mpmath/libmp/libmpf.py
+++ b/mpmath/libmp/libmpf.py
@@ -1450,9 +1450,9 @@ def read_format_spec(format_spec):
         'alternate': False,
         'thousands_separators': '',
         'width': -1,
-        'precision': 6,
+        'precision': -1,
         'rounding': round_nearest,
-        'type': 'g'
+        'type': ''
         }
 
     if match := _FLOAT_FORMAT_SPECIFICATION_MATCHER(format_spec):
@@ -1479,6 +1479,9 @@ def read_format_spec(format_spec):
         if match['zeropad']:
             format_dict['align'] = '='
             format_dict['fill_char'] = '0'
+
+        if format_dict['precision'] < 0 and format_dict['type']:
+            format_dict['precision'] = 6
     else:
         raise ValueError("Invalid format specifier '{}'".format(format_spec))
 
@@ -1623,10 +1626,11 @@ def format_scientific(s,
 def format_digits(num, format_dict, prec):
     hack0 = True
     capitalize = False
-    if format_dict['type'] in 'FGE':
+    if format_dict['type'] in list('FGE'):
         capitalize = True
 
     fmt_type = format_dict['type'].lower()
+
     percent = False
     if fmt_type == '%':
         percent = True
@@ -1644,18 +1648,26 @@ def format_digits(num, format_dict, prec):
 
     rounding = format_dict['rounding']
 
-    if fmt_type == 'g':
-        hack0 = False
+    if not fmt_type or fmt_type == 'g':
+        if fmt_type == 'g':
+            hack0 = False
         if not format_dict['alternate']:
-            strip_last_zero = True
             strip_zeros = True
+            if fmt_type == 'g':
+                strip_last_zero = True
 
-        _, _, exp = to_digits_exp(num, 53/blog2_10, 10)
+        if precision < 0:
+            precision = repr_dps(prec)
         if precision == 0:
             precision = 1
 
-        if -4 <= exp < precision:
-            fmt_type = 'f'
+        _, tdigits, exp = to_digits_exp(num, max(53/blog2_10, precision), 10)
+        if not fmt_type and num[1]:
+            _, exp_add = round_digits(num, tdigits, precision, 10, rounding)
+            exp += exp_add
+
+        fix0 = 0 if fmt_type else 1
+        if -4 <= exp < precision - fix0:
             precision = max(0, precision - exp - 1)
         else:
             fmt_type = 'e'
@@ -1670,34 +1682,40 @@ def format_digits(num, format_dict, prec):
         if percent:
             digits += '%'
 
-    elif fmt_type == 'f':
-        sign, digits = format_fixed(
+    elif fmt_type == 'e':
+        sign, digits = format_scientific(
                 num,
                 precision=precision,
                 strip_zeros=strip_zeros,
-                strip_last_zero=strip_last_zero,
-                thousands_separators=format_dict['thousands_separators'],
                 sign_spec=format_dict['sign'],
-                sep_range=3,
                 base=10,
+                capitalize=capitalize,
                 alternate=format_dict['alternate'],
-                no_neg_0=format_dict['no_neg_0'],
-                hack0=hack0,
                 rounding=rounding
                 )
-        if percent:
-            digits = digits + '%'
-    else:  # The format type is scientific
-        sign, digits = format_scientific(
+        if digits[0] in 'eE':
+            digits = '0' + digits
+
+    else:  # fixed-point format
+        sign, digits = format_fixed(
                 num,
                 precision=precision,
                 strip_zeros=strip_zeros,
+                strip_last_zero=strip_last_zero,
+                thousands_separators=format_dict['thousands_separators'],
                 sign_spec=format_dict['sign'],
+                sep_range=3,
                 base=10,
-                capitalize=capitalize,
                 alternate=format_dict['alternate'],
+                no_neg_0=format_dict['no_neg_0'],
+                hack0=hack0,
                 rounding=rounding
                 )
+        if not fmt_type:
+            if digits[-1] == '.':
+                digits += '0'
+        if percent:
+            digits = digits + '%'
 
     return sign, digits
 


Test patch:
diff --git a/mpmath/tests/test_format.py b/mpmath/tests/test_format.py
index de2b377..dfb6b1a 100644
--- a/mpmath/tests/test_format.py
+++ b/mpmath/tests/test_format.py
@@ -8,6 +8,7 @@
 from hypothesis import given, settings
 
 from mpmath import fp, inf, mp, nan, ninf, workdps
+from mpmath.libmp.libmpf import read_format_spec
 
 
 def random_fmt():
@@ -512,6 +513,10 @@ def test_mpf_fmt_cpython():
     assert f'{nan:%}' == 'nan%'
     assert f'{nan:+%}' == '+nan%'
 
+    # No formatting code.
+
+    assert f'{mp.mpf(0.0):.0}' == '0e+00'
+
 
 def test_mpf_float():
     '''
@@ -581,6 +586,26 @@ def test_mpf_floats_gG(fmt, x):
     assert format(x, fmt) == format(mp.mpf(x), fmt)
 
 
+@settings(max_examples=10000)
+@given(fmtstr(['']),
+       st.floats(allow_nan=True,
+                 allow_infinity=True,
+                 allow_subnormal=True))
+def test_mpf_floats_None(fmt, x):
+    if not x and math.copysign(1, x) == -1:
+        return  # skip negative zero
+    if (',' in fmt or '_' in fmt) and platform.python_implementation() == 'PyPy':
+        return  # see pypy/pypy#5018
+    spec = read_format_spec(fmt)
+    if spec['precision'] < 0 and math.isfinite(x):
+        # The mpmath could choose a different decimal
+        # representative (wrt CPython) for same binary
+        # floating-point number.
+        assert float(format(x)) == float(format(mp.mpf(x)))
+    else:
+        assert format(x, fmt) == format(mp.mpf(x), fmt)
+
+
 def test_mpf_fmt():
     '''
     These tests are either specific tests to mpf, or tests that cover
@@ -642,7 +667,7 @@ def test_mpf_fmt():
         assert f"{mp.mpf('1e-50'):.50g}" == '1e-50'
         assert f"{mp.mpf('1e-50'):.50G}" == '1E-50'
 
-        assert f"{mp.mpf('1e-50'):}" == '1e-50'
+        assert f"{mp.mpf('1e-51'):}" == '1e-51'
 
         # thousands separator
         assert f"{mp.mpf('1e9'):,.0f}" == '1,000,000,000'