diff --git a/IPython/core/blockbreaker.py b/IPython/core/blockbreaker.py index 8ba4a7a..f755063 100644 --- a/IPython/core/blockbreaker.py +++ b/IPython/core/blockbreaker.py @@ -27,6 +27,8 @@ import sys # Utilities #----------------------------------------------------------------------------- +# FIXME: move these utilities to the general ward... + # compiled regexps for autoindent management dedent_re = re.compile(r'^\s+raise|^\s+return|^\s+pass') ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)') @@ -76,10 +78,7 @@ def get_input_encoding(): # Classes and functions #----------------------------------------------------------------------------- - class BlockBreaker(object): - # List - buffer = None # Command compiler compile = None # Number of spaces of indentation @@ -92,17 +91,23 @@ class BlockBreaker(object): code = None # Boolean indicating whether the current block is complete is_complete = None + + # Private attributes + + # List + _buffer = None def __init__(self): - self.buffer = [] + self._buffer = [] self.compile = codeop.CommandCompiler() self.encoding = get_input_encoding() def reset(self): """Reset the input buffer and associated state.""" self.indent_spaces = 0 - self.buffer[:] = [] + self._buffer[:] = [] self.source = '' + self.code = None def get_source(self, reset=False): """Return the input source. @@ -117,34 +122,6 @@ class BlockBreaker(object): self.reset() return out - def update_indent(self, lines): - """Keep track of the indent level.""" - - for line in remove_comments(lines).splitlines(): - - if line and not line.isspace(): - if self.code is not None: - inisp = num_ini_spaces(line) - if inisp < self.indent_spaces: - self.indent_spaces = inisp - - if line[-1] == ':': - self.indent_spaces += 4 - elif dedent_re.match(line): - self.indent_spaces -= 4 - - def store(self, lines): - """Store one or more lines of input. - - If input lines are not newline-terminated, a newline is automatically - appended.""" - - if lines.endswith('\n'): - self.buffer.append(lines) - else: - self.buffer.append(lines+'\n') - self.source = ''.join(self.buffer).encode(self.encoding) - def push(self, lines): """Push one ore more lines of input. @@ -170,10 +147,10 @@ class BlockBreaker(object): # this allows execution of indented pasted code. It is tempting # to add '\n' at the end of source to run commands like ' a=1' # directly, but this fails for more complicated scenarios - if not self.buffer and lines[:1] in [' ', '\t']: + if not self._buffer and lines[:1] in [' ', '\t']: lines = 'if 1:\n%s' % lines - self.store(lines) + self._store(lines) source = self.source # Before calling compile(), reset the code object to None so that if an @@ -187,7 +164,7 @@ class BlockBreaker(object): self.is_complete = False else: self.is_complete = True - self.update_indent(lines) + self._update_indent(lines) return self.is_complete def interactive_block_ready(self): @@ -223,163 +200,38 @@ class BlockBreaker(object): else: return False - def split_blocks(self, lines): """Split a multiline string into multiple input blocks""" + raise NotImplementedError -#----------------------------------------------------------------------------- -# Tests -#----------------------------------------------------------------------------- + #------------------------------------------------------------------------ + # Private interface + #------------------------------------------------------------------------ + + def _update_indent(self, lines): + """Keep track of the indent level.""" -import unittest + for line in remove_comments(lines).splitlines(): + + if line and not line.isspace(): + if self.code is not None: + inisp = num_ini_spaces(line) + if inisp < self.indent_spaces: + self.indent_spaces = inisp -import nose.tools as nt + if line[-1] == ':': + self.indent_spaces += 4 + elif dedent_re.match(line): + self.indent_spaces -= 4 - -def test_spaces(): - tests = [('', 0), - (' ', 1), - ('\n', 0), - (' \n', 1), - ('x', 0), - (' x', 1), - (' x',2), - (' x',4), - # Note: tabs are counted as a single whitespace! - ('\tx', 1), - ('\t x', 2), - ] - - for s, nsp in tests: - nt.assert_equal(num_ini_spaces(s), nsp) - - -def test_remove_comments(): - tests = [('text', 'text'), - ('text # comment', 'text '), - ('text # comment\n', 'text \n'), - ('text # comment \n', 'text \n'), - ('line # c \nline\n','line \nline\n'), - ('line # c \nline#c2 \nline\nline #c\n\n', - 'line \nline\nline\nline \n\n'), - ] - - for inp, out in tests: - nt.assert_equal(remove_comments(inp), out) - - -def test_get_input_encoding(): - encoding = get_input_encoding() - nt.assert_true(isinstance(encoding, basestring)) - # simple-minded check that at least encoding a simple string works with the - # encoding we got. - nt.assert_equal('test'.encode(encoding), 'test') - - -class BlockBreakerTestCase(unittest.TestCase): - def setUp(self): - self.bb = BlockBreaker() - - def test_reset(self): - self.bb.store('hello') - self.bb.reset() - self.assertEqual(self.bb.buffer, []) - self.assertEqual(self.bb.indent_spaces, 0) - self.assertEqual(self.bb.get_source(), '') - - def test_source(self): - self.bb.store('1') - self.bb.store('2') - out = self.bb.get_source() - self.assertEqual(out, '1\n2\n') - out = self.bb.get_source(reset=True) - self.assertEqual(out, '1\n2\n') - self.assertEqual(self.bb.buffer, []) - out = self.bb.get_source() - self.assertEqual(out, '') - - def test_indent(self): - bb = self.bb # shorthand - bb.push('x=1') - self.assertEqual(bb.indent_spaces, 0) - bb.push('if 1:\n x=1') - self.assertEqual(bb.indent_spaces, 4) - bb.push('y=2\n') - self.assertEqual(bb.indent_spaces, 0) - bb.push('if 1:') - self.assertEqual(bb.indent_spaces, 4) - bb.push(' x=1') - self.assertEqual(bb.indent_spaces, 4) - # Blank lines shouldn't change the indent level - bb.push(' '*2) - self.assertEqual(bb.indent_spaces, 4) - - def test_indent2(self): - bb = self.bb - # When a multiline statement contains parens or multiline strings, we - # shouldn't get confused. - bb.push("if 1:") - bb.push(" x = (1+\n 2)") - self.assertEqual(bb.indent_spaces, 4) - - def test_dedent(self): - bb = self.bb # shorthand - bb.push('if 1:') - self.assertEqual(bb.indent_spaces, 4) - bb.push(' pass') - self.assertEqual(bb.indent_spaces, 0) - - def test_push(self): - bb = self.bb - bb.push('x=1') - self.assertTrue(bb.is_complete) - - def test_push2(self): - bb = self.bb - bb.push('if 1:') - self.assertFalse(bb.is_complete) - for line in [' x=1', '# a comment', ' y=2']: - bb.push(line) - self.assertTrue(bb.is_complete) - - def test_push3(self): - """Test input with leading whitespace""" - bb = self.bb - bb.push(' x=1') - bb.push(' y=2') - self.assertEqual(bb.source, 'if 1:\n x=1\n y=2\n') - - def test_interactive_block_ready(self): - bb = self.bb - bb.push('x=1') - self.assertTrue(bb.interactive_block_ready()) - - def test_interactive_block_ready2(self): - bb = self.bb - bb.push('if 1:\n x=1') - self.assertFalse(bb.interactive_block_ready()) - bb.push('') - self.assertTrue(bb.interactive_block_ready()) - - def test_interactive_block_ready3(self): - bb = self.bb - bb.push("x = (2+\n3)") - self.assertTrue(bb.interactive_block_ready()) - - def test_interactive_block_ready4(self): - bb = self.bb - # When a multiline statement contains parens or multiline strings, we - # shouldn't get confused. - # FIXME: we should be able to better handle de-dents in statements like - # multiline strings and multiline expressions (continued with \ or - # parens). Right now we aren't handling the indentation tracking quite - # correctly with this, though in practice it may not be too much of a - # problem. We'll need to see. - bb.push("if 1:") - bb.push(" x = (2+") - bb.push(" 3)") - self.assertFalse(bb.interactive_block_ready()) - bb.push(" y = 3") - self.assertFalse(bb.interactive_block_ready()) - bb.push('') - self.assertTrue(bb.interactive_block_ready()) + def _store(self, lines): + """Store one or more lines of input. + + If input lines are not newline-terminated, a newline is automatically + appended.""" + + if lines.endswith('\n'): + self._buffer.append(lines) + else: + self._buffer.append(lines+'\n') + self.source = ''.join(self._buffer).encode(self.encoding) diff --git a/IPython/core/tests/test_blockbreaker.py b/IPython/core/tests/test_blockbreaker.py new file mode 100644 index 0000000..6bf642d --- /dev/null +++ b/IPython/core/tests/test_blockbreaker.py @@ -0,0 +1,173 @@ +"""Tests for the blockbreaker module. +""" +#----------------------------------------------------------------------------- +# Copyright (C) 2010 The IPython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- +# stdlib +import unittest + +# Third party +import nose.tools as nt + +# Our own +from IPython.core import blockbreaker as BB + +#----------------------------------------------------------------------------- +# Tests +#----------------------------------------------------------------------------- +def test_spaces(): + tests = [('', 0), + (' ', 1), + ('\n', 0), + (' \n', 1), + ('x', 0), + (' x', 1), + (' x',2), + (' x',4), + # Note: tabs are counted as a single whitespace! + ('\tx', 1), + ('\t x', 2), + ] + + for s, nsp in tests: + nt.assert_equal(BB.num_ini_spaces(s), nsp) + + +def test_remove_comments(): + tests = [('text', 'text'), + ('text # comment', 'text '), + ('text # comment\n', 'text \n'), + ('text # comment \n', 'text \n'), + ('line # c \nline\n','line \nline\n'), + ('line # c \nline#c2 \nline\nline #c\n\n', + 'line \nline\nline\nline \n\n'), + ] + + for inp, out in tests: + nt.assert_equal(BB.remove_comments(inp), out) + + +def test_get_input_encoding(): + encoding = BB.get_input_encoding() + nt.assert_true(isinstance(encoding, basestring)) + # simple-minded check that at least encoding a simple string works with the + # encoding we got. + nt.assert_equal('test'.encode(encoding), 'test') + + +class BlockBreakerTestCase(unittest.TestCase): + def setUp(self): + self.bb = BB.BlockBreaker() + + def test_reset(self): + bb = self.bb + bb.push('x=1') + bb.reset() + self.assertEqual(bb._buffer, []) + self.assertEqual(bb.indent_spaces, 0) + self.assertEqual(bb.get_source(), '') + self.assertEqual(bb.code, None) + + def test_source(self): + self.bb._store('1') + self.bb._store('2') + out = self.bb.get_source() + self.assertEqual(out, '1\n2\n') + out = self.bb.get_source(reset=True) + self.assertEqual(out, '1\n2\n') + self.assertEqual(self.bb._buffer, []) + out = self.bb.get_source() + self.assertEqual(out, '') + + def test_indent(self): + bb = self.bb # shorthand + bb.push('x=1') + self.assertEqual(bb.indent_spaces, 0) + bb.push('if 1:\n x=1') + self.assertEqual(bb.indent_spaces, 4) + bb.push('y=2\n') + self.assertEqual(bb.indent_spaces, 0) + bb.push('if 1:') + self.assertEqual(bb.indent_spaces, 4) + bb.push(' x=1') + self.assertEqual(bb.indent_spaces, 4) + # Blank lines shouldn't change the indent level + bb.push(' '*2) + self.assertEqual(bb.indent_spaces, 4) + + def test_indent2(self): + bb = self.bb + # When a multiline statement contains parens or multiline strings, we + # shouldn't get confused. + bb.push("if 1:") + bb.push(" x = (1+\n 2)") + self.assertEqual(bb.indent_spaces, 4) + + def test_dedent(self): + bb = self.bb # shorthand + bb.push('if 1:') + self.assertEqual(bb.indent_spaces, 4) + bb.push(' pass') + self.assertEqual(bb.indent_spaces, 0) + + def test_push(self): + bb = self.bb + bb.push('x=1') + self.assertTrue(bb.is_complete) + + def test_push2(self): + bb = self.bb + bb.push('if 1:') + self.assertFalse(bb.is_complete) + for line in [' x=1', '# a comment', ' y=2']: + bb.push(line) + self.assertTrue(bb.is_complete) + + def test_push3(self): + """Test input with leading whitespace""" + bb = self.bb + bb.push(' x=1') + bb.push(' y=2') + self.assertEqual(bb.source, 'if 1:\n x=1\n y=2\n') + + def test_interactive_block_ready(self): + bb = self.bb + bb.push('x=1') + self.assertTrue(bb.interactive_block_ready()) + + def test_interactive_block_ready2(self): + bb = self.bb + bb.push('if 1:\n x=1') + self.assertFalse(bb.interactive_block_ready()) + bb.push('') + self.assertTrue(bb.interactive_block_ready()) + + def test_interactive_block_ready3(self): + bb = self.bb + bb.push("x = (2+\n3)") + self.assertTrue(bb.interactive_block_ready()) + + def test_interactive_block_ready4(self): + bb = self.bb + # When a multiline statement contains parens or multiline strings, we + # shouldn't get confused. + # FIXME: we should be able to better handle de-dents in statements like + # multiline strings and multiline expressions (continued with \ or + # parens). Right now we aren't handling the indentation tracking quite + # correctly with this, though in practice it may not be too much of a + # problem. We'll need to see. + bb.push("if 1:") + bb.push(" x = (2+") + bb.push(" 3)") + self.assertFalse(bb.interactive_block_ready()) + bb.push(" y = 3") + self.assertFalse(bb.interactive_block_ready()) + bb.push('') + self.assertTrue(bb.interactive_block_ready())