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