##// END OF EJS Templates
Merge branch 'blockbreaker' of git://github.com/fperez/ipython into qtfrontend
epatters -
r2637:ccbd4b37 merge
parent child Browse files
Show More
@@ -0,0 +1,189 b''
1 """Tests for the blockbreaker module.
2 """
3 #-----------------------------------------------------------------------------
4 # Copyright (C) 2010 The IPython Development Team
5 #
6 # Distributed under the terms of the BSD License. The full license is in
7 # the file COPYING, distributed as part of this software.
8 #-----------------------------------------------------------------------------
9
10 #-----------------------------------------------------------------------------
11 # Imports
12 #-----------------------------------------------------------------------------
13 # stdlib
14 import unittest
15
16 # Third party
17 import nose.tools as nt
18
19 # Our own
20 from IPython.core import blockbreaker as BB
21
22 #-----------------------------------------------------------------------------
23 # Tests
24 #-----------------------------------------------------------------------------
25 def test_spaces():
26 tests = [('', 0),
27 (' ', 1),
28 ('\n', 0),
29 (' \n', 1),
30 ('x', 0),
31 (' x', 1),
32 (' x',2),
33 (' x',4),
34 # Note: tabs are counted as a single whitespace!
35 ('\tx', 1),
36 ('\t x', 2),
37 ]
38
39 for s, nsp in tests:
40 nt.assert_equal(BB.num_ini_spaces(s), nsp)
41
42
43 def test_remove_comments():
44 tests = [('text', 'text'),
45 ('text # comment', 'text '),
46 ('text # comment\n', 'text \n'),
47 ('text # comment \n', 'text \n'),
48 ('line # c \nline\n','line \nline\n'),
49 ('line # c \nline#c2 \nline\nline #c\n\n',
50 'line \nline\nline\nline \n\n'),
51 ]
52
53 for inp, out in tests:
54 nt.assert_equal(BB.remove_comments(inp), out)
55
56
57 def test_get_input_encoding():
58 encoding = BB.get_input_encoding()
59 nt.assert_true(isinstance(encoding, basestring))
60 # simple-minded check that at least encoding a simple string works with the
61 # encoding we got.
62 nt.assert_equal('test'.encode(encoding), 'test')
63
64
65 class BlockBreakerTestCase(unittest.TestCase):
66 def setUp(self):
67 self.bb = BB.BlockBreaker()
68
69 def test_reset(self):
70 bb = self.bb
71 bb.push('x=1')
72 bb.reset()
73 self.assertEqual(bb._buffer, [])
74 self.assertEqual(bb.indent_spaces, 0)
75 self.assertEqual(bb.source, '')
76 self.assertEqual(bb.code, None)
77
78 def test_source(self):
79 self.bb._store('1')
80 self.bb._store('2')
81 self.assertEqual(self.bb.source, '1\n2\n')
82 self.assertTrue(len(self.bb._buffer)>0)
83 self.assertEqual(self.bb.source_reset(), '1\n2\n')
84 self.assertEqual(self.bb._buffer, [])
85 self.assertEqual(self.bb.source, '')
86
87 def test_indent(self):
88 bb = self.bb # shorthand
89 bb.push('x=1')
90 self.assertEqual(bb.indent_spaces, 0)
91 bb.push('if 1:\n x=1')
92 self.assertEqual(bb.indent_spaces, 4)
93 bb.push('y=2\n')
94 self.assertEqual(bb.indent_spaces, 0)
95 bb.push('if 1:')
96 self.assertEqual(bb.indent_spaces, 4)
97 bb.push(' x=1')
98 self.assertEqual(bb.indent_spaces, 4)
99 # Blank lines shouldn't change the indent level
100 bb.push(' '*2)
101 self.assertEqual(bb.indent_spaces, 4)
102
103 def test_indent2(self):
104 bb = self.bb
105 # When a multiline statement contains parens or multiline strings, we
106 # shouldn't get confused.
107 bb.push("if 1:")
108 bb.push(" x = (1+\n 2)")
109 self.assertEqual(bb.indent_spaces, 4)
110
111 def test_dedent(self):
112 bb = self.bb # shorthand
113 bb.push('if 1:')
114 self.assertEqual(bb.indent_spaces, 4)
115 bb.push(' pass')
116 self.assertEqual(bb.indent_spaces, 0)
117
118 def test_push(self):
119 bb = self.bb
120 bb.push('x=1')
121 self.assertTrue(bb.is_complete)
122
123 def test_push2(self):
124 bb = self.bb
125 bb.push('if 1:')
126 self.assertFalse(bb.is_complete)
127 for line in [' x=1', '# a comment', ' y=2']:
128 bb.push(line)
129 self.assertTrue(bb.is_complete)
130
131 def test_push3(self):
132 """Test input with leading whitespace"""
133 bb = self.bb
134 bb.push(' x=1')
135 bb.push(' y=2')
136 self.assertEqual(bb.source, 'if 1:\n x=1\n y=2\n')
137
138 def test_replace_mode(self):
139 bb = self.bb
140 bb.input_mode = 'replace'
141 bb.push('x=1')
142 self.assertEqual(bb.source, 'x=1\n')
143 bb.push('x=2')
144 self.assertEqual(bb.source, 'x=2\n')
145
146 def test_interactive_block_ready(self):
147 bb = self.bb
148 bb.push('x=1')
149 self.assertTrue(bb.interactive_block_ready())
150
151 def test_interactive_block_ready2(self):
152 bb = self.bb
153 bb.push('if 1:')
154 self.assertFalse(bb.interactive_block_ready())
155 bb.push(' x=1')
156 self.assertFalse(bb.interactive_block_ready())
157 bb.push('')
158 self.assertTrue(bb.interactive_block_ready())
159
160 def test_interactive_block_ready3(self):
161 bb = self.bb
162 bb.push("x = (2+\n3)")
163 self.assertTrue(bb.interactive_block_ready())
164
165 def test_interactive_block_ready4(self):
166 bb = self.bb
167 # When a multiline statement contains parens or multiline strings, we
168 # shouldn't get confused.
169 # FIXME: we should be able to better handle de-dents in statements like
170 # multiline strings and multiline expressions (continued with \ or
171 # 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
173 # problem. We'll need to see.
174 bb.push("if 1:")
175 bb.push(" x = (2+")
176 bb.push(" 3)")
177 self.assertFalse(bb.interactive_block_ready())
178 bb.push(" y = 3")
179 self.assertFalse(bb.interactive_block_ready())
180 bb.push('')
181 self.assertTrue(bb.interactive_block_ready())
182
183 def test_syntax_error(self):
184 bb = self.bb
185 # Syntax errors immediately produce a 'ready' block, so the invalid
186 # Python can be sent to the kernel for evaluation with possible ipython
187 # special-syntax conversion.
188 bb.push('run foo')
189 self.assertTrue(bb.interactive_block_ready())
@@ -27,6 +27,8 b' import sys'
27 # Utilities
27 # Utilities
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29
29
30 # FIXME: move these utilities to the general ward...
31
30 # compiled regexps for autoindent management
32 # compiled regexps for autoindent management
31 dedent_re = re.compile(r'^\s+raise|^\s+return|^\s+pass')
33 dedent_re = re.compile(r'^\s+raise|^\s+return|^\s+pass')
32 ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)')
34 ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)')
@@ -76,10 +78,7 b' def get_input_encoding():'
76 # Classes and functions
78 # Classes and functions
77 #-----------------------------------------------------------------------------
79 #-----------------------------------------------------------------------------
78
80
79
80 class BlockBreaker(object):
81 class BlockBreaker(object):
81 # List
82 buffer = None
83 # Command compiler
82 # Command compiler
84 compile = None
83 compile = None
85 # Number of spaces of indentation
84 # Number of spaces of indentation
@@ -92,59 +91,50 b' class BlockBreaker(object):'
92 code = None
91 code = None
93 # Boolean indicating whether the current block is complete
92 # Boolean indicating whether the current block is complete
94 is_complete = None
93 is_complete = None
94 # Input mode
95 input_mode = 'append'
96
97 # Private attributes
98
99 # List
100 _buffer = None
95
101
96 def __init__(self):
102 def __init__(self, input_mode=None):
97 self.buffer = []
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 """
118 self._buffer = []
98 self.compile = codeop.CommandCompiler()
119 self.compile = codeop.CommandCompiler()
99 self.encoding = get_input_encoding()
120 self.encoding = get_input_encoding()
121 self.input_mode = BlockBreaker.input_mode if input_mode is None \
122 else input_mode
100
123
101 def reset(self):
124 def reset(self):
102 """Reset the input buffer and associated state."""
125 """Reset the input buffer and associated state."""
103 self.indent_spaces = 0
126 self.indent_spaces = 0
104 self.buffer[:] = []
127 self._buffer[:] = []
105 self.source = ''
128 self.source = ''
129 self.code = None
106
130
107 def get_source(self, reset=False):
131 def source_reset(self):
108 """Return the input source.
132 """Return the input source and perform a full reset.
109
110 Parameters
111 ----------
112 reset : boolean
113 If true, all state is reset and prior input forgotten.
114 """
133 """
115 out = self.source
134 out = self.source
116 if reset:
135 self.reset()
117 self.reset()
118 return out
136 return out
119
137
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):
138 def push(self, lines):
149 """Push one ore more lines of input.
139 """Push one ore more lines of input.
150
140
@@ -166,28 +156,38 b' class BlockBreaker(object):'
166 this value is also stored as an attribute so it can be queried at any
156 this value is also stored as an attribute so it can be queried at any
167 time.
157 time.
168 """
158 """
159 if self.input_mode == 'replace':
160 self.reset()
161
169 # If the source code has leading blanks, add 'if 1:\n' to it
162 # If the source code has leading blanks, add 'if 1:\n' to it
170 # this allows execution of indented pasted code. It is tempting
163 # 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'
164 # to add '\n' at the end of source to run commands like ' a=1'
172 # directly, but this fails for more complicated scenarios
165 # directly, but this fails for more complicated scenarios
173 if not self.buffer and lines[:1] in [' ', '\t']:
166 if not self._buffer and lines[:1] in [' ', '\t']:
174 lines = 'if 1:\n%s' % lines
167 lines = 'if 1:\n%s' % lines
175
168
176 self.store(lines)
169 self._store(lines)
177 source = self.source
170 source = self.source
178
171
179 # Before calling compile(), reset the code object to None so that if an
172 # 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
173 # exception is raised in compilation, we don't mislead by having
181 # inconsistent code/source attributes.
174 # inconsistent code/source attributes.
182 self.code, self.is_complete = None, None
175 self.code, self.is_complete = None, None
183 self.code = self.compile(source)
176 try:
184 # Compilation didn't produce any exceptions (though it may not have
177 self.code = self.compile(source)
185 # given a complete code object)
178 # Invalid syntax can produce any of a number of different errors from
186 if self.code is None:
179 # inside the compiler, so we have to catch them all. Syntax errors
187 self.is_complete = False
180 # immediately produce a 'ready' block, so the invalid Python can be
188 else:
181 # sent to the kernel for evaluation with possible ipython
182 # special-syntax conversion.
183 except (SyntaxError, OverflowError, ValueError, TypeError, MemoryError):
189 self.is_complete = True
184 self.is_complete = True
190 self.update_indent(lines)
185 else:
186 # Compilation didn't produce any exceptions (though it may not have
187 # given a complete code object)
188 self.is_complete = self.code is not None
189 self._update_indent(lines)
190
191 return self.is_complete
191 return self.is_complete
192
192
193 def interactive_block_ready(self):
193 def interactive_block_ready(self):
@@ -223,163 +223,38 b' class BlockBreaker(object):'
223 else:
223 else:
224 return False
224 return False
225
225
226
227 def split_blocks(self, lines):
226 def split_blocks(self, lines):
228 """Split a multiline string into multiple input blocks"""
227 """Split a multiline string into multiple input blocks"""
228 raise NotImplementedError
229
229
230 #-----------------------------------------------------------------------------
230 #------------------------------------------------------------------------
231 # Tests
231 # Private interface
232 #-----------------------------------------------------------------------------
232 #------------------------------------------------------------------------
233
234 def _update_indent(self, lines):
235 """Keep track of the indent level."""
236
237 for line in remove_comments(lines).splitlines():
238
239 if line and not line.isspace():
240 if self.code is not None:
241 inisp = num_ini_spaces(line)
242 if inisp < self.indent_spaces:
243 self.indent_spaces = inisp
233
244
234 import unittest
245 if line[-1] == ':':
246 self.indent_spaces += 4
247 elif dedent_re.match(line):
248 self.indent_spaces -= 4
235
249
236 import nose.tools as nt
250 def _store(self, lines):
251 """Store one or more lines of input.
237
252
238
253 If input lines are not newline-terminated, a newline is automatically
239 def test_spaces():
254 appended."""
240 tests = [('', 0),
255
241 (' ', 1),
256 if lines.endswith('\n'):
242 ('\n', 0),
257 self._buffer.append(lines)
243 (' \n', 1),
258 else:
244 ('x', 0),
259 self._buffer.append(lines+'\n')
245 (' x', 1),
260 self.source = ''.join(self._buffer).encode(self.encoding)
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