Repository: ipython/ipython

Issue:
ipython==8.8.0 has weird tab completion bug
<!-- This is the repository for IPython command line, if you can try to make sure this question/bug/feature belong here and not on one of the Jupyter repositories. 

If it's a generic Python/Jupyter question, try other forums or discourse.jupyter.org.

If you are unsure, it's ok to post here, though, there are few maintainer so you might not get a fast response. 

-->

    self.my_attr.my_method(self.f<tab>
Becomes:

    self.my_attr.my_method(self.my_attr.my_method(self.foo
It looks like it would grab and prepend the front part of the line.

## Workaround
Rolling back to 8.7.0 solves it.

## Repro
Unfortunately, I failed to reproduce it with simple setup with a simple class and method and attribute.

But I easily reproduce this bug at my work projects (which are huge... and have so much complicated dependencies). I think this bug wouldn't manifest itself unless the code base is complicated enough.

I've confirmed 8.7.0 doesn't have this issue.

Discussion:

Patch:
diff --git a/IPython/core/completer.py b/IPython/core/completer.py
index f0bbb4e56..a41f492b0 100644
--- a/IPython/core/completer.py
+++ b/IPython/core/completer.py
@@ -1162,11 +1162,36 @@ def attr_matches(self, text):
             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


Test patch:
diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py
index 7783798eb..3ff6569ed 100644
--- a/IPython/core/tests/test_completer.py
+++ b/IPython/core/tests/test_completer.py
@@ -543,6 +543,7 @@ def test_greedy_completions(self):
         """
         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 @@ def _(line, cursor_pos, expect, message, completion):
             _(
                 "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 @@ def _(line, cursor_pos, expect, message, completion):
             _(
                 "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