Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional prefix arg for dynamic key completions #13921

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
20 changes: 15 additions & 5 deletions IPython/core/completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2486,12 +2486,22 @@ def python_func_kw_matches(self, text):
return argMatches

@staticmethod
def _get_keys(obj: Any) -> List[Any]:
def _get_keys(obj: Any, prefix: Optional[str] = None) -> List[Any]:
# Objects can define their own completions by defining an
# _ipy_key_completions_() method.
method = get_real_method(obj, '_ipython_key_completions_')
if method is not None:
return method()
key_completions_getter = get_real_method(obj, "_ipython_key_completions_")

if key_completions_getter is not None:
# older versions of ipython assumed _ipython_key_completions_ took no arguments
if "prefix" in inspect.signature(key_completions_getter).parameters:
if prefix is not None:
# strip leading apostrophe from key before passing to ._ipython_key_completions
return key_completions_getter(prefix=prefix[1:])
else:
return key_completions_getter(prefix=None)

Comment on lines +2496 to +2502
Copy link
Member

Choose a reason for hiding this comment

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

What I was suggesting was that instead of passing a prefix string we would pass an entire CompletionContext, something like:

class ItemCompletionContext(CompletionContext):
    """Completion context for item sub-matchers (providing information about `__getitem__`)"""
    key_prefix: str
Suggested change
if "prefix" in inspect.signature(key_completions_getter).parameters:
if prefix is not None:
# strip leading apostrophe from key before passing to ._ipython_key_completions
return key_completions_getter(prefix=prefix[1:])
else:
return key_completions_getter(prefix=None)
if (
hasattr(key_completions_getter, 'matcher_api_version')
and key_completions_getter.matcher_api_version == 2
):
item_context = ItemCompletionContext(
key_prefix=key_prefix,
full_text=context.full_text,
cursor_line=context.cursor_line,
cursor_position=context.cursor_position,
token=context.token,
)
return key_completions_getter(item_context)['completions']

Copy link
Member

Choose a reason for hiding this comment

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

As for stripping leading apostrophe: what if the key is a number? Or hex-number? Or raw string? I think we do not want to modify the prefix.

else:
return key_completions_getter()

# Special case some common in-memory dict-like types
if isinstance(obj, dict) or _safe_isinstance(obj, "pandas", "DataFrame"):
Expand Down Expand Up @@ -2541,7 +2551,7 @@ def dict_key_matches(self, text: str) -> List[str]:
if obj is not_found:
return []

keys = self._get_keys(obj)
keys = self._get_keys(obj, key_prefix)
if not keys:
return keys

Expand Down
30 changes: 30 additions & 0 deletions IPython/core/tests/test_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,23 @@ def _ipython_key_completions_(self):
return list(self.things)


class KeyCompleteableFileSystemGivenPrefix:
def __init__(self, things=()):
self.things = things

def _ipython_key_completions_(self, prefix):
"""Completes key only up to next folder separator"""

def depth(path):
return path.count("/")

return [
folder
for folder in self.things
if folder.startswith(prefix) and depth(folder) <= depth(prefix)
]


class TestCompleter(unittest.TestCase):
def setUp(self):
"""
Expand Down Expand Up @@ -1384,6 +1401,19 @@ def test_object_key_completion(self):
self.assertIn("qwerty", matches)
self.assertIn("qwick", matches)

def test_object_key_completion_given_prefix(self):
ip = get_ipython()
ip.user_ns["key_completable"] = KeyCompleteableFileSystemGivenPrefix(
["folder1", "folder1/folder2", "folder1/folder3"]
)

_, matches = ip.Completer.complete(line_buffer="key_completable['f")
self.assertIn("folder1", matches)

_, matches = ip.Completer.complete(line_buffer="key_completable['folder1/f")
self.assertIn("folder1/folder2", matches)
self.assertIn("folder1/folder3", matches)

def test_class_key_completion(self):
ip = get_ipython()
NamedInstanceClass("qwerty")
Expand Down
7 changes: 7 additions & 0 deletions docs/source/config/integrating.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ You can also customise key completions for your objects, e.g. pressing tab after
returns a list of objects which are possible keys in a subscript expression
``obj[key]``.

You can also optionally change your key completions dynamically as a function of the part of the key typed so far,
by passing a ``prefix`` arg to ``_ipython_key_completions_()``.
For example you could have ``obj["f`` tab-complete to ``obj["folder1``,
whilst ``obj["folder1/f`` tab-completes to ``obj["folder1/folder2``
(as opposed to having ``obj["f`` tab-complete to both ``obj["folder1`` and ``obj["folder1/folder2`` up front).
The ``prefix`` arg feature is only available in IPython versions later than v8.9.0.

.. versionadded:: 5.0
Custom key completions

Expand Down
6 changes: 6 additions & 0 deletions docs/source/whatsnew/pr/autocomplete-given-prefix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Autocomplete keys given substring prefix
----------------------------------------

Added an optional ``prefix`` arg to ``_ipython_key_completions_``, which allows key completion suggestions to vary depending on the substring alreday typed out by the user.

See :ghpull:`13921`.