Repository: mpmath/mpmath (python)

Issue:
str and repr with pretty=True
Currently we have following implementations:
https://github.com/mpmath/mpmath/blob/23b5d390bc48ea240828a55e1b66e36f722e797a/mpmath/ctx_mp_python.py#L131-L136
`_repr_digits` digits - enough to ensure that eval-repr round-trip will work.  But that's not guarantied for `str()` output, here is an example:
```pycon
>>> from mpmath import *
>>> mp.prec=128
>>> r = mpf('0.5269770260116541560356928630345245874084'); r
mpf('0.5269770260116541560356928630345245874084')
>>> str(r)
'0.52697702601165415603569286303452458741'
>>> eval(repr(r)) == r
True
>>> mpf(str(r)) == r
False
```
Unfortunately, we use `str()`-like output for `repr()` too, if `mp.pretty` is enabled.  That's especially odd in the CLI interface:
```
$ python -m mpmath --no-ipython --prec 128 --pretty
>>> 0.5269770260116541560356928630345245874084
0.52697702601165415603569286303452458741
>>> 0.52697702601165415603569286303452458741
0.52697702601165415603569286303452458741
```

I suggest treat this as a bug and change `repr()` to always use `_repr_digits` digits.  You should be able to recreate value exactly from the `repr()` output.

Unfortunately, that breaks a lot of doctests (but almost no tests, only some in `test_cli.py`).  Another option might be a new context flag to control this behavior.

BTW, we also might think about `repr()` vs `str()` difference, do we need this at all?  E.g. neither CPython nor gmpy2 have different conventions for `repr()` and `str()` (except for printing `mpfr` prefix in the gmpy2).  Instead, since 3.1 the CPython uses a shortest decimal representative for the given number.  Maybe we should do same?  CPython issue: https://github.com/python/cpython/issues/45921

Pull request:
Add pretty_dps context property to control number of printed digits
Closes #921

Patch:
diff --git a/docs/basics.rst b/docs/basics.rst
index 6f3e343..e41f5fe 100644
--- a/docs/basics.rst
+++ b/docs/basics.rst
@@ -221,6 +221,8 @@ Setting the ``mp.pretty`` option will use the ``str()``-style output for ``repr(
     >>> mpf(0.6)
     mpf('0.59999999999999998')
 
+To use enough digits to be able recreate value exactly, set ``mp.pretty_dps`` to ``"repr"``.
+
 The number of digits with which numbers are printed by default is determined by
 the working precision.  To specify the number of digits to show without
 changing the working precision, use :func:`format syntax support
diff --git a/mpmath/__main__.py b/mpmath/__main__.py
index a0506ee..4a512db 100644
--- a/mpmath/__main__.py
+++ b/mpmath/__main__.py
@@ -57,6 +57,7 @@ def main():
         lines.append(f'mp.prec = {args.prec}')
     if not args.no_pretty:
         lines.append('mp.pretty = True')
+        lines.append('mp.pretty_dps = "repr"')
 
     try:
         import IPython
diff --git a/mpmath/ctx_mp_python.py b/mpmath/ctx_mp_python.py
index 8da8856..feb6765 100644
--- a/mpmath/ctx_mp_python.py
+++ b/mpmath/ctx_mp_python.py
@@ -130,7 +130,9 @@ def __reduce__(self): return _make_mpf, (self._mpf_,)
 
     def __repr__(s):
         if s.context.pretty:
-            return str(s)
+            ndigits = (s.context._repr_digits
+                       if s.context._pretty_repr_dps else s.context._str_digits)
+            return to_str(s._mpf_, ndigits)
         return "mpf('%s')" % to_str(s._mpf_, s.context._repr_digits)
 
     def __str__(s): return to_str(s._mpf_, s.context._str_digits)
@@ -514,7 +516,9 @@ def __reduce__(self): return _make_mpc, self._mpc_
 
     def __repr__(s):
         if s.context.pretty:
-            return str(s)
+            ndigits = (s.context._repr_digits
+                       if s.context._pretty_repr_dps else s.context._str_digits)
+            return "(%s)" % mpc_to_str(s._mpc_, ndigits)
         r = repr(s.real)[4:-1]
         i = repr(s.imag)[4:-1]
         return "%s(real=%s, imag=%s)" % (type(s).__name__, r, i)
@@ -709,6 +713,7 @@ def __init__(ctx):
         ctx.constant = type('constant', (_constant,), {})
         ctx.constant._ctxdata = [ctx.mpf, new, ctx._prec_rounding]
         ctx.constant.context = ctx
+        ctx._pretty_repr_dps = False
 
     def make_mpf(ctx, v):
         a = new(ctx.mpf)
@@ -736,6 +741,14 @@ def _set_dps(ctx, n):
     prec = property(lambda ctx: ctx._prec, _set_prec)
     dps = property(lambda ctx: ctx._dps, _set_dps)
 
+    def _set_pretty_dps(ctx, v):
+        ctx._pretty_repr_dps = True if v == 'repr' else False
+
+    def _get_pretty_dps(ctx):
+        return 'repr' if ctx._pretty_repr_dps else 'str'
+
+    pretty_dps = property(_get_pretty_dps, _set_pretty_dps)
+
     def convert(ctx, x, strings=True):
         """
         Converts *x* to an ``mpf`` or ``mpc``. If *x* is of type ``mpf``,


Test patch:
diff --git a/mpmath/tests/test_cli.py b/mpmath/tests/test_cli.py
index 2d7a8eb..8b3ac8e 100644
--- a/mpmath/tests/test_cli.py
+++ b/mpmath/tests/test_cli.py
@@ -111,7 +111,7 @@ def test_bare_console_pretty():
 
     assert c.expect_exact('>>> ') == 0
     assert c.send("10.9\r\n") == 6
-    assert c.expect_exact("10.9\r\n>>> ") == 0
+    assert c.expect_exact("10.899999999999999999999999999995\r\n>>> ") == 0
     assert c.send("def f():\r\n  x = ?\r\n\r\n") == 21
     assert c.expect('SyntaxError:') == 0