Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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.
  • Loading branch information...
commit 02eecaf061408f26a3c6029886b8794f73581938 1 parent ae65e73
Fernando Perez fperez authored
Showing with 98 additions and 24 deletions.
  1. +55 −24 IPython/core/completer.py
  2. +43 −0 IPython/core/tests/test_completer.py
79 IPython/core/completer.py
View
@@ -101,6 +101,27 @@
# 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 @@ def file_matches(self, text):
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 @@ def dispatch_custom_completer(self, text):
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 @@ def complete(self, text=None, line_buffer=None, cursor_pos=None):
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 @@ def complete(self, text=None, line_buffer=None, cursor_pos=None):
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)
43 IPython/core/tests/test_completer.py
View
@@ -5,6 +5,7 @@
#-----------------------------------------------------------------------------
# stdlib
+import os
import sys
import unittest
@@ -13,6 +14,7 @@
# our own packages
from IPython.core import completer
+from IPython.utils.tempdir import TemporaryDirectory
#-----------------------------------------------------------------------------
# Test functions
@@ -113,3 +115,44 @@ def test_spaces(self):
('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)
Please sign in to comment.
Something went wrong with that request. Please try again.