##// END OF EJS Templates
Merge pull request #1020 from takluyver/dollar-formatter...
Fernando Perez -
r5358:09c9952f merge
parent child Browse files
Show More
@@ -73,7 +73,8 from IPython.utils.pickleshare import PickleShareDB
73 73 from IPython.utils.process import system, getoutput
74 74 from IPython.utils.strdispatch import StrDispatch
75 75 from IPython.utils.syspathcontext import prepended_to_syspath
76 from IPython.utils.text import num_ini_spaces, format_screen, LSString, SList
76 from IPython.utils.text import (num_ini_spaces, format_screen, LSString, SList,
77 DollarFormatter)
77 78 from IPython.utils.traitlets import (Integer, CBool, CaselessStrEnum, Enum,
78 79 List, Unicode, Instance, Type)
79 80 from IPython.utils.warn import warn, error, fatal
@@ -2571,7 +2572,7 class InteractiveShell(SingletonConfigurable, Magic):
2571 2572 # Utilities
2572 2573 #-------------------------------------------------------------------------
2573 2574
2574 def var_expand(self,cmd,depth=0):
2575 def var_expand(self, cmd, depth=0, formatter=DollarFormatter()):
2575 2576 """Expand python variables in a string.
2576 2577
2577 2578 The depth argument indicates how many frames above the caller should
@@ -2580,11 +2581,10 class InteractiveShell(SingletonConfigurable, Magic):
2580 2581 The global namespace for expansion is always the user's interactive
2581 2582 namespace.
2582 2583 """
2583 res = ItplNS(cmd, self.user_ns, # globals
2584 # Skip our own frame in searching for locals:
2585 sys._getframe(depth+1).f_locals # locals
2586 )
2587 return py3compat.str_to_unicode(str(res), res.codec)
2584 ns = self.user_ns.copy()
2585 ns.update(sys._getframe(depth+1).f_locals)
2586 ns.pop('self', None)
2587 return formatter.format(cmd, **ns)
2588 2588
2589 2589 def mktempfile(self, data=None, prefix='ipython_edit_'):
2590 2590 """Make a new tempfile and return its filename.
@@ -44,9 +44,8 def test_columnize_long():
44 44 out = text.columnize(items, displaywidth=size-1)
45 45 nt.assert_equals(out, '\n'.join(items+['']))
46 46
47 def test_eval_formatter():
48 f = text.EvalFormatter()
49 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
47 def eval_formatter_check(f):
48 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os, u=u"cafΓ©", b="cafΓ©")
50 49 s = f.format("{n} {n//4} {stuff.split()[0]}", **ns)
51 50 nt.assert_equals(s, "12 3 hello")
52 51 s = f.format(' '.join(['{n//%i}'%i for i in range(1,8)]), **ns)
@@ -58,12 +57,15 def test_eval_formatter():
58 57 s = f.format("{stuff!r}", **ns)
59 58 nt.assert_equals(s, repr(ns['stuff']))
60 59
61 nt.assert_raises(NameError, f.format, '{dne}', **ns)
60 # Check with unicode:
61 s = f.format("{u}", **ns)
62 nt.assert_equals(s, ns['u'])
63 # This decodes in a platform dependent manner, but it shouldn't error out
64 s = f.format("{b}", **ns)
62 65
66 nt.assert_raises(NameError, f.format, '{dne}', **ns)
63 67
64 def test_eval_formatter_slicing():
65 f = text.EvalFormatter()
66 f.allow_slicing = True
68 def eval_formatter_slicing_check(f):
67 69 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
68 70 s = f.format(" {stuff.split()[:]} ", **ns)
69 71 nt.assert_equals(s, " ['hello', 'there'] ")
@@ -75,9 +77,7 def test_eval_formatter_slicing():
75 77 nt.assert_raises(SyntaxError, f.format, "{n:x}", **ns)
76 78
77 79
78 def test_eval_formatter_no_slicing():
79 f = text.EvalFormatter()
80 f.allow_slicing = False
80 def eval_formatter_no_slicing_check(f):
81 81 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
82 82
83 83 s = f.format('{n:x} {pi**2:+f}', **ns)
@@ -85,3 +85,25 def test_eval_formatter_no_slicing():
85 85
86 86 nt.assert_raises(SyntaxError, f.format, "{a[:]}")
87 87
88 def test_eval_formatter():
89 f = text.EvalFormatter()
90 eval_formatter_check(f)
91 eval_formatter_no_slicing_check(f)
92
93 def test_full_eval_formatter():
94 f = text.FullEvalFormatter()
95 eval_formatter_check(f)
96 eval_formatter_slicing_check(f)
97
98 def test_dollar_formatter():
99 f = text.DollarFormatter()
100 eval_formatter_check(f)
101 eval_formatter_slicing_check(f)
102
103 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
104 s = f.format("$n", **ns)
105 nt.assert_equals(s, "12")
106 s = f.format("$n.real", **ns)
107 nt.assert_equals(s, "12")
108 s = f.format("$n/{stuff[:5]}", **ns)
109 nt.assert_equals(s, "12/hello")
@@ -25,6 +25,7 import textwrap
25 25 from string import Formatter
26 26
27 27 from IPython.external.path import path
28 from IPython.testing.skipdoctest import skip_doctest_py3
28 29 from IPython.utils import py3compat
29 30 from IPython.utils.io import nlprint
30 31 from IPython.utils.data import flatten
@@ -563,12 +564,12 def wrap_paragraphs(text, ncols=80):
563 564 return out_ps
564 565
565 566
566
567 567 class EvalFormatter(Formatter):
568 568 """A String Formatter that allows evaluation of simple expressions.
569 569
570 Any time a format key is not found in the kwargs,
571 it will be tried as an expression in the kwargs namespace.
570 Note that this version interprets a : as specifying a format string (as per
571 standard string formatting), so if slicing is required, you must explicitly
572 create a slice.
572 573
573 574 This is to be used in templating cases, such as the parallel batch
574 575 script templates, where simple arithmetic on arguments is useful.
@@ -580,16 +581,36 class EvalFormatter(Formatter):
580 581 In [2]: f.format('{n//4}', n=8)
581 582 Out[2]: '2'
582 583
583 In [3]: f.format('{list(range(3))}')
584 Out[3]: '[0, 1, 2]'
585
586 In [4]: f.format('{3*2}')
587 Out[4]: '6'
584 In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello")
585 Out [3]: 'll'
588 586 """
587 def get_field(self, name, args, kwargs):
588 v = eval(name, kwargs)
589 return v, name
590
591 @skip_doctest_py3
592 class FullEvalFormatter(Formatter):
593 """A String Formatter that allows evaluation of simple expressions.
589 594
590 # should we allow slicing by disabling the format_spec feature?
591 allow_slicing = True
595 Any time a format key is not found in the kwargs,
596 it will be tried as an expression in the kwargs namespace.
592 597
598 Note that this version allows slicing using [1:2], so you cannot specify
599 a format string. Use :class:`EvalFormatter` to permit format strings.
600
601 Examples
602 --------
603
604 In [1]: f = FullEvalFormatter()
605 In [2]: f.format('{n//4}', n=8)
606 Out[2]: u'2'
607
608 In [3]: f.format('{list(range(5))[2:4]}')
609 Out[3]: u'[2, 3]'
610
611 In [4]: f.format('{3*2}')
612 Out[4]: u'6'
613 """
593 614 # copied from Formatter._vformat with minor changes to allow eval
594 615 # and replace the format_spec code with slicing
595 616 def _vformat(self, format_string, args, kwargs, used_args, recursion_depth):
@@ -608,10 +629,9 class EvalFormatter(Formatter):
608 629 # this is some markup, find the object and do
609 630 # the formatting
610 631
611 if self.allow_slicing and format_spec:
632 if format_spec:
612 633 # override format spec, to allow slicing:
613 634 field_name = ':'.join([field_name, format_spec])
614 format_spec = ''
615 635
616 636 # eval the contents of the field for the object
617 637 # to be formatted
@@ -620,14 +640,43 class EvalFormatter(Formatter):
620 640 # do any conversion on the resulting object
621 641 obj = self.convert_field(obj, conversion)
622 642
623 # expand the format spec, if needed
624 format_spec = self._vformat(format_spec, args, kwargs,
625 used_args, recursion_depth-1)
626
627 643 # format the object and append to the result
628 result.append(self.format_field(obj, format_spec))
644 result.append(self.format_field(obj, ''))
645
646 return u''.join(py3compat.cast_unicode(s) for s in result)
647
648 @skip_doctest_py3
649 class DollarFormatter(FullEvalFormatter):
650 """Formatter allowing Itpl style $foo replacement, for names and attribute
651 access only. Standard {foo} replacement also works, and allows full
652 evaluation of its arguments.
629 653
630 return ''.join(result)
654 Examples
655 --------
656 In [1]: f = DollarFormatter()
657 In [2]: f.format('{n//4}', n=8)
658 Out[2]: u'2'
659
660 In [3]: f.format('23 * 76 is $result', result=23*76)
661 Out[3]: u'23 * 76 is 1748'
662
663 In [4]: f.format('$a or {b}', a=1, b=2)
664 Out[4]: u'1 or 2'
665 """
666 _dollar_pattern = re.compile("(.*)\$([\w\.]+)")
667 def parse(self, fmt_string):
668 for literal_txt, field_name, format_spec, conversion \
669 in Formatter.parse(self, fmt_string):
670
671 # Find $foo patterns in the literal text.
672 continue_from = 0
673 for m in self._dollar_pattern.finditer(literal_txt):
674 new_txt, new_field = m.group(1,2)
675 yield (new_txt, new_field, "", None)
676 continue_from = m.end()
677
678 # Re-yield the {foo} style pattern
679 yield (literal_txt[continue_from:], field_name, format_spec, conversion)
631 680
632 681
633 682 def columnize(items, separator=' ', displaywidth=80):
General Comments 0
You need to be logged in to leave comments. Login now