##// END OF EJS Templates
First pass of input syntax transformation support
Fernando Perez -
Show More
@@ -1,421 +1,521 b''
1 1 """Analysis of text input into executable blocks.
2 2
3 3 The main class in this module, :class:`InputSplitter`, is designed to break
4 4 input from either interactive, line-by-line environments or block-based ones,
5 5 into standalone blocks that can be executed by Python as 'single' statements
6 6 (thus triggering sys.displayhook).
7 7
8 8 For more details, see the class docstring below.
9 9 """
10 10 #-----------------------------------------------------------------------------
11 11 # Copyright (C) 2010 The IPython Development Team
12 12 #
13 13 # Distributed under the terms of the BSD License. The full license is in
14 14 # the file COPYING, distributed as part of this software.
15 15 #-----------------------------------------------------------------------------
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Imports
19 19 #-----------------------------------------------------------------------------
20 20 # stdlib
21 21 import codeop
22 22 import re
23 23 import sys
24 24
25 # IPython modules
26 from IPython.utils.text import make_quoted_expr
27
25 28 #-----------------------------------------------------------------------------
26 29 # Utilities
27 30 #-----------------------------------------------------------------------------
28 31
29 32 # FIXME: move these utilities to the general ward...
30 33
31 34 # compiled regexps for autoindent management
32 35 dedent_re = re.compile(r'^\s+raise|^\s+return|^\s+pass')
33 36 ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)')
34 37
35 38
36 39 def num_ini_spaces(s):
37 40 """Return the number of initial spaces in a string.
38 41
39 42 Note that tabs are counted as a single space. For now, we do *not* support
40 43 mixing of tabs and spaces in the user's input.
41 44
42 45 Parameters
43 46 ----------
44 47 s : string
45 48
46 49 Returns
47 50 -------
48 51 n : int
49 52 """
50 53
51 54 ini_spaces = ini_spaces_re.match(s)
52 55 if ini_spaces:
53 56 return ini_spaces.end()
54 57 else:
55 58 return 0
56 59
57 60
58 61 def remove_comments(src):
59 62 """Remove all comments from input source.
60 63
61 64 Note: comments are NOT recognized inside of strings!
62 65
63 66 Parameters
64 67 ----------
65 68 src : string
66 69 A single or multiline input string.
67 70
68 71 Returns
69 72 -------
70 73 String with all Python comments removed.
71 74 """
72 75
73 76 return re.sub('#.*', '', src)
74 77
75 78
76 79 def get_input_encoding():
77 80 """Return the default standard input encoding.
78 81
79 82 If sys.stdin has no encoding, 'ascii' is returned."""
80 83 # There are strange environments for which sys.stdin.encoding is None. We
81 84 # ensure that a valid encoding is returned.
82 85 encoding = getattr(sys.stdin, 'encoding', None)
83 86 if encoding is None:
84 87 encoding = 'ascii'
85 88 return encoding
86 89
87 90 #-----------------------------------------------------------------------------
88 91 # Classes and functions
89 92 #-----------------------------------------------------------------------------
90 93
91 94 class InputSplitter(object):
92 95 """An object that can split Python source input in executable blocks.
93 96
94 97 This object is designed to be used in one of two basic modes:
95 98
96 99 1. By feeding it python source line-by-line, using :meth:`push`. In this
97 100 mode, it will return on each push whether the currently pushed code
98 101 could be executed already. In addition, it provides a method called
99 102 :meth:`push_accepts_more` that can be used to query whether more input
100 103 can be pushed into a single interactive block.
101 104
102 105 2. By calling :meth:`split_blocks` with a single, multiline Python string,
103 106 that is then split into blocks each of which can be executed
104 107 interactively as a single statement.
105 108
106 109 This is a simple example of how an interactive terminal-based client can use
107 110 this tool::
108 111
109 112 isp = InputSplitter()
110 113 while isp.push_accepts_more():
111 114 indent = ' '*isp.indent_spaces
112 115 prompt = '>>> ' + indent
113 116 line = indent + raw_input(prompt)
114 117 isp.push(line)
115 118 print 'Input source was:\n', isp.source_reset(),
116 119 """
117 120 # Number of spaces of indentation computed from input that has been pushed
118 121 # so far. This is the attributes callers should query to get the current
119 122 # indentation level, in order to provide auto-indent facilities.
120 123 indent_spaces = 0
121 124 # String, indicating the default input encoding. It is computed by default
122 125 # at initialization time via get_input_encoding(), but it can be reset by a
123 126 # client with specific knowledge of the encoding.
124 127 encoding = ''
125 128 # String where the current full source input is stored, properly encoded.
126 129 # Reading this attribute is the normal way of querying the currently pushed
127 130 # source code, that has been properly encoded.
128 131 source = ''
129 132 # Code object corresponding to the current source. It is automatically
130 133 # synced to the source, so it can be queried at any time to obtain the code
131 134 # object; it will be None if the source doesn't compile to valid Python.
132 135 code = None
133 136 # Input mode
134 137 input_mode = 'append'
135 138
136 139 # Private attributes
137 140
138 141 # List with lines of input accumulated so far
139 142 _buffer = None
140 143 # Command compiler
141 144 _compile = None
142 145 # Mark when input has changed indentation all the way back to flush-left
143 146 _full_dedent = False
144 147 # Boolean indicating whether the current block is complete
145 148 _is_complete = None
146 149
147 150 def __init__(self, input_mode=None):
148 151 """Create a new InputSplitter instance.
149 152
150 153 Parameters
151 154 ----------
152 155 input_mode : str
153 156
154 157 One of 'append', 'replace', default is 'append'. This controls how
155 158 new inputs are used: in 'append' mode, they are appended to the
156 159 existing buffer and the whole buffer is compiled; in 'replace' mode,
157 160 each new input completely replaces all prior inputs. Replace mode is
158 161 thus equivalent to prepending a full reset() to every push() call.
159 162
160 163 In practice, line-oriented clients likely want to use 'append' mode
161 164 while block-oriented ones will want to use 'replace'.
162 165 """
163 166 self._buffer = []
164 167 self._compile = codeop.CommandCompiler()
165 168 self.encoding = get_input_encoding()
166 169 self.input_mode = InputSplitter.input_mode if input_mode is None \
167 170 else input_mode
168 171
169 172 def reset(self):
170 173 """Reset the input buffer and associated state."""
171 174 self.indent_spaces = 0
172 175 self._buffer[:] = []
173 176 self.source = ''
174 177 self.code = None
175 178 self._is_complete = False
176 179 self._full_dedent = False
177 180
178 181 def source_reset(self):
179 182 """Return the input source and perform a full reset.
180 183 """
181 184 out = self.source
182 185 self.reset()
183 186 return out
184 187
185 188 def push(self, lines):
186 189 """Push one ore more lines of input.
187 190
188 191 This stores the given lines and returns a status code indicating
189 192 whether the code forms a complete Python block or not.
190 193
191 194 Any exceptions generated in compilation are swallowed, but if an
192 195 exception was produced, the method returns True.
193 196
194 197 Parameters
195 198 ----------
196 199 lines : string
197 200 One or more lines of Python input.
198 201
199 202 Returns
200 203 -------
201 204 is_complete : boolean
202 205 True if the current input source (the result of the current input
203 206 plus prior inputs) forms a complete Python execution block. Note that
204 207 this value is also stored as a private attribute (_is_complete), so it
205 208 can be queried at any time.
206 209 """
207 210 if self.input_mode == 'replace':
208 211 self.reset()
209 212
210 213 # If the source code has leading blanks, add 'if 1:\n' to it
211 214 # this allows execution of indented pasted code. It is tempting
212 215 # to add '\n' at the end of source to run commands like ' a=1'
213 216 # directly, but this fails for more complicated scenarios
214 217 if not self._buffer and lines[:1] in [' ', '\t']:
215 218 lines = 'if 1:\n%s' % lines
216 219
217 220 self._store(lines)
218 221 source = self.source
219 222
220 223 # Before calling _compile(), reset the code object to None so that if an
221 224 # exception is raised in compilation, we don't mislead by having
222 225 # inconsistent code/source attributes.
223 226 self.code, self._is_complete = None, None
224 227
225 228 self._update_indent(lines)
226 229 try:
227 230 self.code = self._compile(source)
228 231 # Invalid syntax can produce any of a number of different errors from
229 232 # inside the compiler, so we have to catch them all. Syntax errors
230 233 # immediately produce a 'ready' block, so the invalid Python can be
231 234 # sent to the kernel for evaluation with possible ipython
232 235 # special-syntax conversion.
233 236 except (SyntaxError, OverflowError, ValueError, TypeError,
234 237 MemoryError):
235 238 self._is_complete = True
236 239 else:
237 240 # Compilation didn't produce any exceptions (though it may not have
238 241 # given a complete code object)
239 242 self._is_complete = self.code is not None
240 243
241 244 return self._is_complete
242 245
243 246 def push_accepts_more(self):
244 247 """Return whether a block of interactive input can accept more input.
245 248
246 249 This method is meant to be used by line-oriented frontends, who need to
247 250 guess whether a block is complete or not based solely on prior and
248 251 current input lines. The InputSplitter considers it has a complete
249 252 interactive block and will not accept more input only when either a
250 253 SyntaxError is raised, or *all* of the following are true:
251 254
252 255 1. The input compiles to a complete statement.
253 256
254 257 2. The indentation level is flush-left (because if we are indented,
255 258 like inside a function definition or for loop, we need to keep
256 259 reading new input).
257 260
258 261 3. There is one extra line consisting only of whitespace.
259 262
260 263 Because of condition #3, this method should be used only by
261 264 *line-oriented* frontends, since it means that intermediate blank lines
262 265 are not allowed in function definitions (or any other indented block).
263 266
264 267 Block-oriented frontends that have a separate keyboard event to
265 268 indicate execution should use the :meth:`split_blocks` method instead.
266 269
267 270 If the current input produces a syntax error, this method immediately
268 271 returns False but does *not* raise the syntax error exception, as
269 272 typically clients will want to send invalid syntax to an execution
270 273 backend which might convert the invalid syntax into valid Python via
271 274 one of the dynamic IPython mechanisms.
272 275 """
273 276
274 277 if not self._is_complete:
275 278 return True
276 279
277 280 if self.indent_spaces==0:
278 281 return False
279 282
280 283 last_line = self.source.splitlines()[-1]
281 284 return bool(last_line and not last_line.isspace())
282 285
283 286 def split_blocks(self, lines):
284 287 """Split a multiline string into multiple input blocks.
285 288
286 289 Note: this method starts by performing a full reset().
287 290
288 291 Parameters
289 292 ----------
290 293 lines : str
291 294 A possibly multiline string.
292 295
293 296 Returns
294 297 -------
295 298 blocks : list
296 299 A list of strings, each possibly multiline. Each string corresponds
297 300 to a single block that can be compiled in 'single' mode (unless it
298 301 has a syntax error)."""
299 302
300 303 # This code is fairly delicate. If you make any changes here, make
301 304 # absolutely sure that you do run the full test suite and ALL tests
302 305 # pass.
303 306
304 307 self.reset()
305 308 blocks = []
306 309
307 310 # Reversed copy so we can use pop() efficiently and consume the input
308 311 # as a stack
309 312 lines = lines.splitlines()[::-1]
310 313 # Outer loop over all input
311 314 while lines:
312 315 # Inner loop to build each block
313 316 while True:
314 317 # Safety exit from inner loop
315 318 if not lines:
316 319 break
317 320 # Grab next line but don't push it yet
318 321 next_line = lines.pop()
319 322 # Blank/empty lines are pushed as-is
320 323 if not next_line or next_line.isspace():
321 324 self.push(next_line)
322 325 continue
323 326
324 327 # Check indentation changes caused by the *next* line
325 328 indent_spaces, _full_dedent = self._find_indent(next_line)
326 329
327 330 # If the next line causes a dedent, it can be for two differnt
328 331 # reasons: either an explicit de-dent by the user or a
329 332 # return/raise/pass statement. These MUST be handled
330 333 # separately:
331 334 #
332 335 # 1. the first case is only detected when the actual explicit
333 336 # dedent happens, and that would be the *first* line of a *new*
334 337 # block. Thus, we must put the line back into the input buffer
335 338 # so that it starts a new block on the next pass.
336 339 #
337 340 # 2. the second case is detected in the line before the actual
338 341 # dedent happens, so , we consume the line and we can break out
339 342 # to start a new block.
340 343
341 344 # Case 1, explicit dedent causes a break
342 345 if _full_dedent and not next_line.startswith(' '):
343 346 lines.append(next_line)
344 347 break
345 348
346 349 # Otherwise any line is pushed
347 350 self.push(next_line)
348 351
349 352 # Case 2, full dedent with full block ready:
350 353 if _full_dedent or \
351 354 self.indent_spaces==0 and not self.push_accepts_more():
352 355 break
353 356 # Form the new block with the current source input
354 357 blocks.append(self.source_reset())
355 358
356 359 return blocks
357 360
358 361 #------------------------------------------------------------------------
359 362 # Private interface
360 363 #------------------------------------------------------------------------
361 364
362 365 def _find_indent(self, line):
363 366 """Compute the new indentation level for a single line.
364 367
365 368 Parameters
366 369 ----------
367 370 line : str
368 371 A single new line of non-whitespace, non-comment Python input.
369 372
370 373 Returns
371 374 -------
372 375 indent_spaces : int
373 376 New value for the indent level (it may be equal to self.indent_spaces
374 377 if indentation doesn't change.
375 378
376 379 full_dedent : boolean
377 380 Whether the new line causes a full flush-left dedent.
378 381 """
379 382 indent_spaces = self.indent_spaces
380 383 full_dedent = self._full_dedent
381 384
382 385 inisp = num_ini_spaces(line)
383 386 if inisp < indent_spaces:
384 387 indent_spaces = inisp
385 388 if indent_spaces <= 0:
386 389 #print 'Full dedent in text',self.source # dbg
387 390 full_dedent = True
388 391
389 392 if line[-1] == ':':
390 393 indent_spaces += 4
391 394 elif dedent_re.match(line):
392 395 indent_spaces -= 4
393 396 if indent_spaces <= 0:
394 397 full_dedent = True
395 398
396 399 # Safety
397 400 if indent_spaces < 0:
398 401 indent_spaces = 0
399 402 #print 'safety' # dbg
400 403
401 404 return indent_spaces, full_dedent
402 405
403 406 def _update_indent(self, lines):
404 407 for line in remove_comments(lines).splitlines():
405 408 if line and not line.isspace():
406 409 self.indent_spaces, self._full_dedent = self._find_indent(line)
407 410
408 411 def _store(self, lines):
409 412 """Store one or more lines of input.
410 413
411 414 If input lines are not newline-terminated, a newline is automatically
412 415 appended."""
413 416
414 417 if lines.endswith('\n'):
415 418 self._buffer.append(lines)
416 419 else:
417 420 self._buffer.append(lines+'\n')
418 421 self._set_source()
419 422
420 423 def _set_source(self):
421 424 self.source = ''.join(self._buffer).encode(self.encoding)
425
426
427 #-----------------------------------------------------------------------------
428 # IPython-specific syntactic support
429 #-----------------------------------------------------------------------------
430
431 # We implement things, as much as possible, as standalone functions that can be
432 # tested and validated in isolation.
433
434 # Each of these uses a regexp, we pre-compile these and keep them close to each
435 # function definition for clarity
436 _assign_system_re = re.compile(r'(?P<lhs>(\s*)([\w\.]+)((\s*,\s*[\w\.]+)*))'
437 r'\s*=\s*!\s*(?P<cmd>.*)')
438
439 def transform_assign_system(line):
440 """Handle the `files = !ls` syntax."""
441 # FIXME: This transforms the line to use %sc, but we've listed that magic
442 # as deprecated. We should then implement this functionality in a
443 # standalone api that we can transform to, without going through a
444 # deprecated magic.
445 m = _assign_system_re.match(line)
446 if m is not None:
447 cmd = m.group('cmd')
448 lhs = m.group('lhs')
449 expr = make_quoted_expr("sc -l = %s" % cmd)
450 new_line = '%s = get_ipython().magic(%s)' % (lhs, expr)
451 return new_line
452 return line
453
454
455 _assign_magic_re = re.compile(r'(?P<lhs>(\s*)([\w\.]+)((\s*,\s*[\w\.]+)*))'
456 r'\s*=\s*%\s*(?P<cmd>.*)')
457
458 def transform_assign_magic(line):
459 """Handle the `a = %who` syntax."""
460 m = _assign_magic_re.match(line)
461 if m is not None:
462 cmd = m.group('cmd')
463 lhs = m.group('lhs')
464 expr = make_quoted_expr(cmd)
465 new_line = '%s = get_ipython().magic(%s)' % (lhs, expr)
466 return new_line
467 return line
468
469
470 _classic_prompt_re = re.compile(r'(^[ \t]*>>> |^[ \t]*\.\.\. )')
471
472 def transform_classic_prompt(line):
473 """Handle inputs that start with '>>> ' syntax."""
474
475 if not line or line.isspace() or line.strip() == '...':
476 # This allows us to recognize multiple input prompts separated by
477 # blank lines and pasted in a single chunk, very common when
478 # pasting doctests or long tutorial passages.
479 return ''
480 m = _classic_prompt_re.match(line)
481 if m:
482 return line[len(m.group(0)):]
483 else:
484 return line
485
486
487 _ipy_prompt_re = re.compile(r'(^[ \t]*In \[\d+\]: |^[ \t]*\ \ \ \.\.\.+: )')
488
489 def transform_ipy_prompt(line):
490 """Handle inputs that start classic IPython prompt syntax."""
491
492 if not line or line.isspace() or line.strip() == '...':
493 # This allows us to recognize multiple input prompts separated by
494 # blank lines and pasted in a single chunk, very common when
495 # pasting doctests or long tutorial passages.
496 return ''
497 m = _ipy_prompt_re.match(line)
498 if m:
499 return line[len(m.group(0)):]
500 else:
501 return line
502
503
504 # Warning, these cannot be changed unless various regular expressions
505 # are updated in a number of places. Not great, but at least we told you.
506 ESC_SHELL = '!'
507 ESC_SH_CAP = '!!'
508 ESC_HELP = '?'
509 ESC_MAGIC = '%'
510 ESC_QUOTE = ','
511 ESC_QUOTE2 = ';'
512 ESC_PAREN = '/'
513
514 class IPythonInputSplitter(InputSplitter):
515 """An input splitter that recognizes all of IPython's special syntax."""
516
517
518 def push(self, lines):
519 """Push one or more lines of IPython input.
520 """
521 return super(IPythonInputSplitter, self).push(lines)
@@ -1,364 +1,411 b''
1 1 """Tests for the inputsplitter module.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (C) 2010 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING, distributed as part of this software.
8 8 #-----------------------------------------------------------------------------
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Imports
12 12 #-----------------------------------------------------------------------------
13 13 # stdlib
14 14 import unittest
15 15 import sys
16 16
17 17 # Third party
18 18 import nose.tools as nt
19 19
20 20 # Our own
21 21 from IPython.core import inputsplitter as isp
22 22
23 23 #-----------------------------------------------------------------------------
24 24 # Semi-complete examples (also used as tests)
25 25 #-----------------------------------------------------------------------------
26 26 def mini_interactive_loop(raw_input):
27 27 """Minimal example of the logic of an interactive interpreter loop.
28 28
29 29 This serves as an example, and it is used by the test system with a fake
30 30 raw_input that simulates interactive input."""
31 31
32 32 from IPython.core.inputsplitter import InputSplitter
33 33
34 34 isp = InputSplitter()
35 35 # In practice, this input loop would be wrapped in an outside loop to read
36 36 # input indefinitely, until some exit/quit command was issued. Here we
37 37 # only illustrate the basic inner loop.
38 38 while isp.push_accepts_more():
39 39 indent = ' '*isp.indent_spaces
40 40 prompt = '>>> ' + indent
41 41 line = indent + raw_input(prompt)
42 42 isp.push(line)
43 43
44 44 # Here we just return input so we can use it in a test suite, but a real
45 45 # interpreter would instead send it for execution somewhere.
46 46 src = isp.source_reset()
47 47 print 'Input source was:\n', src
48 48 return src
49 49
50 50 #-----------------------------------------------------------------------------
51 51 # Test utilities, just for local use
52 52 #-----------------------------------------------------------------------------
53 53
54 54 def assemble(block):
55 55 """Assemble a block into multi-line sub-blocks."""
56 56 return ['\n'.join(sub_block)+'\n' for sub_block in block]
57 57
58 58
59 59 def pseudo_input(lines):
60 60 """Return a function that acts like raw_input but feeds the input list."""
61 61 ilines = iter(lines)
62 62 def raw_in(prompt):
63 63 try:
64 64 return next(ilines)
65 65 except StopIteration:
66 66 return ''
67 67 return raw_in
68 68
69 69 #-----------------------------------------------------------------------------
70 70 # Tests
71 71 #-----------------------------------------------------------------------------
72 72 def test_spaces():
73 73 tests = [('', 0),
74 74 (' ', 1),
75 75 ('\n', 0),
76 76 (' \n', 1),
77 77 ('x', 0),
78 78 (' x', 1),
79 79 (' x',2),
80 80 (' x',4),
81 81 # Note: tabs are counted as a single whitespace!
82 82 ('\tx', 1),
83 83 ('\t x', 2),
84 84 ]
85 85
86 86 for s, nsp in tests:
87 87 nt.assert_equal(isp.num_ini_spaces(s), nsp)
88 88
89 89
90 90 def test_remove_comments():
91 91 tests = [('text', 'text'),
92 92 ('text # comment', 'text '),
93 93 ('text # comment\n', 'text \n'),
94 94 ('text # comment \n', 'text \n'),
95 95 ('line # c \nline\n','line \nline\n'),
96 96 ('line # c \nline#c2 \nline\nline #c\n\n',
97 97 'line \nline\nline\nline \n\n'),
98 98 ]
99 99
100 100 for inp, out in tests:
101 101 nt.assert_equal(isp.remove_comments(inp), out)
102 102
103 103
104 104 def test_get_input_encoding():
105 105 encoding = isp.get_input_encoding()
106 106 nt.assert_true(isinstance(encoding, basestring))
107 107 # simple-minded check that at least encoding a simple string works with the
108 108 # encoding we got.
109 109 nt.assert_equal('test'.encode(encoding), 'test')
110 110
111 111
112 112 class NoInputEncodingTestCase(unittest.TestCase):
113 113 def setUp(self):
114 114 self.old_stdin = sys.stdin
115 115 class X: pass
116 116 fake_stdin = X()
117 117 sys.stdin = fake_stdin
118 118
119 119 def test(self):
120 120 # Verify that if sys.stdin has no 'encoding' attribute we do the right
121 121 # thing
122 122 enc = isp.get_input_encoding()
123 123 self.assertEqual(enc, 'ascii')
124 124
125 125 def tearDown(self):
126 126 sys.stdin = self.old_stdin
127 127
128 128
129 129 class InputSplitterTestCase(unittest.TestCase):
130 130 def setUp(self):
131 131 self.isp = isp.InputSplitter()
132 132
133 133 def test_reset(self):
134 134 isp = self.isp
135 135 isp.push('x=1')
136 136 isp.reset()
137 137 self.assertEqual(isp._buffer, [])
138 138 self.assertEqual(isp.indent_spaces, 0)
139 139 self.assertEqual(isp.source, '')
140 140 self.assertEqual(isp.code, None)
141 141 self.assertEqual(isp._is_complete, False)
142 142
143 143 def test_source(self):
144 144 self.isp._store('1')
145 145 self.isp._store('2')
146 146 self.assertEqual(self.isp.source, '1\n2\n')
147 147 self.assertTrue(len(self.isp._buffer)>0)
148 148 self.assertEqual(self.isp.source_reset(), '1\n2\n')
149 149 self.assertEqual(self.isp._buffer, [])
150 150 self.assertEqual(self.isp.source, '')
151 151
152 152 def test_indent(self):
153 153 isp = self.isp # shorthand
154 154 isp.push('x=1')
155 155 self.assertEqual(isp.indent_spaces, 0)
156 156 isp.push('if 1:\n x=1')
157 157 self.assertEqual(isp.indent_spaces, 4)
158 158 isp.push('y=2\n')
159 159 self.assertEqual(isp.indent_spaces, 0)
160 160 isp.push('if 1:')
161 161 self.assertEqual(isp.indent_spaces, 4)
162 162 isp.push(' x=1')
163 163 self.assertEqual(isp.indent_spaces, 4)
164 164 # Blank lines shouldn't change the indent level
165 165 isp.push(' '*2)
166 166 self.assertEqual(isp.indent_spaces, 4)
167 167
168 168 def test_indent2(self):
169 169 isp = self.isp
170 170 # When a multiline statement contains parens or multiline strings, we
171 171 # shouldn't get confused.
172 172 isp.push("if 1:")
173 173 isp.push(" x = (1+\n 2)")
174 174 self.assertEqual(isp.indent_spaces, 4)
175 175
176 176 def test_dedent(self):
177 177 isp = self.isp # shorthand
178 178 isp.push('if 1:')
179 179 self.assertEqual(isp.indent_spaces, 4)
180 180 isp.push(' pass')
181 181 self.assertEqual(isp.indent_spaces, 0)
182 182
183 183 def test_push(self):
184 184 isp = self.isp
185 185 self.assertTrue(isp.push('x=1'))
186 186
187 187 def test_push2(self):
188 188 isp = self.isp
189 189 self.assertFalse(isp.push('if 1:'))
190 190 for line in [' x=1', '# a comment', ' y=2']:
191 191 self.assertTrue(isp.push(line))
192 192
193 193 def test_push3(self):
194 194 """Test input with leading whitespace"""
195 195 isp = self.isp
196 196 isp.push(' x=1')
197 197 isp.push(' y=2')
198 198 self.assertEqual(isp.source, 'if 1:\n x=1\n y=2\n')
199 199
200 200 def test_replace_mode(self):
201 201 isp = self.isp
202 202 isp.input_mode = 'replace'
203 203 isp.push('x=1')
204 204 self.assertEqual(isp.source, 'x=1\n')
205 205 isp.push('x=2')
206 206 self.assertEqual(isp.source, 'x=2\n')
207 207
208 208 def test_push_accepts_more(self):
209 209 isp = self.isp
210 210 isp.push('x=1')
211 211 self.assertFalse(isp.push_accepts_more())
212 212
213 213 def test_push_accepts_more2(self):
214 214 isp = self.isp
215 215 isp.push('if 1:')
216 216 self.assertTrue(isp.push_accepts_more())
217 217 isp.push(' x=1')
218 218 self.assertTrue(isp.push_accepts_more())
219 219 isp.push('')
220 220 self.assertFalse(isp.push_accepts_more())
221 221
222 222 def test_push_accepts_more3(self):
223 223 isp = self.isp
224 224 isp.push("x = (2+\n3)")
225 225 self.assertFalse(isp.push_accepts_more())
226 226
227 227 def test_push_accepts_more4(self):
228 228 isp = self.isp
229 229 # When a multiline statement contains parens or multiline strings, we
230 230 # shouldn't get confused.
231 231 # FIXME: we should be able to better handle de-dents in statements like
232 232 # multiline strings and multiline expressions (continued with \ or
233 233 # parens). Right now we aren't handling the indentation tracking quite
234 234 # correctly with this, though in practice it may not be too much of a
235 235 # problem. We'll need to see.
236 236 isp.push("if 1:")
237 237 isp.push(" x = (2+")
238 238 isp.push(" 3)")
239 239 self.assertTrue(isp.push_accepts_more())
240 240 isp.push(" y = 3")
241 241 self.assertTrue(isp.push_accepts_more())
242 242 isp.push('')
243 243 self.assertFalse(isp.push_accepts_more())
244 244
245 245 def test_syntax_error(self):
246 246 isp = self.isp
247 247 # Syntax errors immediately produce a 'ready' block, so the invalid
248 248 # Python can be sent to the kernel for evaluation with possible ipython
249 249 # special-syntax conversion.
250 250 isp.push('run foo')
251 251 self.assertFalse(isp.push_accepts_more())
252 252
253 253 def check_split(self, block_lines, compile=True):
254 254 blocks = assemble(block_lines)
255 255 lines = ''.join(blocks)
256 256 oblock = self.isp.split_blocks(lines)
257 257 self.assertEqual(oblock, blocks)
258 258 if compile:
259 259 for block in blocks:
260 260 self.isp._compile(block)
261 261
262 262 def test_split(self):
263 263 # All blocks of input we want to test in a list. The format for each
264 264 # block is a list of lists, with each inner lists consisting of all the
265 265 # lines (as single-lines) that should make up a sub-block.
266 266
267 267 # Note: do NOT put here sub-blocks that don't compile, as the
268 268 # check_split() routine makes a final verification pass to check that
269 269 # each sub_block, as returned by split_blocks(), does compile
270 270 # correctly.
271 271 all_blocks = [ [['x=1']],
272 272
273 273 [['x=1'],
274 274 ['y=2']],
275 275
276 276 [['x=1'],
277 277 ['# a comment'],
278 278 ['y=11']],
279 279
280 280 [['if 1:',
281 281 ' x=1'],
282 282 ['y=3']],
283 283
284 284 [['def f(x):',
285 285 ' return x'],
286 286 ['x=1']],
287 287
288 288 [['def f(x):',
289 289 ' x+=1',
290 290 ' ',
291 291 ' return x'],
292 292 ['x=1']],
293 293
294 294 [['def f(x):',
295 295 ' if x>0:',
296 296 ' y=1',
297 297 ' # a comment',
298 298 ' else:',
299 299 ' y=4',
300 300 ' ',
301 301 ' return y'],
302 302 ['x=1'],
303 303 ['if 1:',
304 304 ' y=11'] ],
305 305
306 306 [['for i in range(10):'
307 307 ' x=i**2']],
308 308
309 309 [['for i in range(10):'
310 310 ' x=i**2'],
311 311 ['z = 1']],
312 312 ]
313 313 for block_lines in all_blocks:
314 314 self.check_split(block_lines)
315 315
316 316 def test_split_syntax_errors(self):
317 317 # Block splitting with invalid syntax
318 318 all_blocks = [ [['a syntax error']],
319 319
320 320 [['x=1'],
321 321 ['a syntax error']],
322 322
323 323 [['for i in range(10):'
324 324 ' an error']],
325 325
326 326 ]
327 327 for block_lines in all_blocks:
328 328 self.check_split(block_lines, compile=False)
329 329
330 330
331 331 class InteractiveLoopTestCase(unittest.TestCase):
332 332 """Tests for an interactive loop like a python shell.
333 333 """
334 334 def check_ns(self, lines, ns):
335 335 """Validate that the given input lines produce the resulting namespace.
336 336
337 337 Note: the input lines are given exactly as they would be typed in an
338 338 auto-indenting environment, as mini_interactive_loop above already does
339 339 auto-indenting and prepends spaces to the input.
340 340 """
341 341 src = mini_interactive_loop(pseudo_input(lines))
342 342 test_ns = {}
343 343 exec src in test_ns
344 344 # We can't check that the provided ns is identical to the test_ns,
345 345 # because Python fills test_ns with extra keys (copyright, etc). But
346 346 # we can check that the given dict is *contained* in test_ns
347 347 for k,v in ns.items():
348 348 self.assertEqual(test_ns[k], v)
349 349
350 350 def test_simple(self):
351 351 self.check_ns(['x=1'], dict(x=1))
352 352
353 353 def test_simple2(self):
354 354 self.check_ns(['if 1:', 'x=2'], dict(x=2))
355 355
356 356 def test_xy(self):
357 357 self.check_ns(['x=1; y=2'], dict(x=1, y=2))
358 358
359 359 def test_abc(self):
360 360 self.check_ns(['if 1:','a=1','b=2','c=3'], dict(a=1, b=2, c=3))
361 361
362 362 def test_multi(self):
363 363 self.check_ns(['x =(1+','1+','2)'], dict(x=4))
364 364
365
366 class IPythonInputTestCase(InputSplitterTestCase):
367 def setUp(self):
368 self.isp = isp.IPythonInputSplitter()
369
370
371 # Transformer tests
372 def transform_checker(tests, func):
373 """Utility to loop over test inputs"""
374 for inp, tr in tests:
375 nt.assert_equals(func(inp), tr)
376
377
378 def test_assign_system():
379 tests = [('a =! ls', 'a = get_ipython().magic("sc -l = ls")'),
380 ('b = !ls', 'b = get_ipython().magic("sc -l = ls")'),
381 ('x=1','x=1')]
382 transform_checker(tests, isp.transform_assign_system)
383
384
385 def test_assign_magic():
386 tests = [('a =% who', 'a = get_ipython().magic("who")'),
387 ('b = %who', 'b = get_ipython().magic("who")'),
388 ('x=1','x=1')]
389 transform_checker(tests, isp.transform_assign_magic)
390
391
392 def test_classic_prompt():
393 tests = [('>>> x=1', 'x=1'),
394 ('>>> for i in range(10):','for i in range(10):'),
395 ('... print i',' print i'),
396 ('...', ''),
397 ('x=1','x=1')
398 ]
399 transform_checker(tests, isp.transform_classic_prompt)
400
401
402 def test_ipy_prompt():
403 tests = [('In [1]: x=1', 'x=1'),
404 ('In [24]: for i in range(10):','for i in range(10):'),
405 (' ....: print i',' print i'),
406 (' ....: ', ''),
407 ('x=1', 'x=1'), # normal input is unmodified
408 (' ','') # blank lines are just collapsed
409 ]
410 transform_checker(tests, isp.transform_ipy_prompt)
411
General Comments 0
You need to be logged in to leave comments. Login now