From 9663a9adc4c87490f1dc59c8b6f32cdfd0c5094a 2023-02-13 09:34:33 From: Matthias Bussonnier Date: 2023-02-13 09:34:33 Subject: [PATCH] Strip prefix in `attr_matches` result (#13943) Fixes #13935 Reasoning behind implementation chosen: - `a.b.c` prefix in `a.b.c.` needs to be preserved as otherwise the completer will replace it with completion rather than appending (so we cannot just use `.suffix`, we need to use `a.b.c.suffix` here) - as in the issue we cannot use `a b.suffix` but need to use `b.suffix` or `.suffix` - `d['a b']` prefix cannot be split using space splitting so we need to tokenize - however, we can do either `a[0].suffix` or `.suffix` --- diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 486f875..42e9eb7 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -1162,11 +1162,36 @@ class Completer(Configurable): raise except Exception: # Silence errors from completion function - #raise # dbg pass # Build match list to return n = len(attr) - return ["%s.%s" % (expr, w) for w in words if w[:n] == attr] + + # Note: ideally we would just return words here and the prefix + # reconciliator would know that we intend to append to rather than + # replace the input text; this requires refactoring to return range + # which ought to be replaced (as does jedi). + tokens = _parse_tokens(expr) + rev_tokens = reversed(tokens) + skip_over = {tokenize.ENDMARKER, tokenize.NEWLINE} + name_turn = True + + parts = [] + for token in rev_tokens: + if token.type in skip_over: + continue + if token.type == tokenize.NAME and name_turn: + parts.append(token.string) + name_turn = False + elif token.type == tokenize.OP and token.string == "." and not name_turn: + parts.append(token.string) + name_turn = True + else: + # short-circuit if not empty nor name token + break + + prefix_after_space = "".join(reversed(parts)) + + return ["%s.%s" % (prefix_after_space, w) for w in words if w[:n] == attr] def _evaluate_expr(self, expr): obj = not_found diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 7783798..3ff6569 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -543,6 +543,7 @@ class TestCompleter(unittest.TestCase): """ ip = get_ipython() ip.ex("a=list(range(5))") + ip.ex("d = {'a b': str}") _, c = ip.complete(".", line="a[0].") self.assertFalse(".real" in c, "Shouldn't have completed on a[0]: %s" % c) @@ -561,14 +562,14 @@ class TestCompleter(unittest.TestCase): _( "a[0].", 5, - "a[0].real", + ".real", "Should have completed on a[0].: %s", Completion(5, 5, "real"), ) _( "a[0].r", 6, - "a[0].real", + ".real", "Should have completed on a[0].r: %s", Completion(5, 6, "real"), ) @@ -576,10 +577,24 @@ class TestCompleter(unittest.TestCase): _( "a[0].from_", 10, - "a[0].from_bytes", + ".from_bytes", "Should have completed on a[0].from_: %s", Completion(5, 10, "from_bytes"), ) + _( + "assert str.star", + 14, + "str.startswith", + "Should have completed on `assert str.star`: %s", + Completion(11, 14, "startswith"), + ) + _( + "d['a b'].str", + 12, + ".strip", + "Should have completed on `d['a b'].str`: %s", + Completion(9, 12, "strip"), + ) def test_omit__names(self): # also happens to test IPCompleter as a configurable