Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 35.2k
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
d7eff96e22a210374eff99eeca3a1c5d27dc621cf5d839aa1f89b9ef04b5feed733675116a63498d3f869d40f1bd04f41d15ea5de0063a700af3c84d9a9f6f0c2681230468ac4700d1a26591aeb2414f824e4afbb5935bc0fbdabc06840b056384ee49f90f53c441e1aabf91c233c51d1a86cafee882e7850d74b7c603a2cf6bf1e0dc7d1649eda063ce8d00c71eed6d6b77e0f91981597c684e588c2040646abb4b117749b9b37dcba06baFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| 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.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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.
Uh oh!There was an error while loading. Please reload this page. | ||
| except Exception: | ||
| return expr, attr, [], [] | ||
| # get the content of the object, except __builtins__ | ||
pablogsal marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| words = set(dir(thisobject)) - {'__builtins__'} | ||
| if hasattr(thisobject, '__class__'): | ||
| words.add('__class__') | ||
pablogsal marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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" | ||
Comment on lines
+187
to
+189
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's open an issue and fix this in a separate PR | ||
| 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 | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.

Uh oh!
There was an error while loading. Please reload this page.