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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -28,6 +28,7 @@ Basic features
|
||||
contexts
|
||||
general
|
||||
plotting
|
||||
cli
|
||||
|
||||
Advanced mathematics
|
||||
--------------------
|
||||
|
||||
@@ -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)
|
||||
---------------------
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user