mirror of
https://github.com/python/cpython.git
synced 2026-04-17 01:10:46 +00:00
PyREPL was still carrying over two readline-specific tricks from the fancy completer: a synthetic CSI prefix to influence sorting and a fake blank completion entry to suppress readline's prefix insertion. Those workarounds are not appropriate in PyREPL because the reader already owns completion ordering and menu rendering, so the fake entries leaked into the UI as real terminal attributes and empty menu cells. Sort completion candidates in ReadlineAlikeReader by their visible text with stripcolor(), and let the fancy completer return only real matches. That keeps colored completions stable without emitting bogus escape sequences, removes the empty completion slot, and adds regression tests for both the low-level completer output and the reader integration.
201 lines
6.8 KiB
Python
201 lines
6.8 KiB
Python
# 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
|
|
|
|
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)
|
|
):
|
|
# 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):
|
|
return [
|
|
self._color_for_obj(name, obj)
|
|
for name, obj in zip(names, values)
|
|
]
|
|
|
|
def _color_for_obj(self, name, value):
|
|
t = type(value)
|
|
color = self._color_by_type(t)
|
|
return f"{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
|