##// END OF EJS Templates
Add support for append/replace mode after discussion with Evan....
Fernando Perez -
Show More
@@ -1,237 +1,259 b''
1 1 """Analysis of text input into executable blocks.
2 2
3 3 This is a simple example of how an interactive terminal-based client can use
4 4 this tool::
5 5
6 6 bb = BlockBreaker()
7 7 while not bb.interactive_block_ready():
8 8 bb.push(raw_input('>>> '))
9 9 print 'Input source was:\n', bb.source,
10 10 """
11 11 #-----------------------------------------------------------------------------
12 12 # Copyright (C) 2010 The IPython Development Team
13 13 #
14 14 # Distributed under the terms of the BSD License. The full license is in
15 15 # the file COPYING, distributed as part of this software.
16 16 #-----------------------------------------------------------------------------
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Imports
20 20 #-----------------------------------------------------------------------------
21 21 # stdlib
22 22 import codeop
23 23 import re
24 24 import sys
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Utilities
28 28 #-----------------------------------------------------------------------------
29 29
30 30 # FIXME: move these utilities to the general ward...
31 31
32 32 # compiled regexps for autoindent management
33 33 dedent_re = re.compile(r'^\s+raise|^\s+return|^\s+pass')
34 34 ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)')
35 35
36 36
37 37 def num_ini_spaces(s):
38 38 """Return the number of initial spaces in a string.
39 39
40 40 Note that tabs are counted as a single space. For now, we do *not* support
41 41 mixing of tabs and spaces in the user's input.
42 42
43 43 Parameters
44 44 ----------
45 45 s : string
46 46 """
47 47
48 48 ini_spaces = ini_spaces_re.match(s)
49 49 if ini_spaces:
50 50 return ini_spaces.end()
51 51 else:
52 52 return 0
53 53
54 54
55 55 def remove_comments(src):
56 56 """Remove all comments from input source.
57 57
58 58 Note: comments are NOT recognized inside of strings!
59 59
60 60 Parameters
61 61 ----------
62 62 src : string
63 63 A single or multiline input string.
64 64
65 65 Returns
66 66 -------
67 67 String with all Python comments removed.
68 68 """
69 69
70 70 return re.sub('#.*', '', src)
71 71
72 72
73 73 def get_input_encoding():
74 74 """Return the default standard input encoding."""
75 75 return getattr(sys.stdin, 'encoding', 'ascii')
76 76
77 77 #-----------------------------------------------------------------------------
78 78 # Classes and functions
79 79 #-----------------------------------------------------------------------------
80 80
81 81 class BlockBreaker(object):
82 82 # Command compiler
83 83 compile = None
84 84 # Number of spaces of indentation
85 85 indent_spaces = 0
86 86 # String, indicating the default input encoding
87 87 encoding = ''
88 88 # String where the current full source input is stored, properly encoded
89 89 source = ''
90 90 # Code object corresponding to the current source
91 91 code = None
92 92 # Boolean indicating whether the current block is complete
93 93 is_complete = None
94
94 # Input mode
95 input_mode = 'append'
96
95 97 # Private attributes
96 98
97 99 # List
98 100 _buffer = None
99 101
100 def __init__(self):
102 def __init__(self, input_mode=None):
103 """Create a new BlockBreaker instance.
104
105 Parameters
106 ----------
107 input_mode : str
108
109 One of 'append', 'replace', default is 'append'. This controls how
110 new inputs are used: in 'append' mode, they are appended to the
111 existing buffer and the whole buffer is compiled; in 'replace' mode,
112 each new input completely replaces all prior inputs. Replace mode is
113 thus equivalent to prepending a full reset() to every push() call.
114
115 In practice, line-oriented clients likely want to use 'append' mode
116 while block-oriented ones will want to use 'replace'.
117 """
101 118 self._buffer = []
102 119 self.compile = codeop.CommandCompiler()
103 120 self.encoding = get_input_encoding()
121 self.input_mode = BlockBreaker.input_mode if input_mode is None \
122 else input_mode
104 123
105 124 def reset(self):
106 125 """Reset the input buffer and associated state."""
107 126 self.indent_spaces = 0
108 127 self._buffer[:] = []
109 128 self.source = ''
110 129 self.code = None
111 130
112 131 def get_source(self, reset=False):
113 132 """Return the input source.
114 133
115 134 Parameters
116 135 ----------
117 136 reset : boolean
118 137 If true, all state is reset and prior input forgotten.
119 138 """
120 139 out = self.source
121 140 if reset:
122 141 self.reset()
123 142 return out
124 143
125 144 def push(self, lines):
126 145 """Push one ore more lines of input.
127 146
128 147 This stores the given lines and returns a status code indicating
129 148 whether the code forms a complete Python block or not.
130 149
131 150 Any exceptions generated in compilation are allowed to propagate.
132 151
133 152 Parameters
134 153 ----------
135 154 lines : string
136 155 One or more lines of Python input.
137 156
138 157 Returns
139 158 -------
140 159 is_complete : boolean
141 160 True if the current input source (the result of the current input
142 161 plus prior inputs) forms a complete Python execution block. Note that
143 162 this value is also stored as an attribute so it can be queried at any
144 163 time.
145 164 """
165 if self.input_mode == 'replace':
166 self.reset()
167
146 168 # If the source code has leading blanks, add 'if 1:\n' to it
147 169 # this allows execution of indented pasted code. It is tempting
148 170 # to add '\n' at the end of source to run commands like ' a=1'
149 171 # directly, but this fails for more complicated scenarios
150 172 if not self._buffer and lines[:1] in [' ', '\t']:
151 173 lines = 'if 1:\n%s' % lines
152 174
153 175 self._store(lines)
154 176 source = self.source
155 177
156 178 # Before calling compile(), reset the code object to None so that if an
157 179 # exception is raised in compilation, we don't mislead by having
158 180 # inconsistent code/source attributes.
159 181 self.code, self.is_complete = None, None
160 182 self.code = self.compile(source)
161 183 # Compilation didn't produce any exceptions (though it may not have
162 184 # given a complete code object)
163 185 if self.code is None:
164 186 self.is_complete = False
165 187 else:
166 188 self.is_complete = True
167 189 self._update_indent(lines)
168 190 return self.is_complete
169 191
170 192 def interactive_block_ready(self):
171 193 """Return whether a block of interactive input is ready for execution.
172 194
173 195 This method is meant to be used by line-oriented frontends, who need to
174 196 guess whether a block is complete or not based solely on prior and
175 197 current input lines. The BlockBreaker considers it has a complete
176 198 interactive block when *all* of the following are true:
177 199
178 200 1. The input compiles to a complete statement.
179 201
180 202 2. The indentation level is flush-left (because if we are indented,
181 203 like inside a function definition or for loop, we need to keep
182 204 reading new input).
183 205
184 206 3. There is one extra line consisting only of whitespace.
185 207
186 208 Because of condition #3, this method should be used only by
187 209 *line-oriented* frontends, since it means that intermediate blank lines
188 210 are not allowed in function definitions (or any other indented block).
189 211
190 212 Block-oriented frontends that have a separate keyboard event to
191 213 indicate execution should use the :meth:`split_blocks` method instead.
192 214 """
193 215 if not self.is_complete:
194 216 return False
195 217 if self.indent_spaces==0:
196 218 return True
197 219 last_line = self.source.splitlines()[-1]
198 220 if not last_line or last_line.isspace():
199 221 return True
200 222 else:
201 223 return False
202 224
203 225 def split_blocks(self, lines):
204 226 """Split a multiline string into multiple input blocks"""
205 227 raise NotImplementedError
206 228
207 229 #------------------------------------------------------------------------
208 230 # Private interface
209 231 #------------------------------------------------------------------------
210 232
211 233 def _update_indent(self, lines):
212 234 """Keep track of the indent level."""
213 235
214 236 for line in remove_comments(lines).splitlines():
215 237
216 238 if line and not line.isspace():
217 239 if self.code is not None:
218 240 inisp = num_ini_spaces(line)
219 241 if inisp < self.indent_spaces:
220 242 self.indent_spaces = inisp
221 243
222 244 if line[-1] == ':':
223 245 self.indent_spaces += 4
224 246 elif dedent_re.match(line):
225 247 self.indent_spaces -= 4
226 248
227 249 def _store(self, lines):
228 250 """Store one or more lines of input.
229 251
230 252 If input lines are not newline-terminated, a newline is automatically
231 253 appended."""
232 254
233 255 if lines.endswith('\n'):
234 256 self._buffer.append(lines)
235 257 else:
236 258 self._buffer.append(lines+'\n')
237 259 self.source = ''.join(self._buffer).encode(self.encoding)
@@ -1,173 +1,183 b''
1 1 """Tests for the blockbreaker module.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (C) 2010 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING, distributed as part of this software.
8 8 #-----------------------------------------------------------------------------
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Imports
12 12 #-----------------------------------------------------------------------------
13 13 # stdlib
14 14 import unittest
15 15
16 16 # Third party
17 17 import nose.tools as nt
18 18
19 19 # Our own
20 20 from IPython.core import blockbreaker as BB
21 21
22 22 #-----------------------------------------------------------------------------
23 23 # Tests
24 24 #-----------------------------------------------------------------------------
25 25 def test_spaces():
26 26 tests = [('', 0),
27 27 (' ', 1),
28 28 ('\n', 0),
29 29 (' \n', 1),
30 30 ('x', 0),
31 31 (' x', 1),
32 32 (' x',2),
33 33 (' x',4),
34 34 # Note: tabs are counted as a single whitespace!
35 35 ('\tx', 1),
36 36 ('\t x', 2),
37 37 ]
38 38
39 39 for s, nsp in tests:
40 40 nt.assert_equal(BB.num_ini_spaces(s), nsp)
41 41
42 42
43 43 def test_remove_comments():
44 44 tests = [('text', 'text'),
45 45 ('text # comment', 'text '),
46 46 ('text # comment\n', 'text \n'),
47 47 ('text # comment \n', 'text \n'),
48 48 ('line # c \nline\n','line \nline\n'),
49 49 ('line # c \nline#c2 \nline\nline #c\n\n',
50 50 'line \nline\nline\nline \n\n'),
51 51 ]
52 52
53 53 for inp, out in tests:
54 54 nt.assert_equal(BB.remove_comments(inp), out)
55 55
56 56
57 57 def test_get_input_encoding():
58 58 encoding = BB.get_input_encoding()
59 59 nt.assert_true(isinstance(encoding, basestring))
60 60 # simple-minded check that at least encoding a simple string works with the
61 61 # encoding we got.
62 62 nt.assert_equal('test'.encode(encoding), 'test')
63 63
64 64
65 65 class BlockBreakerTestCase(unittest.TestCase):
66 66 def setUp(self):
67 67 self.bb = BB.BlockBreaker()
68 68
69 69 def test_reset(self):
70 70 bb = self.bb
71 71 bb.push('x=1')
72 72 bb.reset()
73 73 self.assertEqual(bb._buffer, [])
74 74 self.assertEqual(bb.indent_spaces, 0)
75 75 self.assertEqual(bb.get_source(), '')
76 76 self.assertEqual(bb.code, None)
77 77
78 78 def test_source(self):
79 79 self.bb._store('1')
80 80 self.bb._store('2')
81 81 out = self.bb.get_source()
82 82 self.assertEqual(out, '1\n2\n')
83 83 out = self.bb.get_source(reset=True)
84 84 self.assertEqual(out, '1\n2\n')
85 85 self.assertEqual(self.bb._buffer, [])
86 86 out = self.bb.get_source()
87 87 self.assertEqual(out, '')
88 88
89 89 def test_indent(self):
90 90 bb = self.bb # shorthand
91 91 bb.push('x=1')
92 92 self.assertEqual(bb.indent_spaces, 0)
93 93 bb.push('if 1:\n x=1')
94 94 self.assertEqual(bb.indent_spaces, 4)
95 95 bb.push('y=2\n')
96 96 self.assertEqual(bb.indent_spaces, 0)
97 97 bb.push('if 1:')
98 98 self.assertEqual(bb.indent_spaces, 4)
99 99 bb.push(' x=1')
100 100 self.assertEqual(bb.indent_spaces, 4)
101 101 # Blank lines shouldn't change the indent level
102 102 bb.push(' '*2)
103 103 self.assertEqual(bb.indent_spaces, 4)
104 104
105 105 def test_indent2(self):
106 106 bb = self.bb
107 107 # When a multiline statement contains parens or multiline strings, we
108 108 # shouldn't get confused.
109 109 bb.push("if 1:")
110 110 bb.push(" x = (1+\n 2)")
111 111 self.assertEqual(bb.indent_spaces, 4)
112 112
113 113 def test_dedent(self):
114 114 bb = self.bb # shorthand
115 115 bb.push('if 1:')
116 116 self.assertEqual(bb.indent_spaces, 4)
117 117 bb.push(' pass')
118 118 self.assertEqual(bb.indent_spaces, 0)
119 119
120 120 def test_push(self):
121 121 bb = self.bb
122 122 bb.push('x=1')
123 123 self.assertTrue(bb.is_complete)
124 124
125 125 def test_push2(self):
126 126 bb = self.bb
127 127 bb.push('if 1:')
128 128 self.assertFalse(bb.is_complete)
129 129 for line in [' x=1', '# a comment', ' y=2']:
130 130 bb.push(line)
131 131 self.assertTrue(bb.is_complete)
132 132
133 133 def test_push3(self):
134 134 """Test input with leading whitespace"""
135 135 bb = self.bb
136 136 bb.push(' x=1')
137 137 bb.push(' y=2')
138 138 self.assertEqual(bb.source, 'if 1:\n x=1\n y=2\n')
139 139
140 def test_replace_mode(self):
141 bb = self.bb
142 bb.input_mode = 'replace'
143 bb.push('x=1')
144 self.assertEqual(bb.source, 'x=1\n')
145 bb.push('x=2')
146 self.assertEqual(bb.source, 'x=2\n')
147
140 148 def test_interactive_block_ready(self):
141 149 bb = self.bb
142 150 bb.push('x=1')
143 151 self.assertTrue(bb.interactive_block_ready())
144 152
145 153 def test_interactive_block_ready2(self):
146 154 bb = self.bb
147 bb.push('if 1:\n x=1')
155 bb.push('if 1:')
156 self.assertFalse(bb.interactive_block_ready())
157 bb.push(' x=1')
148 158 self.assertFalse(bb.interactive_block_ready())
149 159 bb.push('')
150 160 self.assertTrue(bb.interactive_block_ready())
151 161
152 162 def test_interactive_block_ready3(self):
153 163 bb = self.bb
154 164 bb.push("x = (2+\n3)")
155 165 self.assertTrue(bb.interactive_block_ready())
156 166
157 167 def test_interactive_block_ready4(self):
158 168 bb = self.bb
159 169 # When a multiline statement contains parens or multiline strings, we
160 170 # shouldn't get confused.
161 171 # FIXME: we should be able to better handle de-dents in statements like
162 172 # multiline strings and multiline expressions (continued with \ or
163 173 # parens). Right now we aren't handling the indentation tracking quite
164 174 # correctly with this, though in practice it may not be too much of a
165 175 # problem. We'll need to see.
166 176 bb.push("if 1:")
167 177 bb.push(" x = (2+")
168 178 bb.push(" 3)")
169 179 self.assertFalse(bb.interactive_block_ready())
170 180 bb.push(" y = 3")
171 181 self.assertFalse(bb.interactive_block_ready())
172 182 bb.push('')
173 183 self.assertTrue(bb.interactive_block_ready())
General Comments 0
You need to be logged in to leave comments. Login now