From 02eecaf061408f26a3c6029886b8794f73581938 2010-10-26 07:14:50 From: Fernando Perez Date: 2010-10-26 07:14:50 Subject: [PATCH] Fix bug in tab completion of filenames when quotes are present. Unit tests added to the completion code as well, to validate quote handling more stringently. --- diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 90c12af..53489b1 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -101,6 +101,27 @@ else: # Main functions and classes #----------------------------------------------------------------------------- +def has_open_quotes(s): + """Return whether a string has open quotes. + + This simply counts whether the number of quote characters of either type in + the string is odd. + + Returns + ------- + If there is an open quote, the quote character is returned. Else, return + False. + """ + # We check " first, then ', so complex cases with nested quotes will get + # the " to take precedence. + if s.count('"') % 2: + return '"' + elif s.count("'") % 2: + return "'" + else: + return False + + def protect_filename(s): """Escape a string to protect certain characters.""" @@ -485,39 +506,41 @@ class IPCompleter(Completer): text_prefix = '!' else: text_prefix = '' - + text_until_cursor = self.text_until_cursor - open_quotes = 0 # track strings with open quotes - try: - # arg_split ~ shlex.split, but with unicode bugs fixed by us - lsplit = arg_split(text_until_cursor)[-1] - except ValueError: - # typically an unmatched ", or backslash without escaped char. - if text_until_cursor.count('"')==1: - open_quotes = 1 - lsplit = text_until_cursor.split('"')[-1] - elif text_until_cursor.count("'")==1: - open_quotes = 1 - lsplit = text_until_cursor.split("'")[-1] - else: - return [] - except IndexError: - # tab pressed on empty line - lsplit = "" + # track strings with open quotes + open_quotes = has_open_quotes(text_until_cursor) + + if '(' in text_until_cursor or '[' in text_until_cursor: + lsplit = text + else: + try: + # arg_split ~ shlex.split, but with unicode bugs fixed by us + lsplit = arg_split(text_until_cursor)[-1] + except ValueError: + # typically an unmatched ", or backslash without escaped char. + if open_quotes: + lsplit = text_until_cursor.split(open_quotes)[-1] + else: + return [] + except IndexError: + # tab pressed on empty line + lsplit = "" if not open_quotes and lsplit != protect_filename(lsplit): - # if protectables are found, do matching on the whole escaped - # name - has_protectables = 1 + # if protectables are found, do matching on the whole escaped name + has_protectables = True text0,text = text,lsplit else: - has_protectables = 0 + has_protectables = False text = os.path.expanduser(text) if text == "": return [text_prefix + protect_filename(f) for f in self.glob("*")] + # Compute the matches from the filesystem m0 = self.clean_glob(text.replace('\\','')) + if has_protectables: # If we had protectables, we need to revert our changes to the # beginning of filename so that we don't double-write the part @@ -711,7 +734,7 @@ class IPCompleter(Completer): return None def complete(self, text=None, line_buffer=None, cursor_pos=None): - """Return the state-th possible completion for 'text'. + """Find completions for the given text and line context. This is called successively with state == 0, 1, 2, ... until it returns None. The completion should begin with 'text'. @@ -734,6 +757,14 @@ class IPCompleter(Completer): cursor_pos : int, optional Index of the cursor in the full line buffer. Should be provided by remote frontends where kernel has no access to frontend state. + + Returns + ------- + text : str + Text that was actually used in the completion. + + matches : list + A list of completion matches. """ #io.rprint('\nCOMP1 %r %r %r' % (text, line_buffer, cursor_pos)) # dbg @@ -772,7 +803,7 @@ class IPCompleter(Completer): except: # Show the ugly traceback if the matcher causes an # exception, but do NOT crash the kernel! - sys.excepthook() + sys.excepthook(*sys.exc_info()) else: for matcher in self.matchers: self.matches = matcher(text) diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index f3c3a57..e3ae238 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -5,6 +5,7 @@ #----------------------------------------------------------------------------- # stdlib +import os import sys import unittest @@ -13,6 +14,7 @@ import nose.tools as nt # our own packages from IPython.core import completer +from IPython.utils.tempdir import TemporaryDirectory #----------------------------------------------------------------------------- # Test functions @@ -113,3 +115,44 @@ class CompletionSplitterTestCase(unittest.TestCase): ('run foo', 'bar', 'foo'), ] check_line_split(self.sp, t) + + +def test_has_open_quotes1(): + for s in ["'", "'''", "'hi' '"]: + nt.assert_equal(completer.has_open_quotes(s), "'") + + +def test_has_open_quotes2(): + for s in ['"', '"""', '"hi" "']: + nt.assert_equal(completer.has_open_quotes(s), '"') + + +def test_has_open_quotes3(): + for s in ["''", "''' '''", "'hi' 'ipython'"]: + nt.assert_false(completer.has_open_quotes(s)) + + +def test_has_open_quotes4(): + for s in ['""', '""" """', '"hi" "ipython"']: + nt.assert_false(completer.has_open_quotes(s)) + + +def test_file_completions(): + + ip = get_ipython() + with TemporaryDirectory() as tmpdir: + prefix = os.path.join(tmpdir, 'foo') + suffixes = map(str, [1,2]) + names = [prefix+s for s in suffixes] + for n in names: + open(n, 'w').close() + + # Check simple completion + c = ip.complete(prefix)[1] + nt.assert_equal(c, names) + + # Now check with a function call + cmd = 'a = f("%s' % prefix + c = ip.complete(prefix, cmd)[1] + comp = [prefix+s for s in suffixes] + nt.assert_equal(c, comp)