Skip to content
Browse files

update EvalFormatter to allow arbitrary expressions

EvalFormatter should now support arbitrary Python expressions, including
slicing.  Since slicing conflicts with the format_spec part of fancy formatting, there is a switch to restore format_spec behavior at the expense of slicing.

Associated tests added as well.
  • Loading branch information...
1 parent adba85c commit 8f5539570ceb3f69a336c6ba4eca1a568aaebc41 @minrk committed Aug 20, 2011
Showing with 84 additions and 12 deletions.
  1. +43 −0 IPython/utils/tests/test_text.py
  2. +41 −12 IPython/utils/text.py
View
43 IPython/utils/tests/test_text.py
@@ -13,6 +13,7 @@
#-----------------------------------------------------------------------------
import os
+import math
import nose.tools as nt
@@ -42,3 +43,45 @@ def test_columnize_long():
items = [l*size for l in 'abc']
out = text.columnize(items, displaywidth=size-1)
nt.assert_equals(out, '\n'.join(items+['']))
+
+def test_eval_formatter():
+ f = text.EvalFormatter()
+ ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
+ s = f.format("{n} {n/4} {stuff.split()[0]}", **ns)
+ nt.assert_equals(s, "12 3 hello")
+ s = f.format(' '.join(['{n//%i}'%i for i in range(1,8)]), **ns)
+ nt.assert_equals(s, "12 6 4 3 2 2 1")
+ s = f.format('{[n//i for i in range(1,8)]}', **ns)
+ nt.assert_equals(s, "[12, 6, 4, 3, 2, 2, 1]")
+ s = f.format("{stuff!s}", **ns)
+ nt.assert_equals(s, ns['stuff'])
+ s = f.format("{stuff!r}", **ns)
+ nt.assert_equals(s, repr(ns['stuff']))
+
+ nt.assert_raises(NameError, f.format, '{dne}', **ns)
+
+
+def test_eval_formatter_slicing():
+ f = text.EvalFormatter()
+ f.allow_slicing = True
+ ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
+ s = f.format(" {stuff.split()[:]} ", **ns)
+ nt.assert_equals(s, " ['hello', 'there'] ")
+ s = f.format(" {stuff.split()[::-1]} ", **ns)
+ nt.assert_equals(s, " ['there', 'hello'] ")
+ s = f.format("{stuff[::2]}", **ns)
+ nt.assert_equals(s, ns['stuff'][::2])
+
+ nt.assert_raises(SyntaxError, f.format, "{n:x}", **ns)
+
+
+def test_eval_formatter_no_slicing():
+ f = text.EvalFormatter()
+ f.allow_slicing = False
+ ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
+
+ s = f.format('{n:x} {pi**2:+f}', **ns)
+ nt.assert_equals(s, "c +9.869604")
+
+ nt.assert_raises(SyntaxError, f.format, "{a[:]}")
+
View
53 IPython/utils/text.py
@@ -597,18 +597,47 @@ class EvalFormatter(Formatter):
Out[4]: '6'
"""
- def get_value(self, key, args, kwargs):
- if isinstance(key, (int, long)):
- return args[key]
- elif key in kwargs:
- return kwargs[key]
- else:
- # evaluate the expression using kwargs as namespace
- try:
- return eval(key, kwargs)
- except Exception:
- # classify all bad expressions as key errors
- raise KeyError(key)
+ # should we allow slicing by disabling the format_spec feature?
+ allow_slicing = True
+
+ # copied from Formatter._vformat with minor changes to allow eval
+ # and replace the format_spec code with slicing
+ def _vformat(self, format_string, args, kwargs, used_args, recursion_depth):
+ if recursion_depth < 0:
+ raise ValueError('Max string recursion exceeded')
+ result = []
+ for literal_text, field_name, format_spec, conversion in \
+ self.parse(format_string):
+
+ # output the literal text
+ if literal_text:
+ result.append(literal_text)
+
+ # if there's a field, output it
+ if field_name is not None:
+ # this is some markup, find the object and do
+ # the formatting
+
+ if self.allow_slicing and format_spec:
+ # override format spec, to allow slicing:
+ field_name = ':'.join([field_name, format_spec])
+ format_spec = ''
+
+ # eval the contents of the field for the object
+ # to be formatted
+ obj = eval(field_name, kwargs)
+
+ # do any conversion on the resulting object
+ obj = self.convert_field(obj, conversion)
+
+ # expand the format spec, if needed
+ format_spec = self._vformat(format_spec, args, kwargs,
+ used_args, recursion_depth-1)
+
+ # format the object and append to the result
+ result.append(self.format_field(obj, format_spec))
+
+ return ''.join(result)
def columnize(items, separator=' ', displaywidth=80):

0 comments on commit 8f55395

Please sign in to comment.
Something went wrong with that request. Please try again.