##// 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 27 # Utilities
28 28 #-----------------------------------------------------------------------------
29 29
30 # FIXME: move these utilities to the general ward...
31
30 32 # compiled regexps for autoindent management
31 33 dedent_re = re.compile(r'^\s+raise|^\s+return|^\s+pass')
32 34 ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)')
@@ -76,10 +78,7 b' def get_input_encoding():'
76 78 # Classes and functions
77 79 #-----------------------------------------------------------------------------
78 80
79
80 81 class BlockBreaker(object):
81 # List
82 buffer = None
83 82 # Command compiler
84 83 compile = None
85 84 # Number of spaces of indentation
@@ -92,59 +91,50 b' class BlockBreaker(object):'
92 91 code = None
93 92 # Boolean indicating whether the current block is complete
94 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):
97 self.buffer = []
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 """
118 self._buffer = []
98 119 self.compile = codeop.CommandCompiler()
99 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 124 def reset(self):
102 125 """Reset the input buffer and associated state."""
103 126 self.indent_spaces = 0
104 self.buffer[:] = []
127 self._buffer[:] = []
105 128 self.source = ''
129 self.code = None
106 130
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.
131 def source_reset(self):
132 """Return the input source and perform a full reset.
114 133 """
115 134 out = self.source
116 if reset:
117 self.reset()
135 self.reset()
118 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 138 def push(self, lines):
149 139 """Push one ore more lines of input.
150 140
@@ -166,28 +156,38 b' class BlockBreaker(object):'
166 156 this value is also stored as an attribute so it can be queried at any
167 157 time.
168 158 """
159 if self.input_mode == 'replace':
160 self.reset()
161
169 162 # If the source code has leading blanks, add 'if 1:\n' to it
170 163 # this allows execution of indented pasted code. It is tempting
171 164 # to add '\n' at the end of source to run commands like ' a=1'
172 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 167 lines = 'if 1:\n%s' % lines
175 168
176 self.store(lines)
169 self._store(lines)
177 170 source = self.source
178 171
179 172 # Before calling compile(), reset the code object to None so that if an
180 173 # exception is raised in compilation, we don't mislead by having
181 174 # inconsistent code/source attributes.
182 175 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:
176 try:
177 self.code = self.compile(source)
178 # 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
180 # immediately produce a 'ready' block, so the invalid Python can be
181 # sent to the kernel for evaluation with possible ipython
182 # special-syntax conversion.
183 except (SyntaxError, OverflowError, ValueError, TypeError, MemoryError):
189 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 191 return self.is_complete
192 192
193 193 def interactive_block_ready(self):
@@ -223,163 +223,38 b' class BlockBreaker(object):'
223 223 else:
224 224 return False
225 225
226
227 226 def split_blocks(self, lines):
228 227 """Split a multiline string into multiple input blocks"""
228 raise NotImplementedError
229 229
230 #-----------------------------------------------------------------------------
231 # Tests
232 #-----------------------------------------------------------------------------
230 #------------------------------------------------------------------------
231 # Private interface
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
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())
253 If input lines are not newline-terminated, a newline is automatically
254 appended."""
255
256 if lines.endswith('\n'):
257 self._buffer.append(lines)
258 else:
259 self._buffer.append(lines+'\n')
260 self.source = ''.join(self._buffer).encode(self.encoding)
General Comments 0
You need to be logged in to leave comments. Login now