diff --git a/IPython/utils/tests/test_text.py b/IPython/utils/tests/test_text.py index 0fa23cb..c4c0629 100644 --- a/IPython/utils/tests/test_text.py +++ b/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[:]}") + diff --git a/IPython/utils/text.py b/IPython/utils/text.py index a23905f..9b2070f 100644 --- a/IPython/utils/text.py +++ b/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):