##// END OF EJS Templates
Completed full block splitting for block-based frontends.
Fernando Perez -
Show More
@@ -83,6 +83,8 b' class BlockBreaker(object):'
83 compile = None
83 compile = None
84 # Number of spaces of indentation
84 # Number of spaces of indentation
85 indent_spaces = 0
85 indent_spaces = 0
86 # Mark when input has changed indentation all the way back to flush-left
87 full_dedent = False
86 # String, indicating the default input encoding
88 # String, indicating the default input encoding
87 encoding = ''
89 encoding = ''
88 # String where the current full source input is stored, properly encoded
90 # String where the current full source input is stored, properly encoded
@@ -127,6 +129,8 b' class BlockBreaker(object):'
127 self._buffer[:] = []
129 self._buffer[:] = []
128 self.source = ''
130 self.source = ''
129 self.code = None
131 self.code = None
132 self.is_complete = False
133 self.full_dedent = False
130
134
131 def source_reset(self):
135 def source_reset(self):
132 """Return the input source and perform a full reset.
136 """Return the input source and perform a full reset.
@@ -173,6 +177,8 b' class BlockBreaker(object):'
173 # exception is raised in compilation, we don't mislead by having
177 # exception is raised in compilation, we don't mislead by having
174 # inconsistent code/source attributes.
178 # inconsistent code/source attributes.
175 self.code, self.is_complete = None, None
179 self.code, self.is_complete = None, None
180
181 self._update_indent(lines)
176 try:
182 try:
177 self.code = self.compile(source)
183 self.code = self.compile(source)
178 # Invalid syntax can produce any of a number of different errors from
184 # Invalid syntax can produce any of a number of different errors from
@@ -180,13 +186,13 b' class BlockBreaker(object):'
180 # immediately produce a 'ready' block, so the invalid Python can be
186 # immediately produce a 'ready' block, so the invalid Python can be
181 # sent to the kernel for evaluation with possible ipython
187 # sent to the kernel for evaluation with possible ipython
182 # special-syntax conversion.
188 # special-syntax conversion.
183 except (SyntaxError, OverflowError, ValueError, TypeError, MemoryError):
189 except (SyntaxError, OverflowError, ValueError, TypeError,
190 MemoryError):
184 self.is_complete = True
191 self.is_complete = True
185 else:
192 else:
186 # Compilation didn't produce any exceptions (though it may not have
193 # Compilation didn't produce any exceptions (though it may not have
187 # given a complete code object)
194 # given a complete code object)
188 self.is_complete = self.code is not None
195 self.is_complete = self.code is not None
189 self._update_indent(lines)
190
196
191 return self.is_complete
197 return self.is_complete
192
198
@@ -213,6 +219,10 b' class BlockBreaker(object):'
213 Block-oriented frontends that have a separate keyboard event to
219 Block-oriented frontends that have a separate keyboard event to
214 indicate execution should use the :meth:`split_blocks` method instead.
220 indicate execution should use the :meth:`split_blocks` method instead.
215 """
221 """
222 #print 'complete?', self.source # dbg
223 #if self.full_dedent:
224 # True
225
216 if not self.is_complete:
226 if not self.is_complete:
217 return False
227 return False
218 if self.indent_spaces==0:
228 if self.indent_spaces==0:
@@ -224,28 +234,129 b' class BlockBreaker(object):'
224 return False
234 return False
225
235
226 def split_blocks(self, lines):
236 def split_blocks(self, lines):
227 """Split a multiline string into multiple input blocks"""
237 """Split a multiline string into multiple input blocks.
228 raise NotImplementedError
238
239 Note: this method starts by performing a full reset().
240
241 Parameters
242 ----------
243 lines : str
244 A possibly multiline string.
245
246 Returns
247 -------
248 blocks : list
249 A list of strings, each possibly multiline. Each string corresponds
250 to a single block that can be compiled in 'single' mode (unless it
251 has a syntax error)."""
252
253 # This code is fairly delicate. If you make any changes here, make
254 # absolutely sure that you do run the full test suite and ALL tests
255 # pass.
256
257 self.reset()
258 blocks = []
259
260 # Reversed copy so we can use pop() efficiently and consume the input
261 # as a stack
262 lines = lines.splitlines()[::-1]
263 # Outer loop over all input
264 while lines:
265 # Inner loop to build each block
266 while True:
267 # Safety exit from inner loop
268 if not lines:
269 break
270 # Grab next line but don't push it yet
271 next_line = lines.pop()
272 # Blank/empty lines are pushed as-is
273 if not next_line or next_line.isspace():
274 self.push(next_line)
275 continue
276
277 # Check indentation changes caused by the *next* line
278 indent_spaces, full_dedent = self._find_indent(next_line)
279
280 # If the next line causes a dedent, it can be for two differnt
281 # reasons: either an explicit de-dent by the user or a
282 # return/raise/pass statement. These MUST be handled
283 # separately:
284 #
285 # 1. the first case is only detected when the actual explicit
286 # dedent happens, and that would be the *first* line of a *new*
287 # block. Thus, we must put the line back into the input buffer
288 # so that it starts a new block on the next pass.
289 #
290 # 2. the second case is detected in the line before the actual
291 # dedent happens, so , we consume the line and we can break out
292 # to start a new block.
293
294 # Case 1, explicit dedent causes a break
295 if full_dedent and not next_line.startswith(' '):
296 lines.append(next_line)
297 break
298
299 # Otherwise any line is pushed
300 self.push(next_line)
301
302 # Case 2, full dedent with full block ready:
303 if full_dedent or \
304 self.indent_spaces==0 and self.interactive_block_ready():
305 break
306 # Form the new block with the current source input
307 blocks.append(self.source_reset())
308
309 return blocks
229
310
230 #------------------------------------------------------------------------
311 #------------------------------------------------------------------------
231 # Private interface
312 # Private interface
232 #------------------------------------------------------------------------
313 #------------------------------------------------------------------------
233
234 def _update_indent(self, lines):
235 """Keep track of the indent level."""
236
314
237 for line in remove_comments(lines).splitlines():
315 def _find_indent(self, line):
316 """Compute the new indentation level for a single line.
317
318 Parameters
319 ----------
320 line : str
321 A single new line of non-whitespace, non-comment Python input.
322
323 Returns
324 -------
325 indent_spaces : int
326 New value for the indent level (it may be equal to self.indent_spaces
327 if indentation doesn't change.
328
329 full_dedent : boolean
330 Whether the new line causes a full flush-left dedent.
331 """
332 indent_spaces = self.indent_spaces
333 full_dedent = self.full_dedent
334
335 inisp = num_ini_spaces(line)
336 if inisp < indent_spaces:
337 indent_spaces = inisp
338 if indent_spaces <= 0:
339 #print 'Full dedent in text',self.source # dbg
340 full_dedent = True
341
342 if line[-1] == ':':
343 indent_spaces += 4
344 elif dedent_re.match(line):
345 indent_spaces -= 4
346 if indent_spaces <= 0:
347 full_dedent = True
348
349 # Safety
350 if indent_spaces < 0:
351 indent_spaces = 0
352 #print 'safety' # dbg
238
353
354 return indent_spaces, full_dedent
355
356 def _update_indent(self, lines):
357 for line in remove_comments(lines).splitlines():
239 if line and not line.isspace():
358 if line and not line.isspace():
240 if self.code is not None:
359 self.indent_spaces, self.full_dedent = self._find_indent(line)
241 inisp = num_ini_spaces(line)
242 if inisp < self.indent_spaces:
243 self.indent_spaces = inisp
244
245 if line[-1] == ':':
246 self.indent_spaces += 4
247 elif dedent_re.match(line):
248 self.indent_spaces -= 4
249
360
250 def _store(self, lines):
361 def _store(self, lines):
251 """Store one or more lines of input.
362 """Store one or more lines of input.
@@ -257,4 +368,8 b' class BlockBreaker(object):'
257 self._buffer.append(lines)
368 self._buffer.append(lines)
258 else:
369 else:
259 self._buffer.append(lines+'\n')
370 self._buffer.append(lines+'\n')
371 self._set_source()
372
373 def _set_source(self):
260 self.source = ''.join(self._buffer).encode(self.encoding)
374 self.source = ''.join(self._buffer).encode(self.encoding)
375
@@ -20,6 +20,14 b' import nose.tools as nt'
20 from IPython.core import blockbreaker as BB
20 from IPython.core import blockbreaker as BB
21
21
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23 # Test utilities, just for local use
24 #-----------------------------------------------------------------------------
25
26 def assemble(block):
27 """Assemble a block into multi-line sub-blocks."""
28 return ['\n'.join(sub_block)+'\n' for sub_block in block]
29
30 #-----------------------------------------------------------------------------
23 # Tests
31 # Tests
24 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
25 def test_spaces():
33 def test_spaces():
@@ -74,6 +82,7 b' class BlockBreakerTestCase(unittest.TestCase):'
74 self.assertEqual(bb.indent_spaces, 0)
82 self.assertEqual(bb.indent_spaces, 0)
75 self.assertEqual(bb.source, '')
83 self.assertEqual(bb.source, '')
76 self.assertEqual(bb.code, None)
84 self.assertEqual(bb.code, None)
85 self.assertEqual(bb.is_complete, False)
77
86
78 def test_source(self):
87 def test_source(self):
79 self.bb._store('1')
88 self.bb._store('1')
@@ -187,3 +196,81 b' class BlockBreakerTestCase(unittest.TestCase):'
187 # special-syntax conversion.
196 # special-syntax conversion.
188 bb.push('run foo')
197 bb.push('run foo')
189 self.assertTrue(bb.interactive_block_ready())
198 self.assertTrue(bb.interactive_block_ready())
199
200 def check_split(self, block_lines, compile=True):
201 blocks = assemble(block_lines)
202 lines = ''.join(blocks)
203 oblock = self.bb.split_blocks(lines)
204 self.assertEqual(oblock, blocks)
205 if compile:
206 for block in blocks:
207 self.bb.compile(block)
208
209 def test_split(self):
210 # All blocks of input we want to test in a list. The format for each
211 # block is a list of lists, with each inner lists consisting of all the
212 # lines (as single-lines) that should make up a sub-block.
213
214 # Note: do NOT put here sub-blocks that don't compile, as the
215 # check_split() routine makes a final verification pass to check that
216 # each sub_block, as returned by split_blocks(), does compile
217 # correctly.
218 all_blocks = [ [['x=1']],
219
220 [['x=1'],
221 ['y=2']],
222
223 [['x=1'],
224 ['# a comment'],
225 ['y=11']],
226
227 [['if 1:',
228 ' x=1'],
229 ['y=3']],
230
231 [['def f(x):',
232 ' return x'],
233 ['x=1']],
234
235 [['def f(x):',
236 ' x+=1',
237 ' ',
238 ' return x'],
239 ['x=1']],
240
241 [['def f(x):',
242 ' if x>0:',
243 ' y=1',
244 ' # a comment',
245 ' else:',
246 ' y=4',
247 ' ',
248 ' return y'],
249 ['x=1'],
250 ['if 1:',
251 ' y=11'] ],
252
253 [['for i in range(10):'
254 ' x=i**2']],
255
256 [['for i in range(10):'
257 ' x=i**2'],
258 ['z = 1']],
259 ]
260 for block_lines in all_blocks:
261 self.check_split(block_lines)
262
263 def test_split_syntax_errors(self):
264 # Block splitting with invalid syntax
265 all_blocks = [ [['a syntax error']],
266
267 [['x=1'],
268 ['a syntax error']],
269
270 [['for i in range(10):'
271 ' an error']],
272
273 ]
274 for block_lines in all_blocks:
275 self.check_split(block_lines, compile=False)
276
General Comments 0
You need to be logged in to leave comments. Login now