-
-
Notifications
You must be signed in to change notification settings - Fork 34.4k
gh-130472: Integrate fancycompleter with the new repl, to get colored tab completions #130473
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
Merged
Merged
Changes from all commits
Commits
Show all changes
49 commits
Select commit
Hold shift + click to select a range
d7eff96
import fancycompleter from https://github.com/pdbpp/fancycompleter/co…
antocuni e22a210
add copyright notice
antocuni 374eff9
enable FancyCompleter by default, unless you set PYTHON_BASIC_COMPLETER
antocuni 9eeca3a
WIP: kill a lot of code which is no longer necessary
antocuni 1c5d27d
force colors for now
antocuni c621cf5
kill the logic to find a readline, we can always use _pyrepl.readline…
antocuni d839aa1
kill LazyVersion
antocuni f89b9ef
we surely don't need to support python 2.7 now :)
antocuni 04b5fee
kill ConfigurableClass
antocuni d733675
better name
antocuni 116a634
use _colorize instead of our own Color
antocuni 98d3f86
WIP: copy&adapt some tests from the original fancycompleter. They don…
antocuni 9d40f1b
edited by copilot: move from pytest-style to unittest-style
antocuni d04f41d
don't try to be too clever with exceptions: if a global name raises a…
antocuni 15ea5de
no longer needed
antocuni 0063a70
this doesn't test anything meaningful
antocuni 0af3c84
fix this test
antocuni d9a9f6f
fix this test
antocuni 0c26812
Fix this test
antocuni 30468ac
Apply hugovk suggestions from code review
antocuni 4700d1a
Apply suggestions from code review
antocuni 26591ae
Apply suggestions from code review
antocuni b2414f8
remove unneeded lazy import
antocuni 24e4afb
Update Lib/_pyrepl/fancycompleter.py
antocuni b5935bc
move import to module scope
antocuni 0fbdabc
move import
antocuni 06840b0
kill this for now, we can redintroduce it later if/when we enable fan…
antocuni 56384ee
this link is dead, add a comment to explain what it does instead
antocuni 49f90f5
fix precommit
antocuni 3c441e1
we need to make this import lazy, else we get circular imports
antocuni aabf91c
now that we have themes, we can kill the config object
antocuni 233c51d
style
antocuni 1a86caf
📜🤖 Added by blurb_it.
blurb-it[bot] ee882e7
fix mypy
antocuni 850d74b
document PYTHON_BASIC_COMPLETER
antocuni 7c603a2
try to manually fix the filename
antocuni cf6bf1e
Typo
antocuni 0dc7d16
reword
antocuni 49eda06
force PYTHON_COLORS=1 for tests which expects to see colors. Hopefull…
antocuni 3ce8d00
fix it in a different way: just look in the theme to find the expecte…
antocuni c71eed6
fix precommit
antocuni d6b77e0
Update Lib/_pyrepl/fancycompleter.py
antocuni f919815
put _colorize.FancyCompleter in alphabetical order w.r.t. the other s…
antocuni 97c684e
get_theme() is relatively expensive, fetch it early and cache it
antocuni 588c204
base is never used when calling commonprefix, remove it
antocuni 0646abb
Update Lib/_pyrepl/fancycompleter.py
antocuni 4b11774
there is no need to sort words in advance, we can just sort names later
antocuni 9b9b37d
undo 6a5bcfe9ed: there IS actually a good reason to sort the words in…
antocuni cba06ba
gh-130472: Fix pyrepl fancycompleter edge cases
pablogsal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| # Copyright 2010-2025 Antonio Cuni | ||
| # Daniel Hahler | ||
| # | ||
| # All Rights Reserved | ||
| """Colorful tab completion for Python prompt""" | ||
| from _colorize import ANSIColors, get_colors, get_theme | ||
| import rlcompleter | ||
| import keyword | ||
| import types | ||
|
|
||
| class Completer(rlcompleter.Completer): | ||
| """ | ||
| When doing something like a.b.<tab>, keep the full a.b.attr completion | ||
| stem so readline-style completion can keep refining the menu as you type. | ||
|
|
||
| Optionally, display the various completions in different colors | ||
| depending on the type. | ||
| """ | ||
| def __init__( | ||
| self, | ||
| namespace=None, | ||
| *, | ||
| use_colors='auto', | ||
| consider_getitems=True, | ||
| ): | ||
| from _pyrepl import readline | ||
| rlcompleter.Completer.__init__(self, namespace) | ||
| if use_colors == 'auto': | ||
| # use colors only if we can | ||
| use_colors = get_colors().RED != "" | ||
| self.use_colors = use_colors | ||
| self.consider_getitems = consider_getitems | ||
|
|
||
| if self.use_colors: | ||
| # In GNU readline, this prevents escaping of ANSI control | ||
| # characters in completion results. pyrepl's parse_and_bind() | ||
| # is a no-op, but pyrepl handles ANSI sequences natively | ||
| # via real_len()/stripcolor(). | ||
| readline.parse_and_bind('set dont-escape-ctrl-chars on') | ||
| self.theme = get_theme() | ||
| else: | ||
| self.theme = None | ||
|
|
||
| if self.consider_getitems: | ||
| delims = readline.get_completer_delims() | ||
| delims = delims.replace('[', '') | ||
| delims = delims.replace(']', '') | ||
| readline.set_completer_delims(delims) | ||
|
|
||
| def complete(self, text, state): | ||
| # if you press <tab> at the beginning of a line, insert an actual | ||
| # \t. Else, trigger completion. | ||
| if text == "": | ||
| return ('\t', None)[state] | ||
| else: | ||
| return rlcompleter.Completer.complete(self, text, state) | ||
|
|
||
| def _callable_postfix(self, val, word): | ||
| # disable automatic insertion of '(' for global callables | ||
| return word | ||
|
|
||
eendebakpt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| def _callable_attr_postfix(self, val, word): | ||
| return rlcompleter.Completer._callable_postfix(self, val, word) | ||
|
|
||
| def global_matches(self, text): | ||
| names = rlcompleter.Completer.global_matches(self, text) | ||
| prefix = commonprefix(names) | ||
| if prefix and prefix != text: | ||
| return [prefix] | ||
|
|
||
| names.sort() | ||
| values = [] | ||
| for name in names: | ||
| clean_name = name.rstrip(': ') | ||
| if keyword.iskeyword(clean_name) or keyword.issoftkeyword(clean_name): | ||
| values.append(None) | ||
| else: | ||
| try: | ||
| values.append(eval(name, self.namespace)) | ||
| except Exception: | ||
| values.append(None) | ||
| if self.use_colors and names: | ||
| return self.colorize_matches(names, values) | ||
| return names | ||
|
|
||
| def attr_matches(self, text): | ||
| try: | ||
| expr, attr, names, values = self._attr_matches(text) | ||
| except ValueError: | ||
| return [] | ||
|
|
||
| if not names: | ||
| return [] | ||
|
|
||
| if len(names) == 1: | ||
| # No coloring: when returning a single completion, readline | ||
| # inserts it directly into the prompt, so ANSI codes would | ||
| # appear as literal characters. | ||
| return [self._callable_attr_postfix(values[0], f'{expr}.{names[0]}')] | ||
|
|
||
| prefix = commonprefix(names) | ||
| if prefix and prefix != attr: | ||
| return [f'{expr}.{prefix}'] # autocomplete prefix | ||
|
|
||
| names = [f'{expr}.{name}' for name in names] | ||
| if self.use_colors: | ||
| return self.colorize_matches(names, values) | ||
|
|
||
| if prefix: | ||
| names.append(' ') | ||
| return names | ||
|
|
||
| def _attr_matches(self, text): | ||
| expr, attr = text.rsplit('.', 1) | ||
| if '(' in expr or ')' in expr: # don't call functions | ||
| return expr, attr, [], [] | ||
| try: | ||
| thisobject = eval(expr, self.namespace) | ||
pablogsal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| except Exception: | ||
| return expr, attr, [], [] | ||
|
|
||
| # get the content of the object, except __builtins__ | ||
pablogsal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| words = set(dir(thisobject)) - {'__builtins__'} | ||
|
|
||
| if hasattr(thisobject, '__class__'): | ||
| words.add('__class__') | ||
pablogsal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| words.update(rlcompleter.get_class_members(thisobject.__class__)) | ||
| names = [] | ||
| values = [] | ||
| n = len(attr) | ||
| if attr == '': | ||
| noprefix = '_' | ||
| elif attr == '_': | ||
| noprefix = '__' | ||
| else: | ||
| noprefix = None | ||
|
|
||
| # sort the words now to make sure to return completions in | ||
| # alphabetical order. It's easier to do it now, else we would need to | ||
| # sort 'names' later but make sure that 'values' in kept in sync, | ||
| # which is annoying. | ||
| words = sorted(words) | ||
| while True: | ||
| for word in words: | ||
| if ( | ||
| word[:n] == attr | ||
| and not (noprefix and word[:n+1] == noprefix) | ||
| ): | ||
| # Mirror rlcompleter's safeguards so completion does not | ||
| # call properties or reify lazy module attributes. | ||
| if isinstance(getattr(type(thisobject), word, None), property): | ||
| value = None | ||
| elif ( | ||
| isinstance(thisobject, types.ModuleType) | ||
| and isinstance( | ||
| thisobject.__dict__.get(word), | ||
| types.LazyImportType, | ||
| ) | ||
| ): | ||
| value = thisobject.__dict__.get(word) | ||
| else: | ||
| value = getattr(thisobject, word, None) | ||
|
|
||
| names.append(word) | ||
| values.append(value) | ||
| if names or not noprefix: | ||
| break | ||
| if noprefix == '_': | ||
| noprefix = '__' | ||
| else: | ||
| noprefix = None | ||
|
|
||
| return expr, attr, names, values | ||
|
|
||
| def colorize_matches(self, names, values): | ||
pablogsal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| matches = [self._color_for_obj(i, name, obj) | ||
| for i, (name, obj) | ||
| in enumerate(zip(names, values))] | ||
| # We add a space at the end to prevent the automatic completion of the | ||
| # common prefix, which is the ANSI escape sequence. | ||
| matches.append(' ') | ||
| return matches | ||
|
|
||
| def _color_for_obj(self, i, name, value): | ||
| t = type(value) | ||
| color = self._color_by_type(t) | ||
| # Encode the match index into a fake escape sequence that | ||
| # stripcolor() can still remove once i reaches four digits. | ||
| N = f"\x1b[{i // 100:03d};{i % 100:02d}m" | ||
| return f"{N}{color}{name}{ANSIColors.RESET}" | ||
|
|
||
| def _color_by_type(self, t): | ||
| typename = t.__name__ | ||
| # this is needed e.g. to turn method-wrapper into method_wrapper, | ||
| # because if we want _colorize.FancyCompleter to be "dataclassable" | ||
| # our keys need to be valid identifiers. | ||
| typename = typename.replace('-', '_').replace('.', '_') | ||
| return getattr(self.theme.fancycompleter, typename, ANSIColors.RESET) | ||
|
|
||
|
|
||
| def commonprefix(names): | ||
| """Return the common prefix of all 'names'""" | ||
| if not names: | ||
| return '' | ||
| s1 = min(names) | ||
| s2 = max(names) | ||
| for i, c in enumerate(s1): | ||
| if c != s2[i]: | ||
| return s1[:i] | ||
| return s1 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.