| # 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 | |
| lazy from typing import Any | |
| lazy from _colorize import Theme | |
| def safe_getattr(obj: object, name: str) -> object: | |
| # Mirror rlcompleter's safeguards so completion does not | |
| # call properties or reify lazy module attributes. | |
| if isinstance(getattr(type(obj), name, None), property): | |
| return None | |
| if (isinstance(obj, types.ModuleType) | |
| and isinstance(obj.__dict__.get(name), types.LazyImportType) | |
| ): | |
| return obj.__dict__.get(name) | |
| return getattr(obj, name, None) | |
| def colorize_matches(names: list[str], values: list[Any], theme: Theme) -> list[str]: | |
| return [ | |
| _color_for_obj(name, obj, theme) | |
| for name, obj in zip(names, values) | |
| ] | |
| def _color_for_obj(name: str, value: object, theme: Theme) -> str: | |
| t = type(value) | |
| color = _color_by_type(t, theme) | |
| return f"{color}{name}{ANSIColors.RESET}" | |
| def _color_by_type(t, theme): | |
| 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(theme.fancycompleter, typename, ANSIColors.RESET) | |
| 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 | |
| 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) | |
| 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) | |
| except Exception: | |
| return expr, attr, [], [] | |
| # get the content of the object, except __builtins__ | |
| words = set(dir(thisobject)) - {'__builtins__'} | |
| if hasattr(thisobject, '__class__'): | |
| words.add('__class__') | |
| 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) | |
| ): | |
| value = safe_getattr(thisobject, word) | |
| 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): | |
| return colorize_matches(names, values, self.theme) | |
| 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 |