Run mpmath as a module for interactive work

Example:
```
$ python -m mpmath --prec 100 --no-ipython
>>> 10.9
mpf('10.899999999999999999999999999995')
>>> mpf(1090/100)
mpf('10.899999999999999999999999999995')
>>> mpf('10.9')
mpf('10.899999999999999999999999999995')
>>> mpf(109)/mpf(10)
mpf('10.899999999999999999999999999995')
>>> mpf(10.9)
mpf('10.899999999999999999999999999995')
```

Closes #677
Closes #765
This commit is contained in:
Sergey B Kirpichev
2024-04-02 18:54:41 +03:00
parent 18fbda65e0
commit 5f724296dd
9 changed files with 335 additions and 4 deletions
+4
View File
@@ -39,6 +39,8 @@ jobs:
- name: Remove gmpy (for coverage tests)
if: matrix.nogmpy
run: pip uninstall -y gmpy2
- name: Install ~/.python_history
run: touch ~/.python_history
- name: Linting with flake8, etc
if: matrix.default
run: |
@@ -53,6 +55,8 @@ jobs:
if: matrix.coverage
run: |
pytest
pip uninstall -y ipython
pytest mpmath/tests/test_cli.py
coverage html
coverage xml
- name: Upload coverage data
+8
View File
@@ -0,0 +1,8 @@
.. _cli:
Command-Line Usage
==================
When called as a program from the command line, the following form is used:
.. autoprogram:: mpmath.__main__:parser
+1 -1
View File
@@ -14,7 +14,7 @@ import mpmath
# Add any Sphinx extension module names here, as strings.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.mathjax',
'sphinx.ext.intersphinx',
'sphinx.ext.intersphinx', 'sphinxcontrib.autoprogram',
'matplotlib.sphinxext.plot_directive']
# Sphinx will warn about all references where the target cannot be found.
+1
View File
@@ -28,6 +28,7 @@ Basic features
contexts
general
plotting
cli
Advanced mathematics
--------------------
+7
View File
@@ -74,6 +74,13 @@ Python interpreter and do the following::
>>> print(2*pi)
6.2831853071795864769252867665590057683943387987502
.. tip::
:ref:`Run mpmath as a module <cli>` for interactive work::
python -m mpmath
Using gmpy (optional)
---------------------
+145
View File
@@ -0,0 +1,145 @@
"""
Python shell for mpmath.
This is just a normal Python shell (IPython shell if you have the
IPython package installed), that adds default imports and run
some initialization code.
"""
import argparse
import ast
import atexit
import code
import os
import readline
import rlcompleter
import sys
from mpmath import __version__
from mpmath._interactive import (IntegerDivisionWrapper,
wrap_float_literals)
__all__ = ()
parser = argparse.ArgumentParser(description=__doc__,
prog='python -m mpmath')
parser.add_argument('--no-wrap-division',
help="Don't wrap integer divisions with Fraction",
action='store_true')
parser.add_argument('--no-ipython', help="Don't use IPython",
action='store_true')
parser.add_argument('--no-wrap-floats',
help="Don't wrap float/complex literals",
action='store_true')
parser.add_argument('-V', '--version',
help='Print the mpmath version and exit',
action='store_true')
parser.add_argument('--prec', type=int,
help='Set default mpmath precision')
parser.add_argument('--pretty', help='Enable pretty-printing',
action='store_true')
def main():
args, ipython_args = parser.parse_known_args()
if args.version:
print(__version__)
sys.exit(0)
lines = ['from mpmath import *',
'from fractions import Fraction']
if args.prec:
lines.append(f'mp.prec = {args.prec}')
if args.pretty:
lines.append('mp.pretty = True')
try:
import IPython
import traitlets
except ImportError:
args.no_ipython = True
if not args.no_ipython:
config = traitlets.config.loader.Config()
shell = config.InteractiveShell
ast_transformers = shell.ast_transformers
if not args.no_wrap_division:
ast_transformers.append(IntegerDivisionWrapper())
shell.confirm_exit = False
config.TerminalIPythonApp.display_banner = False
config.TerminalInteractiveShell.autoformatter = None
app = IPython.terminal.ipapp.TerminalIPythonApp.instance(config=config)
app.initialize(ipython_args)
shell = app.shell
for l in lines:
shell.run_cell(l, silent=True)
if not args.no_wrap_floats:
shell.run_cell('from mpmath._interactive import wrap_float_literals')
shell.run_cell('ip = get_ipython()')
shell.run_cell('ip.input_transformers_post.append(wrap_float_literals)')
shell.run_cell('del ip')
app.start()
else:
ast_transformers = []
source_transformers = []
ns = {}
if not args.no_wrap_division:
ast_transformers.append(IntegerDivisionWrapper())
if not args.no_wrap_floats:
source_transformers.append(wrap_float_literals)
class MpmathConsole(code.InteractiveConsole):
"""An interactive console with readline support."""
def __init__(self, ast_transformers=[],
source_transformers=[], **kwargs):
super().__init__(**kwargs)
readline.set_completer(rlcompleter.Completer(ns).complete)
readline.parse_and_bind('tab: complete')
history = os.path.expanduser('~/.python_history')
readline.read_history_file(history)
atexit.register(readline.write_history_file, history)
self.ast_transformers = ast_transformers
self.source_transformers = source_transformers
def runsource(self, source, filename='<input>', symbol='single'):
if not source:
return True
for t in self.source_transformers:
source = '\n'.join(t(source.splitlines()))
if self.ast_transformers:
tree = ast.parse(source, mode=symbol)
for t in self.ast_transformers:
tree = t.visit(tree)
ast.fix_missing_locations(tree)
code_obj = compile(tree, filename, mode=symbol)
try:
self.runcode(code_obj)
except SystemExit:
os.exit(0)
return False
return super().runsource(source, filename=filename, symbol=symbol)
c = MpmathConsole(ast_transformers=ast_transformers,
source_transformers=source_transformers, locals=ns)
for l in lines:
c.push(l)
c.interact('', '')
if __name__ == '__main__':
main()
+44
View File
@@ -0,0 +1,44 @@
import ast
import io
import tokenize
class IntegerDivisionWrapper(ast.NodeTransformer):
"""Wrap all int divisions in a call to :class:`~fractions.Fraction`."""
def visit_BinOp(self, node):
def is_integer(x):
if isinstance(x, ast.Constant) and isinstance(x.value, int):
return True
if isinstance(x, ast.UnaryOp) and isinstance(x.op, (ast.USub,
ast.UAdd)):
return is_integer(x.operand)
if isinstance(x, ast.BinOp) and isinstance(x.op, (ast.Add,
ast.Sub,
ast.Mult,
ast.Pow)):
return is_integer(x.left) and is_integer(x.right)
return False
if isinstance(node.op, ast.Div) and all(map(is_integer,
[node.left, node.right])):
return ast.Call(ast.Name('Fraction', ast.Load()),
[node.left, node.right], [])
return self.generic_visit(node)
def wrap_float_literals(lines):
"""Wraps all float/complex literals with mpmath classes."""
new_lines = []
for line in lines:
result = []
g = tokenize.tokenize(io.BytesIO(line.encode()).readline)
for toknum, tokval, _, _, _ in g:
if toknum == tokenize.NUMBER:
if 'j' in tokval:
tokval = f"mpc(0, mpf('{tokval[:-1]}'))"
elif '.' in tokval:
tokval = f"mpf('{tokval}')"
result.append((toknum, tokval))
new_lines.append(tokenize.untokenize(result).decode())
return new_lines
+120
View File
@@ -0,0 +1,120 @@
"""Tests for the Command-Line Interface."""
import platform
import time
import pexpect
import pytest
if platform.python_implementation() == 'PyPy':
pytest.skip("Don't run CLI tests on PyPy.",
allow_module_level=True)
class Console(pexpect.spawn):
"""Spawned console for testing."""
def __init__(self, command, timeout=60):
super().__init__(command, timeout=timeout, encoding='utf-8')
def __del__(self):
self.send('exit()\r\n')
time.sleep(10) # a delay to allow coverage finish work
if self.isalive():
self.terminate(force=True)
def test_bare_console_no_bare_division():
c = Console('python -m mpmath --no-ipython --no-wrap-floats')
assert c.expect_exact('>>> ') == 0
assert c.send('1 + 2\r\n') == 7
assert c.expect_exact('3\r\n>>> ') == 0
assert c.send('1/2\r\n') == 5
assert c.expect_exact('Fraction(1, 2)\r\n>>> ') == 0
assert c.send('-1/2\r\n') == 6
assert c.expect_exact('Fraction(-1, 2)\r\n>>> ') == 0
assert c.send('2**3/7\r\n') == 8
assert c.expect_exact('Fraction(8, 7)\r\n>>> ') == 0
assert c.send('(3 + 5)/7\r\n') == 11
assert c.expect_exact('Fraction(8, 7)\r\n>>> ') == 0
assert c.send('(0.5 + 1)/2\r\n') == 13
assert c.expect_exact('0.75\r\n>>> ') == 0
def test_bare_console_bare_division():
c = Console('python -m mpmath --no-ipython --no-wrap-division '
'--no-wrap-floats')
assert c.expect_exact('>>> ') == 0
assert c.send('1/2\r\n') == 5
assert c.expect_exact('0.5\r\n>>> ') == 0
def test_bare_console_without_ipython():
try:
import IPython
del IPython
pytest.skip('IPython is available')
except ImportError:
pass
c = Console('python -m mpmath')
assert c.expect_exact('>>> ') == 0
assert c.send('1 + 2\r\n') == 7
assert c.expect_exact('3\r\n>>> ') == 0
assert c.send('1/2\r\n') == 5
assert c.expect_exact('\r\nFraction(1, 2)\r\n>>> ') == 0
def test_ipython_console_bare_division_noauto():
pytest.importorskip('IPython')
c = Console('python -m mpmath --simple-prompt --no-wrap-floats '
"--no-wrap-division --colors 'NoColor' ")
assert c.expect_exact('\r\nIn [1]: ') == 0
assert c.send('1/2\r\n') == 5
assert c.expect_exact('\r\nOut[1]: 0.5\r\n\r\nIn [2]: ') == 0
def test_ipython_console_wrap_floats():
pytest.importorskip('IPython')
c = Console('python -m mpmath --simple-prompt --prec 100 '
"--colors 'NoColor'")
assert c.expect_exact('\r\nIn [1]: ') == 0
assert c.send('10.9\r\n') == 6
assert c.expect_exact("\r\nOut[1]: mpf('10.899999999999999999999999999995')\r\n\r\nIn [2]: ") == 0
def test_bare_console_wrap_floats():
c = Console('python -m mpmath --simple-prompt --no-ipython --prec 100 '
"--colors 'NoColor'")
assert c.expect_exact('>>> ') == 0
assert c.send("10.9\r\n") == 6
assert c.expect_exact("mpf('10.899999999999999999999999999995')\r\n>>> ") == 0
assert c.send("1+10.9j\r\n") == 9
assert c.expect_exact("mpc(real='1.0', imag='10.899999999999999999999999999995')\r\n>>> ") == 0
assert c.send('mpf(10.9)\r\n') == 11
assert c.expect_exact("mpf('10.899999999999999999999999999995')\r\n>>> ") == 0
def test_bare_console_pretty():
c = Console('python -m mpmath --simple-prompt --no-ipython --prec 100 '
"--colors 'NoColor' --pretty")
assert c.expect_exact('>>> ') == 0
assert c.send("10.9\r\n") == 6
assert c.expect_exact("10.9\r\n>>> ") == 0
def test_mpmath_version():
c = Console('python -m mpmath --version')
assert c.expect(pexpect.EOF) == 0
assert c.before.startswith('1.')
+5 -3
View File
@@ -31,12 +31,13 @@ Homepage = 'https://mpmath.org/'
Documentation = 'http://mpmath.org/doc/current/'
[project.optional-dependencies]
tests = ['pytest>=6', 'numpy; python_version<"3.13"',
'matplotlib; python_version<"3.13"']
'matplotlib; python_version<"3.13"', 'pexpect', 'ipython']
develop = ['mpmath[tests]', 'flake518>=1.5; python_version>="3.9"',
'pytest-cov', 'wheel', 'build']
gmpy = ['gmpy2>=2.1.0a4; platform_python_implementation!="PyPy" and python_version<"3.12"',
'gmpy2>=2.2.0a1; platform_python_implementation!="PyPy" and python_version>="3.12" and python_version<"3.13"']
docs = ['sphinx', 'matplotlib; python_version<"3.13"']
docs = ['sphinx', 'matplotlib; python_version<"3.13"',
'sphinxcontrib-autoprogram']
ci = ['pytest-xdist']
[tool.setuptools]
zip-safe = true
@@ -62,7 +63,8 @@ omit = ['mpmath/tests/*']
[tool.coverage.report]
exclude_lines = ['pragma: no cover',
'raise NotImplementedError',
'return NotImplemented']
'return NotImplemented',
'if __name__ == .__main__.:']
show_missing = true
[tool.isort]
lines_after_imports = 2