Skip to content

Commit

Permalink
Merge pull request #1470 from asmeurer/autorat
Browse files Browse the repository at this point in the history
Autorat
  • Loading branch information
certik committed Aug 10, 2012
2 parents 30778a6 + 64366c2 commit 08617ed
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 18 deletions.
55 changes: 47 additions & 8 deletions bin/isympy
Expand Up @@ -103,7 +103,7 @@ COMMAND LINE OPTIONS
This is equivalent to setting the environment variable
SYMPY_USE_CACHE='no'.
-a, --auto
-a, --auto-symbols
Automatically create missing symbols. Normally, typing a name of a
Symbol that has not been instantiated first would raise NameError,
Expand All @@ -129,6 +129,27 @@ COMMAND LINE OPTIONS
example, if you define a function in isympy with an undefined
Symbol, it will not work.
See also the -i and -I options.
-i, --int-to-Integer
Automatically wrap int literals with Integer. This makes it so that
things like 1/2 will come out as Rational(1, 2), rather than 0.5. This
works by preprocessing the source and wrapping all int literals with
Integer. Note that this will not change the behavior of int literals
assigned to variables, and it also won't change the behavior of functions
that return int literals.
If you want an int, you can wrap the literal in int().
Note that this requires IPython.
-I, --interactive
This is equivalent to --auto-symbols --int-to-Integer. Future options
designed for ease of interactive use may be added to this. Note that this
requires IPython.
-D, --debug
Enable debugging output. This is the same as setting the
Expand All @@ -137,10 +158,11 @@ COMMAND LINE OPTIONS
-- IPython options
Additionally you can pass command line options directly to the
IPython interpreter (the standard Python shell is not supported).
However you need to add '--' separator between two types of options.
To run SymPy without startup banner and colors, for example, issue in IPython 0.11 or higher:
Additionally you can pass command line options directly to the IPython
interpreter (the standard Python shell is not supported). However you
need to add '--' separator between two types of options. To run SymPy
without startup banner and colors, for example, issue in IPython 0.11 or
higher:
$isympy -q -- --colors=NoColor
Expand All @@ -149,6 +171,7 @@ COMMAND LINE OPTIONS
$isympy -q -- -colors NoColor
See also isympy --help.
"""

import os, sys, warnings
Expand Down Expand Up @@ -240,12 +263,26 @@ def main():
help='disable caching mechanism')

parser.add_option(
'-a', '--auto',
dest='auto',
'-a', '--auto-symbols',
dest='auto_symbols',
action='store_true',
default=False,
help='automatically construct missing symbols')

parser.add_option(
'-i', '--int-to-Integer',
dest='auto_int_to_Integer',
action='store_true',
default=False,
help="automatically wrap int literals with Integer")

parser.add_option(
'-I', '--interactive',
dest='interactive',
action='store_true',
default=False,
help="equivalent to -a -i")

parser.add_option(
'-D', '--debug',
dest='debug',
Expand Down Expand Up @@ -293,7 +330,9 @@ def main():
args['order'] = options.order

args['quiet'] = options.quiet
args['auto'] = options.auto
args['auto_symbols'] = options.auto_symbols or options.interactive
args['auto_int_to_Integer'] = options.auto_int_to_Integer or options.interactive


from sympy.utilities.exceptions import SymPyDeprecationWarning
warnings.simplefilter("always", SymPyDeprecationWarning)
Expand Down
181 changes: 173 additions & 8 deletions sympy/interactive/session.py
Expand Up @@ -68,8 +68,162 @@ def _make_message(ipython=True, quiet=False, source=None):

return message

def int_to_Integer(s):
"""
Wrap integer literals with Integer.
This is based on the decistmt example from
http://docs.python.org/library/tokenize.html.
Only integer literals are converted. Float literals are left alone.
Example
=======
>>> from sympy.interactive.session import int_to_Integer
>>> from sympy import Integer
>>> s = '1.2 + 1/2 - 0x12 + a1'
>>> int_to_Integer(s)
'1.2 +Integer (1 )/Integer (2 )-Integer (0x12 )+a1 '
>>> s = 'print (1/2)'
>>> int_to_Integer(s)
'print (Integer (1 )/Integer (2 ))'
>>> exec(s) #doctest: +SKIP
0.5
>>> exec(int_to_Integer(s))
1/2
"""
from tokenize import generate_tokens, untokenize, NUMBER, NAME, OP
from StringIO import StringIO

def _is_int(num):
"""
Returns true if string value num (with token NUMBER) represents an integer.
"""
# XXX: Is there something in the standard library that will do this?
if '.' in num or 'j' in num.lower() or 'e' in num.lower():
return False
return True

result = []
g = generate_tokens(StringIO(s).readline) # tokenize the string
for toknum, tokval, _, _, _ in g:
if toknum == NUMBER and _is_int(tokval): # replace NUMBER tokens
result.extend([
(NAME, 'Integer'),
(OP, '('),
(NUMBER, tokval),
(OP, ')')
])
else:
result.append((toknum, tokval))
return untokenize(result)

# XXX: Something like this might be used, but it only works on single line
# inputs. See
# http://mail.scipy.org/pipermail/ipython-user/2012-August/010846.html and
# https://github.com/ipython/ipython/issues/1491. So instead we are forced to
# just monkey-patch run_cell until IPython builds a better API.
#
# class IntTransformer(object):
# """
# IPython command line transformer that recognizes and replaces int
# literals.
#
# Based on
# https://bitbucket.org/birkenfeld/ipython-physics/src/71b2d850da00/physics.py.
#
# """
# priority = 99
# enabled = True
# def transform(self, line, continue_prompt):
# import re
# from tokenize import TokenError
# leading_space = re.compile(' *')
# spaces = re.match(leading_space, line).span()[1]
# try:
# return ' '*spaces + int_to_Integer(line)
# except TokenError:
# return line
#
# int_transformer = IntTransformer()
#
# def enable_automatic_int_sympification(app):
# """
# Allow IPython to automatically convert integer literals to Integer.
#
# This lets things like 1/2 be executed as (essentially) Rational(1, 2).
# """
# app.shell.prefilter_manager.register_transformer(int_transformer)

def enable_automatic_int_sympification(app):
"""
Allow IPython to automatically convert integer literals to Integer.
"""
hasshell = hasattr(app, 'shell')

import ast
if hasshell:
old_run_cell = app.shell.run_cell
else:
old_run_cell = app.run_cell
def my_run_cell(cell, *args, **kwargs):
try:
# Check the cell for syntax errors. This way, the syntax error
# will show the original input, not the transformed input. The
# downside here is that IPython magic like %timeit will not work
# with transformed input (but on the other hand, IPython magic
# that doesn't expect transformed input will continue to work).
ast.parse(cell)
except SyntaxError:
pass
else:
cell = int_to_Integer(cell)
old_run_cell(cell, *args, **kwargs)

if hasshell:
app.shell.run_cell = my_run_cell
else:
app.run_cell = my_run_cell

def enable_automatic_symbols(app):
"""Allow IPython to automatially create symbols (``isympy -a``). """
# XXX: This should perhaps use tokenize, like int_to_Integer() above.
# This would avoid re-executing the code, which can lead to subtle
# issues. For example:
#
# In [1]: a = 1
#
# In [2]: for i in range(10):
# ...: a += 1
# ...:
#
# In [3]: a
# Out[3]: 11
#
# In [4]: a = 1
#
# In [5]: for i in range(10):
# ...: a += 1
# ...: print b
# ...:
# b
# b
# b
# b
# b
# b
# b
# b
# b
# b
#
# In [6]: a
# Out[6]: 12
#
# Note how the for loop is executed again because `b` was not defined, but `a`
# was already incremented once, so the result is that it is incremented
# multiple times.

import re
re_nameerror = re.compile("name '(?P<symbol>[A-Za-z_][A-Za-z0-9_]*)' is not defined")

Expand Down Expand Up @@ -100,7 +254,7 @@ def _handler(self, etype, value, tb, tb_offset=None):
# This was restructured in IPython 0.13
app.set_custom_exc((NameError,), _handler)

def init_ipython_session(argv=[], auto=False):
def init_ipython_session(argv=[], auto_symbols=False, auto_int_to_Integer=False):
"""Construct new IPython session. """
import IPython

Expand All @@ -113,8 +267,10 @@ def init_ipython_session(argv=[], auto=False):
app.display_banner = False
app.initialize(argv)

if auto:
if auto_symbols:
enable_automatic_symbols(app)
if auto_int_to_Integer:
enable_automatic_int_sympification(app)

return app.shell
else:
Expand Down Expand Up @@ -154,7 +310,7 @@ def __init__(self):
return SymPyConsole()

def init_session(ipython=None, pretty_print=True, order=None,
use_unicode=None, quiet=False, auto=False, argv=[]):
use_unicode=None, quiet=False, auto_symbols=False, auto_int_to_Integer=False, argv=[]):
"""
Initialize an embedded IPython or Python session.
Expand All @@ -177,9 +333,15 @@ def init_session(ipython=None, pretty_print=True, order=None,
quiet: boolean
If True, init_session will not print messages regarding its status;
if False, init_session will print messages regarding its status.
auto: boolean
If True, init_session will automatically create symbols for you;
if False, it will not.
auto_symbols: boolean
If True, IPython will automatically create symbols for you.
If False, it will not.
The default is False.
auto_int_to_Integer: boolean
If True, IPython will automatically wrap int literals with Integer, so
that things like 1/2 give Rational(1, 2).
If False, it will not.
The default is False.
ipython: boolean or None
If True, printing will initialize for an IPython console;
if False, printing will initialize for a normal console;
Expand Down Expand Up @@ -260,7 +422,8 @@ def init_session(ipython=None, pretty_print=True, order=None,
if ip is not None:
in_ipython = True
else:
ip = init_ipython_session(argv=argv, auto=auto)
ip = init_ipython_session(argv=argv,
auto_symbols=auto_symbols, auto_int_to_Integer=auto_int_to_Integer)

if IPython.__version__ >= '0.11':
# runsource is gone, use run_cell instead, which doesn't
Expand All @@ -270,8 +433,10 @@ def init_session(ipython=None, pretty_print=True, order=None,
if not in_ipython:
mainloop = ip.mainloop

if auto and (not ipython or IPython.__version__ < '0.11'):
if auto_symbols and (not ipython or IPython.__version__ < '0.11'):
raise RuntimeError("automatic construction of symbols is possible only in IPython 0.11 or above")
if auto_int_to_Integer and (not ipython or IPython.__version__ < '0.11'):
raise RuntimeError("automatic int to Integer transformation is possible only in IPython 0.11 or above")

_preexec_source = preexec_source

Expand Down
15 changes: 15 additions & 0 deletions sympy/interactive/tests/test_interactive.py
@@ -0,0 +1,15 @@
import sys

from sympy.interactive.session import int_to_Integer

def test_int_to_Integer():
assert int_to_Integer("1 + 2.2 + 0x3 + 40") == \
'Integer (1 )+2.2 +Integer (0x3 )+Integer (40 )'
if sys.version_info[0] == 2:
assert int_to_Integer("1l") == 'Integer (1l )'
if sys.version_info[1] > 5 or sys.version_info[0] == 3:
# Binary literals were added in Python 2.6
assert int_to_Integer("0b101") == 'Integer (0b101 )'
assert int_to_Integer("ab1 + 1 + '1 + 2'") == "ab1 +Integer (1 )+'1 + 2'"
assert int_to_Integer("(2 + \n3)") == '(Integer (2 )+\nInteger (3 ))'
assert int_to_Integer("2 + 2.0 + 2j + 2e-10") == 'Integer (2 )+2.0 +2j +2e-10 '
33 changes: 31 additions & 2 deletions sympy/interactive/tests/test_ipython.py
@@ -1,8 +1,9 @@
"""Tests of tools for setting up interactive IPython sessions. """

from sympy.interactive.session import init_ipython_session, enable_automatic_symbols
from sympy.interactive.session import (init_ipython_session,
enable_automatic_symbols, enable_automatic_int_sympification)

from sympy.core import Symbol
from sympy.core import Symbol, Rational, Integer
from sympy.external import import_module
from sympy.utilities.pytest import raises

Expand Down Expand Up @@ -31,3 +32,31 @@ def test_automatic_symbols():
app.run_cell(symbol, False)
assert symbol in app.user_ns
assert isinstance(app.user_ns[symbol], Symbol)

# Check that built-in names aren't overridden
app.run_cell("a = all == __builtin__.all", False)
assert "all" not in app.user_ns
assert app.user_ns['a'] == True

# Check that sympy names aren't overridden
app.run_cell("import sympy")
app.run_cell("a = factorial == sympy.factorial")
assert app.user_ns['a'] == True

def test_int_to_Integer():
# XXX: Warning, don't test with == here. 0.5 == Rational(1, 2) is True!
app = init_ipython_session()
app.run_cell("a = 1")
assert isinstance(app.user_ns['a'], int)

enable_automatic_int_sympification(app)
app.run_cell("a = 1/2")
assert isinstance(app.user_ns['a'], Rational)
app.run_cell("a = 1")
assert isinstance(app.user_ns['a'], Integer)
app.run_cell("a = int(1)")
assert isinstance(app.user_ns['a'], int)
app.run_cell("a = (1/\n2)")
assert app.user_ns['a'] == Rational(1, 2)
# TODO: How can we test that the output of a SyntaxError is the original
# input, not the transformed input?

0 comments on commit 08617ed

Please sign in to comment.