mirror of
https://github.com/python/cpython.git
synced 2026-04-15 00:00:57 +00:00
210 lines
7.3 KiB
Python
210 lines
7.3 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)
|
|
|
|
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)
|
|
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):
|
|
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
|