##// END OF EJS Templates
Split blockbreaker tests into a separate file and clean up api....
Fernando Perez -
Show More
@@ -0,0 +1,173 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.get_source(), '')
76 self.assertEqual(bb.code, None)
77
78 def test_source(self):
79 self.bb._store('1')
80 self.bb._store('2')
81 out = self.bb.get_source()
82 self.assertEqual(out, '1\n2\n')
83 out = self.bb.get_source(reset=True)
84 self.assertEqual(out, '1\n2\n')
85 self.assertEqual(self.bb._buffer, [])
86 out = self.bb.get_source()
87 self.assertEqual(out, '')
88
89 def test_indent(self):
90 bb = self.bb # shorthand
91 bb.push('x=1')
92 self.assertEqual(bb.indent_spaces, 0)
93 bb.push('if 1:\n x=1')
94 self.assertEqual(bb.indent_spaces, 4)
95 bb.push('y=2\n')
96 self.assertEqual(bb.indent_spaces, 0)
97 bb.push('if 1:')
98 self.assertEqual(bb.indent_spaces, 4)
99 bb.push(' x=1')
100 self.assertEqual(bb.indent_spaces, 4)
101 # Blank lines shouldn't change the indent level
102 bb.push(' '*2)
103 self.assertEqual(bb.indent_spaces, 4)
104
105 def test_indent2(self):
106 bb = self.bb
107 # When a multiline statement contains parens or multiline strings, we
108 # shouldn't get confused.
109 bb.push("if 1:")
110 bb.push(" x = (1+\n 2)")
111 self.assertEqual(bb.indent_spaces, 4)
112
113 def test_dedent(self):
114 bb = self.bb # shorthand
115 bb.push('if 1:')
116 self.assertEqual(bb.indent_spaces, 4)
117 bb.push(' pass')
118 self.assertEqual(bb.indent_spaces, 0)
119
120 def test_push(self):
121 bb = self.bb
122 bb.push('x=1')
123 self.assertTrue(bb.is_complete)
124
125 def test_push2(self):
126 bb = self.bb
127 bb.push('if 1:')
128 self.assertFalse(bb.is_complete)
129 for line in [' x=1', '# a comment', ' y=2']:
130 bb.push(line)
131 self.assertTrue(bb.is_complete)
132
133 def test_push3(self):
134 """Test input with leading whitespace"""
135 bb = self.bb
136 bb.push(' x=1')
137 bb.push(' y=2')
138 self.assertEqual(bb.source, 'if 1:\n x=1\n y=2\n')
139
140 def test_interactive_block_ready(self):
141 bb = self.bb
142 bb.push('x=1')
143 self.assertTrue(bb.interactive_block_ready())
144
145 def test_interactive_block_ready2(self):
146 bb = self.bb
147 bb.push('if 1:\n x=1')
148 self.assertFalse(bb.interactive_block_ready())
149 bb.push('')
150 self.assertTrue(bb.interactive_block_ready())
151
152 def test_interactive_block_ready3(self):
153 bb = self.bb
154 bb.push("x = (2+\n3)")
155 self.assertTrue(bb.interactive_block_ready())
156
157 def test_interactive_block_ready4(self):
158 bb = self.bb
159 # When a multiline statement contains parens or multiline strings, we
160 # shouldn't get confused.
161 # FIXME: we should be able to better handle de-dents in statements like
162 # multiline strings and multiline expressions (continued with \ or
163 # parens). Right now we aren't handling the indentation tracking quite
164 # correctly with this, though in practice it may not be too much of a
165 # problem. We'll need to see.
166 bb.push("if 1:")
167 bb.push(" x = (2+")
168 bb.push(" 3)")
169 self.assertFalse(bb.interactive_block_ready())
170 bb.push(" y = 3")
171 self.assertFalse(bb.interactive_block_ready())
172 bb.push('')
173 self.assertTrue(bb.interactive_block_ready())
@@ -1,385 +1,237 b''
1 1 """Analysis of text input into executable blocks.
2 2
3 3 This is a simple example of how an interactive terminal-based client can use
4 4 this tool::
5 5
6 6 bb = BlockBreaker()
7 7 while not bb.interactive_block_ready():
8 8 bb.push(raw_input('>>> '))
9 9 print 'Input source was:\n', bb.source,
10 10 """
11 11 #-----------------------------------------------------------------------------
12 12 # Copyright (C) 2010 The IPython Development Team
13 13 #
14 14 # Distributed under the terms of the BSD License. The full license is in
15 15 # the file COPYING, distributed as part of this software.
16 16 #-----------------------------------------------------------------------------
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Imports
20 20 #-----------------------------------------------------------------------------
21 21 # stdlib
22 22 import codeop
23 23 import re
24 24 import sys
25 25
26 26 #-----------------------------------------------------------------------------
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]+)')
33 35
34 36
35 37 def num_ini_spaces(s):
36 38 """Return the number of initial spaces in a string.
37 39
38 40 Note that tabs are counted as a single space. For now, we do *not* support
39 41 mixing of tabs and spaces in the user's input.
40 42
41 43 Parameters
42 44 ----------
43 45 s : string
44 46 """
45 47
46 48 ini_spaces = ini_spaces_re.match(s)
47 49 if ini_spaces:
48 50 return ini_spaces.end()
49 51 else:
50 52 return 0
51 53
52 54
53 55 def remove_comments(src):
54 56 """Remove all comments from input source.
55 57
56 58 Note: comments are NOT recognized inside of strings!
57 59
58 60 Parameters
59 61 ----------
60 62 src : string
61 63 A single or multiline input string.
62 64
63 65 Returns
64 66 -------
65 67 String with all Python comments removed.
66 68 """
67 69
68 70 return re.sub('#.*', '', src)
69 71
70 72
71 73 def get_input_encoding():
72 74 """Return the default standard input encoding."""
73 75 return getattr(sys.stdin, 'encoding', 'ascii')
74 76
75 77 #-----------------------------------------------------------------------------
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
86 85 indent_spaces = 0
87 86 # String, indicating the default input encoding
88 87 encoding = ''
89 88 # String where the current full source input is stored, properly encoded
90 89 source = ''
91 90 # Code object corresponding to the current source
92 91 code = None
93 92 # Boolean indicating whether the current block is complete
94 93 is_complete = None
94
95 # Private attributes
96
97 # List
98 _buffer = None
95 99
96 100 def __init__(self):
97 self.buffer = []
101 self._buffer = []
98 102 self.compile = codeop.CommandCompiler()
99 103 self.encoding = get_input_encoding()
100 104
101 105 def reset(self):
102 106 """Reset the input buffer and associated state."""
103 107 self.indent_spaces = 0
104 self.buffer[:] = []
108 self._buffer[:] = []
105 109 self.source = ''
110 self.code = None
106 111
107 112 def get_source(self, reset=False):
108 113 """Return the input source.
109 114
110 115 Parameters
111 116 ----------
112 117 reset : boolean
113 118 If true, all state is reset and prior input forgotten.
114 119 """
115 120 out = self.source
116 121 if reset:
117 122 self.reset()
118 123 return out
119 124
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 125 def push(self, lines):
149 126 """Push one ore more lines of input.
150 127
151 128 This stores the given lines and returns a status code indicating
152 129 whether the code forms a complete Python block or not.
153 130
154 131 Any exceptions generated in compilation are allowed to propagate.
155 132
156 133 Parameters
157 134 ----------
158 135 lines : string
159 136 One or more lines of Python input.
160 137
161 138 Returns
162 139 -------
163 140 is_complete : boolean
164 141 True if the current input source (the result of the current input
165 142 plus prior inputs) forms a complete Python execution block. Note that
166 143 this value is also stored as an attribute so it can be queried at any
167 144 time.
168 145 """
169 146 # If the source code has leading blanks, add 'if 1:\n' to it
170 147 # this allows execution of indented pasted code. It is tempting
171 148 # to add '\n' at the end of source to run commands like ' a=1'
172 149 # directly, but this fails for more complicated scenarios
173 if not self.buffer and lines[:1] in [' ', '\t']:
150 if not self._buffer and lines[:1] in [' ', '\t']:
174 151 lines = 'if 1:\n%s' % lines
175 152
176 self.store(lines)
153 self._store(lines)
177 154 source = self.source
178 155
179 156 # Before calling compile(), reset the code object to None so that if an
180 157 # exception is raised in compilation, we don't mislead by having
181 158 # inconsistent code/source attributes.
182 159 self.code, self.is_complete = None, None
183 160 self.code = self.compile(source)
184 161 # Compilation didn't produce any exceptions (though it may not have
185 162 # given a complete code object)
186 163 if self.code is None:
187 164 self.is_complete = False
188 165 else:
189 166 self.is_complete = True
190 self.update_indent(lines)
167 self._update_indent(lines)
191 168 return self.is_complete
192 169
193 170 def interactive_block_ready(self):
194 171 """Return whether a block of interactive input is ready for execution.
195 172
196 173 This method is meant to be used by line-oriented frontends, who need to
197 174 guess whether a block is complete or not based solely on prior and
198 175 current input lines. The BlockBreaker considers it has a complete
199 176 interactive block when *all* of the following are true:
200 177
201 178 1. The input compiles to a complete statement.
202 179
203 180 2. The indentation level is flush-left (because if we are indented,
204 181 like inside a function definition or for loop, we need to keep
205 182 reading new input).
206 183
207 184 3. There is one extra line consisting only of whitespace.
208 185
209 186 Because of condition #3, this method should be used only by
210 187 *line-oriented* frontends, since it means that intermediate blank lines
211 188 are not allowed in function definitions (or any other indented block).
212 189
213 190 Block-oriented frontends that have a separate keyboard event to
214 191 indicate execution should use the :meth:`split_blocks` method instead.
215 192 """
216 193 if not self.is_complete:
217 194 return False
218 195 if self.indent_spaces==0:
219 196 return True
220 197 last_line = self.source.splitlines()[-1]
221 198 if not last_line or last_line.isspace():
222 199 return True
223 200 else:
224 201 return False
225 202
226
227 203 def split_blocks(self, lines):
228 204 """Split a multiline string into multiple input blocks"""
205 raise NotImplementedError
229 206
230 #-----------------------------------------------------------------------------
231 # Tests
232 #-----------------------------------------------------------------------------
207 #------------------------------------------------------------------------
208 # Private interface
209 #------------------------------------------------------------------------
210
211 def _update_indent(self, lines):
212 """Keep track of the indent level."""
233 213
234 import unittest
214 for line in remove_comments(lines).splitlines():
215
216 if line and not line.isspace():
217 if self.code is not None:
218 inisp = num_ini_spaces(line)
219 if inisp < self.indent_spaces:
220 self.indent_spaces = inisp
235 221
236 import nose.tools as nt
222 if line[-1] == ':':
223 self.indent_spaces += 4
224 elif dedent_re.match(line):
225 self.indent_spaces -= 4
237 226
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())
227 def _store(self, lines):
228 """Store one or more lines of input.
229
230 If input lines are not newline-terminated, a newline is automatically
231 appended."""
232
233 if lines.endswith('\n'):
234 self._buffer.append(lines)
235 else:
236 self._buffer.append(lines+'\n')
237 self.source = ''.join(self._buffer).encode(self.encoding)
General Comments 0
You need to be logged in to leave comments. Login now