##// END OF EJS Templates
Created tool for handling interactive input blocks....
Fernando Perez -
Show More
@@ -0,0 +1,385 b''
1 """Analysis of text input into executable blocks.
2
3 This is a simple example of how an interactive terminal-based client can use
4 this tool::
5
6 bb = BlockBreaker()
7 while not bb.interactive_block_ready():
8 bb.push(raw_input('>>> '))
9 print 'Input source was:\n', bb.source,
10 """
11 #-----------------------------------------------------------------------------
12 # Copyright (C) 2010 The IPython Development Team
13 #
14 # Distributed under the terms of the BSD License. The full license is in
15 # the file COPYING, distributed as part of this software.
16 #-----------------------------------------------------------------------------
17
18 #-----------------------------------------------------------------------------
19 # Imports
20 #-----------------------------------------------------------------------------
21 # stdlib
22 import codeop
23 import re
24 import sys
25
26 #-----------------------------------------------------------------------------
27 # Utilities
28 #-----------------------------------------------------------------------------
29
30 # compiled regexps for autoindent management
31 dedent_re = re.compile(r'^\s+raise|^\s+return|^\s+pass')
32 ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)')
33
34
35 def num_ini_spaces(s):
36 """Return the number of initial spaces in a string.
37
38 Note that tabs are counted as a single space. For now, we do *not* support
39 mixing of tabs and spaces in the user's input.
40
41 Parameters
42 ----------
43 s : string
44 """
45
46 ini_spaces = ini_spaces_re.match(s)
47 if ini_spaces:
48 return ini_spaces.end()
49 else:
50 return 0
51
52
53 def remove_comments(src):
54 """Remove all comments from input source.
55
56 Note: comments are NOT recognized inside of strings!
57
58 Parameters
59 ----------
60 src : string
61 A single or multiline input string.
62
63 Returns
64 -------
65 String with all Python comments removed.
66 """
67
68 return re.sub('#.*', '', src)
69
70
71 def get_input_encoding():
72 """Return the default standard input encoding."""
73 return getattr(sys.stdin, 'encoding', 'ascii')
74
75 #-----------------------------------------------------------------------------
76 # Classes and functions
77 #-----------------------------------------------------------------------------
78
79
80 class BlockBreaker(object):
81 # List
82 buffer = None
83 # Command compiler
84 compile = None
85 # Number of spaces of indentation
86 indent_spaces = 0
87 # String, indicating the default input encoding
88 encoding = ''
89 # String where the current full source input is stored, properly encoded
90 source = ''
91 # Code object corresponding to the current source
92 code = None
93 # Boolean indicating whether the current block is complete
94 is_complete = None
95
96 def __init__(self):
97 self.buffer = []
98 self.compile = codeop.CommandCompiler()
99 self.encoding = get_input_encoding()
100
101 def reset(self):
102 """Reset the input buffer and associated state."""
103 self.indent_spaces = 0
104 self.buffer[:] = []
105 self.source = ''
106
107 def get_source(self, reset=False):
108 """Return the input source.
109
110 Parameters
111 ----------
112 reset : boolean
113 If true, all state is reset and prior input forgotten.
114 """
115 out = self.source
116 if reset:
117 self.reset()
118 return out
119
120 def update_indent(self, lines):
121 """Keep track of the indent level."""
122
123 for line in remove_comments(lines).splitlines():
124
125 if line and not line.isspace():
126 if self.code is not None:
127 inisp = num_ini_spaces(line)
128 if inisp < self.indent_spaces:
129 self.indent_spaces = inisp
130
131 if line[-1] == ':':
132 self.indent_spaces += 4
133 elif dedent_re.match(line):
134 self.indent_spaces -= 4
135
136 def store(self, lines):
137 """Store one or more lines of input.
138
139 If input lines are not newline-terminated, a newline is automatically
140 appended."""
141
142 if lines.endswith('\n'):
143 self.buffer.append(lines)
144 else:
145 self.buffer.append(lines+'\n')
146 self.source = ''.join(self.buffer).encode(self.encoding)
147
148 def push(self, lines):
149 """Push one ore more lines of input.
150
151 This stores the given lines and returns a status code indicating
152 whether the code forms a complete Python block or not.
153
154 Any exceptions generated in compilation are allowed to propagate.
155
156 Parameters
157 ----------
158 lines : string
159 One or more lines of Python input.
160
161 Returns
162 -------
163 is_complete : boolean
164 True if the current input source (the result of the current input
165 plus prior inputs) forms a complete Python execution block. Note that
166 this value is also stored as an attribute so it can be queried at any
167 time.
168 """
169 # If the source code has leading blanks, add 'if 1:\n' to it
170 # this allows execution of indented pasted code. It is tempting
171 # to add '\n' at the end of source to run commands like ' a=1'
172 # directly, but this fails for more complicated scenarios
173 if not self.buffer and lines[:1] in [' ', '\t']:
174 lines = 'if 1:\n%s' % lines
175
176 self.store(lines)
177 source = self.source
178
179 # Before calling compile(), reset the code object to None so that if an
180 # exception is raised in compilation, we don't mislead by having
181 # inconsistent code/source attributes.
182 self.code, self.is_complete = None, None
183 self.code = self.compile(source)
184 # Compilation didn't produce any exceptions (though it may not have
185 # given a complete code object)
186 if self.code is None:
187 self.is_complete = False
188 else:
189 self.is_complete = True
190 self.update_indent(lines)
191 return self.is_complete
192
193 def interactive_block_ready(self):
194 """Return whether a block of interactive input is ready for execution.
195
196 This method is meant to be used by line-oriented frontends, who need to
197 guess whether a block is complete or not based solely on prior and
198 current input lines. The BlockBreaker considers it has a complete
199 interactive block when *all* of the following are true:
200
201 1. The input compiles to a complete statement.
202
203 2. The indentation level is flush-left (because if we are indented,
204 like inside a function definition or for loop, we need to keep
205 reading new input).
206
207 3. There is one extra line consisting only of whitespace.
208
209 Because of condition #3, this method should be used only by
210 *line-oriented* frontends, since it means that intermediate blank lines
211 are not allowed in function definitions (or any other indented block).
212
213 Block-oriented frontends that have a separate keyboard event to
214 indicate execution should use the :meth:`split_blocks` method instead.
215 """
216 if not self.is_complete:
217 return False
218 if self.indent_spaces==0:
219 return True
220 last_line = self.source.splitlines()[-1]
221 if not last_line or last_line.isspace():
222 return True
223 else:
224 return False
225
226
227 def split_blocks(self, lines):
228 """Split a multiline string into multiple input blocks"""
229
230 #-----------------------------------------------------------------------------
231 # Tests
232 #-----------------------------------------------------------------------------
233
234 import unittest
235
236 import nose.tools as nt
237
238
239 def test_spaces():
240 tests = [('', 0),
241 (' ', 1),
242 ('\n', 0),
243 (' \n', 1),
244 ('x', 0),
245 (' x', 1),
246 (' x',2),
247 (' x',4),
248 # Note: tabs are counted as a single whitespace!
249 ('\tx', 1),
250 ('\t x', 2),
251 ]
252
253 for s, nsp in tests:
254 nt.assert_equal(num_ini_spaces(s), nsp)
255
256
257 def test_remove_comments():
258 tests = [('text', 'text'),
259 ('text # comment', 'text '),
260 ('text # comment\n', 'text \n'),
261 ('text # comment \n', 'text \n'),
262 ('line # c \nline\n','line \nline\n'),
263 ('line # c \nline#c2 \nline\nline #c\n\n',
264 'line \nline\nline\nline \n\n'),
265 ]
266
267 for inp, out in tests:
268 nt.assert_equal(remove_comments(inp), out)
269
270
271 def test_get_input_encoding():
272 encoding = get_input_encoding()
273 nt.assert_true(isinstance(encoding, basestring))
274 # simple-minded check that at least encoding a simple string works with the
275 # encoding we got.
276 nt.assert_equal('test'.encode(encoding), 'test')
277
278
279 class BlockBreakerTestCase(unittest.TestCase):
280 def setUp(self):
281 self.bb = BlockBreaker()
282
283 def test_reset(self):
284 self.bb.store('hello')
285 self.bb.reset()
286 self.assertEqual(self.bb.buffer, [])
287 self.assertEqual(self.bb.indent_spaces, 0)
288 self.assertEqual(self.bb.get_source(), '')
289
290 def test_source(self):
291 self.bb.store('1')
292 self.bb.store('2')
293 out = self.bb.get_source()
294 self.assertEqual(out, '1\n2\n')
295 out = self.bb.get_source(reset=True)
296 self.assertEqual(out, '1\n2\n')
297 self.assertEqual(self.bb.buffer, [])
298 out = self.bb.get_source()
299 self.assertEqual(out, '')
300
301 def test_indent(self):
302 bb = self.bb # shorthand
303 bb.push('x=1')
304 self.assertEqual(bb.indent_spaces, 0)
305 bb.push('if 1:\n x=1')
306 self.assertEqual(bb.indent_spaces, 4)
307 bb.push('y=2\n')
308 self.assertEqual(bb.indent_spaces, 0)
309 bb.push('if 1:')
310 self.assertEqual(bb.indent_spaces, 4)
311 bb.push(' x=1')
312 self.assertEqual(bb.indent_spaces, 4)
313 # Blank lines shouldn't change the indent level
314 bb.push(' '*2)
315 self.assertEqual(bb.indent_spaces, 4)
316
317 def test_indent2(self):
318 bb = self.bb
319 # When a multiline statement contains parens or multiline strings, we
320 # shouldn't get confused.
321 bb.push("if 1:")
322 bb.push(" x = (1+\n 2)")
323 self.assertEqual(bb.indent_spaces, 4)
324
325 def test_dedent(self):
326 bb = self.bb # shorthand
327 bb.push('if 1:')
328 self.assertEqual(bb.indent_spaces, 4)
329 bb.push(' pass')
330 self.assertEqual(bb.indent_spaces, 0)
331
332 def test_push(self):
333 bb = self.bb
334 bb.push('x=1')
335 self.assertTrue(bb.is_complete)
336
337 def test_push2(self):
338 bb = self.bb
339 bb.push('if 1:')
340 self.assertFalse(bb.is_complete)
341 for line in [' x=1', '# a comment', ' y=2']:
342 bb.push(line)
343 self.assertTrue(bb.is_complete)
344
345 def test_push3(self):
346 """Test input with leading whitespace"""
347 bb = self.bb
348 bb.push(' x=1')
349 bb.push(' y=2')
350 self.assertEqual(bb.source, 'if 1:\n x=1\n y=2\n')
351
352 def test_interactive_block_ready(self):
353 bb = self.bb
354 bb.push('x=1')
355 self.assertTrue(bb.interactive_block_ready())
356
357 def test_interactive_block_ready2(self):
358 bb = self.bb
359 bb.push('if 1:\n x=1')
360 self.assertFalse(bb.interactive_block_ready())
361 bb.push('')
362 self.assertTrue(bb.interactive_block_ready())
363
364 def test_interactive_block_ready3(self):
365 bb = self.bb
366 bb.push("x = (2+\n3)")
367 self.assertTrue(bb.interactive_block_ready())
368
369 def test_interactive_block_ready4(self):
370 bb = self.bb
371 # When a multiline statement contains parens or multiline strings, we
372 # shouldn't get confused.
373 # FIXME: we should be able to better handle de-dents in statements like
374 # multiline strings and multiline expressions (continued with \ or
375 # parens). Right now we aren't handling the indentation tracking quite
376 # correctly with this, though in practice it may not be too much of a
377 # problem. We'll need to see.
378 bb.push("if 1:")
379 bb.push(" x = (2+")
380 bb.push(" 3)")
381 self.assertFalse(bb.interactive_block_ready())
382 bb.push(" y = 3")
383 self.assertFalse(bb.interactive_block_ready())
384 bb.push('')
385 self.assertTrue(bb.interactive_block_ready())
General Comments 0
You need to be logged in to leave comments. Login now