Skip to content

Fix Tab Completion Context Detection#14838

Merged
krassowski merged 15 commits intoipython:mainfrom
Darshan808:fix-completions
Mar 26, 2025
Merged

Fix Tab Completion Context Detection#14838
krassowski merged 15 commits intoipython:mainfrom
Darshan808:fix-completions

Conversation

@Darshan808
Copy link
Copy Markdown
Collaborator

Fixes #14836

This PR resolves the issue with IPython tab completion incorrectly triggering attribute completion when a dot (.) appears in the line. Instead of just checking for '.', I've added a new function, _determine_completion_context, to accurately determine whether the cursor is in an attribute or global completion context.

The solution has been tested against multiple edge cases (e.g., nested expressions, strings, comments) and handles them all correctly. Please review the changes and let me know if you have any feedback or suggestions!

@krassowski
Copy link
Copy Markdown
Member

@Darshan808 can you merge my branch with a test case into your branch and then I will close #14837?

It looks like CI indicates a failure on a pre-existing test:

FAILED tests/test_completer.py::TestCompleter::test_greedy_completions - AssertionError: '.real' not found in ['.editorconfig', '.flake8', '.git-blame-ignore-revs', '.git/', '.gitattributes', '.github/', '.gitignore', '.mailmap', '.meeseeksdev.yml', '.pre-commit-config.yaml', '.readthedocs.yaml'] : Should have completed on a[0].r: ['.editorconfig', '.flake8', '.git-blame-ignore-revs', '.git/', '.gitattributes', '.github/', '.gitignore', '.mailmap', '.meeseeksdev.yml', '.pre-commit-config.yaml', '.readthedocs.yaml']

General feedback on the fix:

  • let's use Enum over string comparison
  • regular expressions can be slow; an alternative would be to use tokenizer; at a glance your solution may be fine but we will need to test with somewhat longer lines too
  • can you add more tests for the various scenarios that you considered?

@Darshan808 Darshan808 closed this Mar 14, 2025
@Darshan808 Darshan808 reopened this Mar 14, 2025
@Darshan808
Copy link
Copy Markdown
Collaborator Author

I am not sure if the test failure is due to my changes. Initially, 4 tests were failing, and on rerun, only 1 is failing. When I run it locally, the test sometimes passes and sometimes fails—are these tests flaky?

Also, about adding tests for my changes—I tested the _get_completion_context function by providing different lines and checking if it gives the correct completion context. Could we test it the same way in CI? There are already other tests to verify completions.

I'll start by adding a test to verify _get_completion_context. If we need to change the approach and test for the actual completion returned, we can do that too.

@krassowski
Copy link
Copy Markdown
Member

I think it a good idea to have both unit tests (for _determine_completion_context) and a few integration tests. Since #14837 already includes one integration test, you could build on that but we would not want to add too many.

@Darshan808
Copy link
Copy Markdown
Collaborator Author

I’ve added two more integration tests: one for completion inside f-strings and another for completion inside dictionaries. I believe these cover the main cases, but do you think there are any other scenarios worth testing?

Also, regarding the test failures, I see that one test, test_greedy_completions, is consistently failing. I’m looking into it, but I haven’t found any reasons for why it’s failing yet.

Comment thread tests/test_completer.py
@Darshan808
Copy link
Copy Markdown
Collaborator Author

I modified the regex to check for patterns like <anything>.w. I'm not entirely sure if this is the best approach, but it seems to work well and passes all the test cases I could think of.

@Darshan808 Darshan808 requested a review from krassowski March 14, 2025 17:49
Comment thread IPython/core/completer.py Outdated
Comment on lines +2389 to +2392
if "#" in text:
parts = text.split("#")
if len(parts) > 1:
return True
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. What is the split() used for?
  2. What about d["#"].a - this will be relatively common with pandas column access, some columns will contain # in name

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved the logic for comment checking!

Comment thread IPython/core/completer.py Outdated
i = 0

while i < len(text):
# Check for f-string start
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be future-proof we could allow t-strings (https://peps.python.org/pep-0750/). There is a strong indication that this JEP will be approved (https://discuss.python.org/t/pep750-template-strings-new-updates/71594/118).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Added this!

Comment thread IPython/core/completer.py Outdated
if line.endswith("."):
return self._CompletionContextType.ATTRIBUTE

last_token_match = re.search(r"([\w]+)$", line)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment with an example explaining which case it addresses?

In Python tokens can include digits (but cannot start with digits), not sure if this is relevant here yet.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comments!

can include digits (but cannot start with digits)

Initially, I didn't think this was important, but I've now updated the regex to account for it.

@Darshan808
Copy link
Copy Markdown
Collaborator Author

After reanalyzing the logic, I found redundant checks and simplified it to a single regex search.

@Darshan808 Darshan808 requested a review from krassowski March 17, 2025 18:56
Copy link
Copy Markdown
Member

@krassowski krassowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few edge cases

Comment thread IPython/core/completer.py
"""Match attributes or global python names"""
text = context.line_with_cursor
if "." in text:
completion_type = self._determine_completion_context(text)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A fundamental problem here is that it ignores the cursor position. So for example this works:

image

This fails:

image

I think we need to use context.text_until_cursor instead, see:

class CompletionContext:
"""Completion context provided as an argument to matchers in the Matcher API v2."""
# rationale: many legacy matchers relied on completer state (`self.text_until_cursor`)
# which was not explicitly visible as an argument of the matcher, making any refactor
# prone to errors; by explicitly passing `cursor_position` we can decouple the matchers
# from the completer, and make substituting them in sub-classes easier.
#: Relevant fragment of code directly preceding the cursor.
#: The extraction of token is implemented via splitter heuristic
#: (following readline behaviour for legacy reasons), which is user configurable
#: (by switching the greedy mode).
token: str
#: The full available content of the editor or buffer
full_text: str
#: Cursor position in the line (the same for ``full_text`` and ``text``).
cursor_position: int
#: Cursor line in ``full_text``.
cursor_line: int
#: The maximum number of completions that will be used downstream.
#: Matchers can use this information to abort early.
#: The built-in Jedi matcher is currently excepted from this limit.
# If not given, return all possible completions.
limit: Optional[int]
@cached_property
def text_until_cursor(self) -> str:
return self.line_with_cursor[: self.cursor_position]
@cached_property
def line_with_cursor(self) -> str:
return self.full_text.split("\n")[self.cursor_line]

Comment thread IPython/core/completer.py
and text[i] in ("f", "t")
and (text[i + 1] == '"' or text[i + 1] == "'")
):
in_template_string = True
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is never reset to False, here is how it fails - can you add a test case?

image

Comment thread IPython/core/completer.py
# Ex: 'var', '', '3.', '-42.5.'
return self._CompletionContextType.GLOBAL

def _is_in_string_or_comment(self, text):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fail on nested f-strings:

image

I am not sure if we want to support it (if it is easy then sure, if it is hard - not sure), but at least need to document it, possibly by adding xfail test?

Comment thread IPython/core/completer.py Outdated
Comment on lines +2375 to +2383
# Match 'anything-dot-word' at end for attribute context.
# Ex: 'obj.', 'np.random.ran'.
chain_match = re.search(r"([a-zA-Z_].*)\.([a-zA-Z_][a-zA-Z0-9_]*)?$", line)
if chain_match:
return self._CompletionContextType.ATTRIBUTE

# No attribute pattern → GLOBAL.
# Ex: 'var', '', '3.', '-42.5.'
return self._CompletionContextType.GLOBAL
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some edge cases not covered, probably not important enough to cover, but listing them for completness:

(3).to_<tab>

expecting (3).to_bytes

Note that 3.to_bytes is invalid in Python

3.1.as_<tab>
-3.1.r<tab>

Expecting 3.1.as_integer_ratio and -3.1.real respectively.

@Darshan808
Copy link
Copy Markdown
Collaborator Author

Darshan808 commented Mar 24, 2025

covered cases like these

3.1.as_<tab>
-3.1.r<tab>
(3).to_<tab>

and used context.text_until_cursor
However, handling nested f-strings seems to be more challenging. I’ve added an xfail test for it to track the issue.

Since Jedi is a dependency of IPython and its completion is very robust, I think we can skip some very special cases like these.

@Darshan808
Copy link
Copy Markdown
Collaborator Author

I’ve run into an interesting issue with the python_matcher. For f'{a., it correctly suggests attributes like .append (context_type is attribute). But for f'{a.}' with the cursor in the same spot, despite also identifying as ATTRIBUTE, it completes to .ipynb instead.

  # Debug line
   completion_type = self._determine_completion_context(text)
   print(completion_type, " for ", text)
a = []

In [3]: f'{a.|        # suggests .append
_CompletionContextType.ATTRIBUTE  for  f'{a.

In [4]: f'{a.|}'     # suggests .ipynb
_CompletionContextType.ATTRIBUTE  for  f'{a.

@krassowski
Copy link
Copy Markdown
Member

I see that there is a couple of things that could go wrong in:

_ATTR_MATCH_RE = re.compile(r"(.+)\.(\w*)$")
def _attr_matches(
self, text: str, include_prefix: bool = True
) -> tuple[Sequence[str], str]:
m2 = self._ATTR_MATCH_RE.match(self.line_buffer)
if not m2:
return [], ""
expr, attr = m2.group(1, 2)
obj = self._evaluate_expr(expr)

Maybe it is because it uses the line_buffer instead of text_until_cursor?

If this is not as easy to solve maybe we can open an issue to track it.

@Darshan808
Copy link
Copy Markdown
Collaborator Author

Darshan808 commented Mar 24, 2025

Maybe it is because it uses the line_buffer instead of text_until_cursor?

Yes, using argument text instead of self.line_buffer fixed the issue. Thanks!

Comment thread IPython/core/completer.py
ATTRIBUTE = "attribute" # For attribute completion
GLOBAL = "global" # For global completion

def _determine_completion_context(self, line):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documenting another edge case - double braces represent literals braces so should return global:

image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think fixing this one might be of relatively little value.

Comment thread IPython/core/completer.py Outdated
Comment on lines +2386 to +2389
# Match tuples followed by a dot (e.g., (a, b).index)
tuple_attr_match = re.search(
r"\(\s*[a-zA-Z_][a-zA-Z0-9_]*\s*(,\s*[a-zA-Z_][a-zA-Z0-9_]*\s*)+\)\.$", line
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is overly strict as it only handles tuples with two or more identifiers. It will miss on cases like:

  • (a,)
  • (1, 2)
  • ('a', 'b')
    etc

Now, I don't know if we should special-case tuples over other similar collections (lists, sets). I also worry that making this syntax more complex to account for all these cases would make it slower, and completer needs to be very fast.

I know that using tokenizer out right will often not work as it does not work for incomplete lines, but could we use a hybrid approach where for cases like this - where we assume that there is a valid tuple (list, set) followed by a dot we just remove the final dot and see if tokenizer can parse it and if the result is one of: list, tuple, dictionary, set?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're interested in speed you could compile the expression to speed things up.

https://docs.python.org/3/library/re.html#re.compile

@Darshan808
Copy link
Copy Markdown
Collaborator Author

Darshan808 commented Mar 25, 2025

There are so many edge cases, and trying to make the logic perfect keeps causing some tests to fail. Covering all cases would require a lot of extra checks. My initial approach of matching anything.word handles most cases well, with only a few minor flaws. If we don’t need to be too strict, this simpler solution with just two regex expressions could work. What do you think?
I have added few new tests and also unsupported checks in xfail tests.

Copy link
Copy Markdown
Member

@krassowski krassowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Darshan808. I think this is a good trade-off between completeness and complexity. I left one tiny suggestion on making part of the code easier to read.

Comment thread IPython/core/completer.py
@krassowski krassowski merged commit 46b09de into ipython:main Mar 26, 2025
18 checks passed
@krassowski krassowski added this to the 8.34 milestone Mar 28, 2025
@krassowski
Copy link
Copy Markdown
Member

@meeseeksdev please backport to 8.x

@lumberbot-app
Copy link
Copy Markdown
Contributor

lumberbot-app Bot commented Mar 28, 2025

Owee, I'm MrMeeseeks, Look at me.

There seem to be a conflict, please backport manually. Here are approximate instructions:

  1. Checkout backport branch and update it.
git checkout 8.x
git pull
  1. Cherry pick the first parent branch of the this PR on top of the older branch:
git cherry-pick -x -m1 46b09de968521d08a2903316090557da547007ee
  1. You will likely have some merge/cherry-pick conflict here, fix them and commit:
git commit -am 'Backport PR #14838: Fix Tab Completion Context Detection'
  1. Push to a named branch:
git push YOURFORK 8.x:auto-backport-of-pr-14838-on-8.x
  1. Create a PR against branch 8.x, I would have named this PR:

"Backport PR #14838 on branch 8.x (Fix Tab Completion Context Detection)"

And apply the correct labels and milestones.

Congratulations — you did some good work! Hopefully your backport PR will be tested by the continuous integration and merged soon!

Remember to remove the Still Needs Manual Backport label once the PR gets merged.

If these instructions are inaccurate, feel free to suggest an improvement.

@lumberbot-app lumberbot-app Bot added the Still Needs Manual Backport Added My MrMeeseeks when a backport fails. Help by backporting it, solving conflicts, send PR. label Mar 28, 2025
krassowski added a commit to krassowski/ipython that referenced this pull request Mar 28, 2025
@krassowski krassowski removed the Still Needs Manual Backport Added My MrMeeseeks when a backport fails. Help by backporting it, solving conflicts, send PR. label Mar 28, 2025
krassowski added a commit that referenced this pull request Mar 28, 2025
…n) (#14855)

Backport PR #14838 on branch 8.x
(Fix Tab Completion Context Detection)
@krassowski krassowski modified the milestones: 8.34, 8.35 Apr 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tab completion for globals does not work in lines with dots

3 participants