From fdd327591569ec5fc1d5c6d79f41df48bf5162b0 2020-06-23 23:31:40 From: Matthias Bussonnier Date: 2020-06-23 23:31:40 Subject: [PATCH] Merge pull request #12395 from cphyc/feature/tuple-completion --- diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 526aed6..dcee838 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -753,7 +753,8 @@ def get__all__entries(obj): return [w for w in words if isinstance(w, str)] -def match_dict_keys(keys: List[Union[str, bytes]], prefix: str, delims: str) -> Tuple[str, int, List[str]]: +def match_dict_keys(keys: List[Union[str, bytes, Tuple[Union[str, bytes]]]], prefix: str, delims: str, + extra_prefix: Optional[Tuple[str, bytes]]=None) -> Tuple[str, int, List[str]]: """Used by dict_key_matches, matching the prefix to a list of keys Parameters @@ -761,9 +762,12 @@ def match_dict_keys(keys: List[Union[str, bytes]], prefix: str, delims: str) -> keys: list of keys in dictionary currently being completed. prefix: - Part of the text already typed by the user. e.g. `mydict[b'fo` + 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. + extra_prefix: optional + Part of the text already typed in multi-key index cases. E.g. for + `mydict['foo', "bar", 'b`, this would be `('foo', 'bar')`. Returns ======= @@ -774,10 +778,37 @@ def match_dict_keys(keys: List[Union[str, bytes]], prefix: str, delims: str) -> ``matches`` a list of replacement/completion """ - keys = [k for k in keys if isinstance(k, (str, bytes))] + prefix_tuple = extra_prefix if extra_prefix else () + Nprefix = len(prefix_tuple) + def filter_prefix_tuple(key): + # Reject too short keys + if len(key) <= Nprefix: + return False + # Reject keys with non str/bytes in it + for k in key: + if not isinstance(k, (str, bytes)): + return False + # Reject keys that do not match the prefix + for k, pt in zip(key, prefix_tuple): + if k != pt: + return False + # All checks passed! + return True + + filtered_keys:List[Union[str,bytes]] = [] + def _add_to_filtered_keys(key): + if isinstance(key, (str, bytes)): + filtered_keys.append(key) + + for k in keys: + if isinstance(k, tuple): + if filter_prefix_tuple(k): + _add_to_filtered_keys(k[Nprefix]) + else: + _add_to_filtered_keys(k) + if not prefix: - return '', 0, [repr(k) for k in keys - if isinstance(k, (str, bytes))] + return '', 0, [repr(k) for k in filtered_keys] quote_match = re.search('["\']', prefix) assert quote_match is not None # silence mypy quote = quote_match.group() @@ -793,7 +824,7 @@ def match_dict_keys(keys: List[Union[str, bytes]], prefix: str, delims: str) -> token_prefix = token_match.group() matched:List[str] = [] - for key in keys: + for key in filtered_keys: try: if not key.startswith(prefix_str): continue @@ -1641,6 +1672,15 @@ class IPCompleter(Completer): ) \[ # open bracket \s* # and optional whitespace + # Capture any number of str-like objects (e.g. "a", "b", 'c') + ((?:[uUbB]? # string prefix (r not handled) + (?: + '(?:[^']|(??" + + keys = [("foo", "bar"), ("foo", "oof"), ("foo", b"bar"), ('other', 'test')] + + # Completion on first key == "foo" + assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["bar", "oof"]) + assert match_dict_keys(keys, "\"", delims=delims, extra_prefix=("foo",)) == ("\"", 1, ["bar", "oof"]) + assert match_dict_keys(keys, "'o", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["oof"]) + assert match_dict_keys(keys, "\"o", delims=delims, extra_prefix=("foo",)) == ("\"", 1, ["oof"]) + assert match_dict_keys(keys, "b'", delims=delims, extra_prefix=("foo",)) == ("'", 2, ["bar"]) + assert match_dict_keys(keys, "b\"", delims=delims, extra_prefix=("foo",)) == ("\"", 2, ["bar"]) + assert match_dict_keys(keys, "b'b", delims=delims, extra_prefix=("foo",)) == ("'", 2, ["bar"]) + assert match_dict_keys(keys, "b\"b", delims=delims, extra_prefix=("foo",)) == ("\"", 2, ["bar"]) + + # No Completion + assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("no_foo",)) == ("'", 1, []) + assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("fo",)) == ("'", 1, []) + + keys = [('foo1', 'foo2', 'foo3', 'foo4'), ('foo1', 'foo2', 'bar', 'foo4')] + assert match_dict_keys(keys, "'foo", delims=delims, extra_prefix=('foo1',)) == ("'", 1, ["foo2", "foo2"]) + assert match_dict_keys(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2')) == ("'", 1, ["foo3"]) + assert match_dict_keys(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3')) == ("'", 1, ["foo4"]) + assert match_dict_keys(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3', 'foo4')) == ("'", 1, []) + def test_dict_key_completion_string(self): """Test dictionary key completion for string keys""" ip = get_ipython() @@ -891,12 +920,16 @@ class TestCompleter(unittest.TestCase): "bad": None, object(): None, 5: None, + ("abe", None): None, + (None, "abf"): None } _, matches = complete(line_buffer="d['a") nt.assert_in("abc", matches) nt.assert_in("abd", matches) nt.assert_not_in("bad", matches) + nt.assert_not_in("abe", matches) + nt.assert_not_in("abf", matches) assert not any(m.endswith(("]", '"', "'")) for m in matches), matches # check escaping and whitespace @@ -930,6 +963,74 @@ class TestCompleter(unittest.TestCase): _, matches = complete(line_buffer="d['before-af") nt.assert_in("before-after", matches) + # check completion on tuple-of-string keys at different stage - on first key + ip.user_ns["d"] = {('foo', 'bar'): None} + _, matches = complete(line_buffer="d[") + nt.assert_in("'foo'", matches) + nt.assert_not_in("'foo']", matches) + nt.assert_not_in("'bar'", matches) + nt.assert_not_in("foo", matches) + nt.assert_not_in("bar", matches) + + # - match the prefix + _, matches = complete(line_buffer="d['f") + nt.assert_in("foo", matches) + nt.assert_not_in("foo']", matches) + nt.assert_not_in("foo\"]", matches) + _, matches = complete(line_buffer="d['foo") + nt.assert_in("foo", matches) + + # - can complete on second key + _, matches = complete(line_buffer="d['foo', ") + nt.assert_in("'bar'", matches) + _, matches = complete(line_buffer="d['foo', 'b") + nt.assert_in("bar", matches) + nt.assert_not_in("foo", matches) + + # - does not propose missing keys + _, matches = complete(line_buffer="d['foo', 'f") + nt.assert_not_in("bar", matches) + nt.assert_not_in("foo", matches) + + # check sensitivity to following context + _, matches = complete(line_buffer="d['foo',]", cursor_pos=8) + nt.assert_in("'bar'", matches) + nt.assert_not_in("bar", matches) + nt.assert_not_in("'foo'", matches) + nt.assert_not_in("foo", matches) + + _, matches = complete(line_buffer="d['']", cursor_pos=3) + nt.assert_in("foo", matches) + assert not any(m.endswith(("]", '"', "'")) for m in matches), matches + + _, matches = complete(line_buffer='d[""]', cursor_pos=3) + nt.assert_in("foo", matches) + assert not any(m.endswith(("]", '"', "'")) for m in matches), matches + + _, matches = complete(line_buffer='d["foo","]', cursor_pos=9) + nt.assert_in("bar", matches) + assert not any(m.endswith(("]", '"', "'")) for m in matches), matches + + _, matches = complete(line_buffer='d["foo",]', cursor_pos=8) + nt.assert_in("'bar'", matches) + nt.assert_not_in("bar", matches) + + # Can complete with longer tuple keys + ip.user_ns["d"] = {('foo', 'bar', 'foobar'): None} + + # - can complete second key + _, matches = complete(line_buffer="d['foo', 'b") + nt.assert_in('bar', matches) + nt.assert_not_in('foo', matches) + nt.assert_not_in('foobar', matches) + + # - can complete third key + _, matches = complete(line_buffer="d['foo', 'bar', 'fo") + nt.assert_in('foobar', matches) + nt.assert_not_in('foo', matches) + nt.assert_not_in('bar', matches) + + def test_dict_key_completion_contexts(self): """Test expression contexts in which dict key completion occurs""" ip = get_ipython()