Repository: ipython/ipython

Issue:
Dict key completion with bytes keys raises
Dict key tab completion raises when trying to complete a prefix of a bytes literal that is a valid key in the dict, as below.  This issue persists after `pip install --upgrade ipython`.

``` python
Python 3.4.3 (default, Mar  3 2015, 10:49:33) 
Type "copyright", "credits" or "license" for more information.

IPython 3.2.1 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: foo = {b'spam': True, 'ham': False}

In [2]: foo['s<TAB>
%%script             %%system             %store               set                  stat+tlrc.BRIK.gz    str                  symptoms.csv
%%sh                 %save                %sx                  setattr              stat+tlrc.HEAD       sum                  
%%svg                %sc                  %system              slice                staticmethod         sun_et_al_seeds.txt  
%%sx                 %set_env             scripts/             sorted               stims/               super                

In [2]: foo['h<TAB>
%%html    %hist     %history  ham       hasattr   hash      help      hex       

In [2]: foo[b'h<TAB>
%%html    %hist     %history  hasattr   hash      help      hex       

In [2]: foo[b's<TAB>Traceback (most recent call last):
  File "/home/schwabacher/lib/python3.4/site-packages/IPython/core/completer.py", line 1092, in complete
    self.matches.extend(matcher(text))
  File "/home/schwabacher/lib/python3.4/site-packages/IPython/core/completer.py", line 917, in dict_key_matches
    closing_quote, token_offset, matches = match_dict_keys(keys, prefix, self.splitter.delims)
  File "/home/schwabacher/lib/python3.4/site-packages/IPython/core/completer.py", line 449, in match_dict_keys
    rem_repr = repr(rem + '"')
TypeError: can't concat bytes to str

If you suspect this is an IPython bug, please report it at:
    https://github.com/ipython/ipython/issues
or send an email to the mailing list at ipython-dev@scipy.org

You can print a more detailed traceback right now with "%tb", or use "%debug"
to interactively debug it.

Extra-detailed tracebacks for bug-reporting purposes can be enabled via:
    %config Application.verbose_crash=True


%%script             %%system             %store               set                  stat+tlrc.BRIK.gz    str                  symptoms.csv
%%sh                 %save                %sx                  setattr              stat+tlrc.HEAD       sum                  
%%svg                %sc                  %system              slice                staticmethod         sun_et_al_seeds.txt  
%%sx                 %set_env             scripts/             sorted               stims/               super                
```


Discussion:

Patch:
diff --git a/IPython/core/completer.py b/IPython/core/completer.py
index d2637c4bb..80aafee41 100644
--- a/IPython/core/completer.py
+++ b/IPython/core/completer.py
@@ -622,8 +622,27 @@ def get__all__entries(obj):
     return [cast_unicode_py2(w) for w in words if isinstance(w, str)]
 
 
-def match_dict_keys(keys, prefix, delims):
-    """Used by dict_key_matches, matching the prefix to a list of keys"""
+def match_dict_keys(keys: List[str], prefix: str, delims: str):
+    """Used by dict_key_matches, matching the prefix to a list of keys
+
+    Parameters
+    ==========
+        keys:
+            list of keys in dictionary currently being completed.
+        prefix:
+            Part of the text already typed by the user. e.g. `mydict[b'fo`
+        delims:
+            String of delimiters to consider when finding the current key.
+
+    Returns
+    =======
+
+    A tuple of three elements: ``quote``, ``token_start``, ``matched``, with
+    ``quote`` being the quote that need to be used to close current string.
+    ``token_start`` the position where the replacement should start occurring,
+    ``matches`` a list of replacement/completion
+
+    """
     if not prefix:
         return None, 0, [repr(k) for k in keys
                       if isinstance(k, (str, bytes))]
@@ -639,7 +658,6 @@ def match_dict_keys(keys, prefix, delims):
     token_start = token_match.start()
     token_prefix = token_match.group()
 
-    # TODO: support bytes in Py3k
     matched = []
     for key in keys:
         try:
@@ -652,7 +670,7 @@ def match_dict_keys(keys, prefix, delims):
         # reformat remainder of key to begin with prefix
         rem = key[len(prefix_str):]
         # force repr wrapped in '
-        rem_repr = repr(rem + '"')
+        rem_repr = repr(rem + '"') if isinstance(rem, str) else repr(rem + b'"')
         if rem_repr.startswith('u') and prefix[0] not in 'uU':
             # Found key is unicode, but prefix is Py2 string.
             # Therefore attempt to interpret key as string.


Test patch:
diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py
index dab79ecad..cd193b934 100644
--- a/IPython/core/tests/test_completer.py
+++ b/IPython/core/tests/test_completer.py
@@ -20,7 +20,7 @@
 from IPython.utils.generics import complete_object
 from IPython.testing import decorators as dec
 
-from IPython.core.completer import Completion, provisionalcompleter
+from IPython.core.completer import Completion, provisionalcompleter, match_dict_keys
 from nose.tools import assert_in, assert_not_in
 
 #-----------------------------------------------------------------------------
@@ -526,7 +526,28 @@ def test_magic_completion_order():
 
     # Order of user variable and line and cell magics with same name:
     text, matches = c.complete('timeit')
-    nt.assert_equal(matches, ["timeit", "%timeit","%%timeit"])
+    nt.assert_equal(matches, ["timeit", "%timeit", "%%timeit"])
+
+def test_match_dict_keys():
+    """
+    Test that match_dict_keys works on a couple of use case does return what
+    expected, and does not crash
+    """
+    delims = ' \t\n`!@#$^&*()=+[{]}\\|;:\'",<>?'
+
+
+    keys = ['foo', b'far']
+    assert match_dict_keys(keys, "b'", delims=delims)  == ("'", 2 ,['far'])
+    assert match_dict_keys(keys, "b'f", delims=delims) == ("'", 2 ,['far'])
+    assert match_dict_keys(keys, 'b"', delims=delims)  == ('"', 2 ,['far'])
+    assert match_dict_keys(keys, 'b"f', delims=delims) == ('"', 2 ,['far'])
+
+    assert match_dict_keys(keys, "'", delims=delims)  == ("'", 1 ,['foo'])
+    assert match_dict_keys(keys, "'f", delims=delims) == ("'", 1 ,['foo'])
+    assert match_dict_keys(keys, '"', delims=delims)  == ('"', 1 ,['foo'])
+    assert match_dict_keys(keys, '"f', delims=delims) == ('"', 1 ,['foo'])
+    
+    match_dict_keys
 
 
 def test_dict_key_completion_string():