Skip to content
Merged
Show file tree
Hide file tree
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 Feb 18, 2025
e22a210
add copyright notice
antocuni Feb 18, 2025
374eff9
enable FancyCompleter by default, unless you set PYTHON_BASIC_COMPLETER
antocuni Feb 18, 2025
9eeca3a
WIP: kill a lot of code which is no longer necessary
antocuni Feb 18, 2025
1c5d27d
force colors for now
antocuni Feb 18, 2025
c621cf5
kill the logic to find a readline, we can always use _pyrepl.readline…
antocuni Feb 18, 2025
d839aa1
kill LazyVersion
antocuni Feb 18, 2025
f89b9ef
we surely don't need to support python 2.7 now :)
antocuni Feb 18, 2025
04b5fee
kill ConfigurableClass
antocuni Feb 18, 2025
d733675
better name
antocuni Feb 18, 2025
116a634
use _colorize instead of our own Color
antocuni Feb 18, 2025
98d3f86
WIP: copy&adapt some tests from the original fancycompleter. They don…
antocuni Feb 18, 2025
9d40f1b
edited by copilot: move from pytest-style to unittest-style
antocuni Feb 18, 2025
d04f41d
don't try to be too clever with exceptions: if a global name raises a…
antocuni Feb 18, 2025
15ea5de
no longer needed
antocuni Feb 18, 2025
0063a70
this doesn't test anything meaningful
antocuni Feb 18, 2025
0af3c84
fix this test
antocuni Feb 18, 2025
d9a9f6f
fix this test
antocuni Feb 18, 2025
0c26812
Fix this test
antocuni Feb 18, 2025
30468ac
Apply hugovk suggestions from code review
antocuni Sep 19, 2025
4700d1a
Apply suggestions from code review
antocuni Sep 19, 2025
26591ae
Apply suggestions from code review
antocuni Sep 19, 2025
b2414f8
remove unneeded lazy import
antocuni Sep 19, 2025
24e4afb
Update Lib/_pyrepl/fancycompleter.py
antocuni Sep 19, 2025
b5935bc
move import to module scope
antocuni Sep 19, 2025
0fbdabc
move import
antocuni Sep 19, 2025
06840b0
kill this for now, we can redintroduce it later if/when we enable fan…
antocuni Sep 19, 2025
56384ee
this link is dead, add a comment to explain what it does instead
antocuni Sep 19, 2025
49f90f5
fix precommit
antocuni Sep 19, 2025
3c441e1
we need to make this import lazy, else we get circular imports
antocuni Sep 19, 2025
aabf91c
now that we have themes, we can kill the config object
antocuni Sep 19, 2025
233c51d
style
antocuni Sep 19, 2025
1a86caf
📜🤖 Added by blurb_it.
blurb-it[bot] Sep 19, 2025
ee882e7
fix mypy
antocuni Sep 19, 2025
850d74b
document PYTHON_BASIC_COMPLETER
antocuni Sep 19, 2025
7c603a2
try to manually fix the filename
antocuni Sep 19, 2025
cf6bf1e
Typo
antocuni Sep 19, 2025
0dc7d16
reword
antocuni Sep 19, 2025
49eda06
force PYTHON_COLORS=1 for tests which expects to see colors. Hopefull…
antocuni Sep 19, 2025
3ce8d00
fix it in a different way: just look in the theme to find the expecte…
antocuni Sep 19, 2025
c71eed6
fix precommit
antocuni Sep 19, 2025
d6b77e0
Update Lib/_pyrepl/fancycompleter.py
antocuni Sep 21, 2025
f919815
put _colorize.FancyCompleter in alphabetical order w.r.t. the other s…
antocuni Sep 22, 2025
97c684e
get_theme() is relatively expensive, fetch it early and cache it
antocuni Sep 26, 2025
588c204
base is never used when calling commonprefix, remove it
antocuni Sep 26, 2025
0646abb
Update Lib/_pyrepl/fancycompleter.py
antocuni Sep 26, 2025
4b11774
there is no need to sort words in advance, we can just sort names later
antocuni Sep 27, 2025
9b9b37d
undo 6a5bcfe9ed: there IS actually a good reason to sort the words in…
antocuni Sep 27, 2025
cba06ba
gh-130472: Fix pyrepl fancycompleter edge cases
pablogsal Apr 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,13 @@ conflict.

.. versionadded:: 3.13

.. envvar:: PYTHON_BASIC_COMPLETER

If this variable is set to any value, PyREPL will use :mod:`rlcompleter` to
implement tab completion, instead of the default one which uses colors.

.. versionadded:: 3.15

.. envvar:: PYTHON_HISTORY

This environment variable can be used to set the location of a
Expand Down
29 changes: 28 additions & 1 deletion Lib/_colorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

class ANSIColors:
RESET = "\x1b[0m"

BLACK = "\x1b[30m"
BLUE = "\x1b[34m"
CYAN = "\x1b[36m"
Expand Down Expand Up @@ -200,6 +199,30 @@ class Difflib(ThemeSection):
reset: str = ANSIColors.RESET


@dataclass(frozen=True, kw_only=True)
class FancyCompleter(ThemeSection):
# functions and methods
function: str = ANSIColors.BOLD_BLUE
builtin_function_or_method: str = ANSIColors.BOLD_BLUE
method: str = ANSIColors.BOLD_CYAN
method_wrapper: str = ANSIColors.BOLD_CYAN
wrapper_descriptor: str = ANSIColors.BOLD_CYAN
method_descriptor: str = ANSIColors.BOLD_CYAN

# numbers
int: str = ANSIColors.BOLD_YELLOW
float: str = ANSIColors.BOLD_YELLOW
complex: str = ANSIColors.BOLD_YELLOW
bool: str = ANSIColors.BOLD_YELLOW

# others
type: str = ANSIColors.BOLD_MAGENTA
module: str = ANSIColors.CYAN
NoneType: str = ANSIColors.GREY
bytes: str = ANSIColors.BOLD_GREEN
str: str = ANSIColors.BOLD_GREEN


@dataclass(frozen=True, kw_only=True)
class LiveProfiler(ThemeSection):
"""Theme section for the live profiling TUI (Tachyon profiler).
Expand Down Expand Up @@ -354,6 +377,7 @@ class Theme:
"""
argparse: Argparse = field(default_factory=Argparse)
difflib: Difflib = field(default_factory=Difflib)
fancycompleter: FancyCompleter = field(default_factory=FancyCompleter)
live_profiler: LiveProfiler = field(default_factory=LiveProfiler)
syntax: Syntax = field(default_factory=Syntax)
traceback: Traceback = field(default_factory=Traceback)
Expand All @@ -364,6 +388,7 @@ def copy_with(
*,
argparse: Argparse | None = None,
difflib: Difflib | None = None,
fancycompleter: FancyCompleter | None = None,
live_profiler: LiveProfiler | None = None,
syntax: Syntax | None = None,
traceback: Traceback | None = None,
Expand All @@ -377,6 +402,7 @@ def copy_with(
return type(self)(
argparse=argparse or self.argparse,
difflib=difflib or self.difflib,
fancycompleter=fancycompleter or self.fancycompleter,
live_profiler=live_profiler or self.live_profiler,
syntax=syntax or self.syntax,
traceback=traceback or self.traceback,
Expand All @@ -394,6 +420,7 @@ def no_colors(cls) -> Self:
return cls(
argparse=Argparse.no_colors(),
difflib=Difflib.no_colors(),
fancycompleter=FancyCompleter.no_colors(),
live_profiler=LiveProfiler.no_colors(),
syntax=Syntax.no_colors(),
traceback=Traceback.no_colors(),
Expand Down
12 changes: 7 additions & 5 deletions Lib/_pyrepl/completing_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,14 @@ def do(self) -> None:
if not completions:
r.error("no matches")
elif len(completions) == 1:
if completions_unchangable and len(completions[0]) == len(stem):
completion = stripcolor(completions[0])
if completions_unchangable and len(completion) == len(stem):
r.msg = "[ sole completion ]"
r.dirty = True
r.insert(completions[0][len(stem):])
r.insert(completion[len(stem):])
else:
p = prefix(completions, len(stem))
clean_completions = [stripcolor(word) for word in completions]
p = prefix(clean_completions, len(stem))
if p:
r.insert(p)
if last_is_completer:
Expand All @@ -195,7 +197,7 @@ def do(self) -> None:
r.dirty = True
elif not r.cmpltn_menu_visible:
r.cmpltn_message_visible = True
if stem + p in completions:
if stem + p in clean_completions:
r.msg = "[ complete but not unique ]"
r.dirty = True
else:
Expand All @@ -215,7 +217,7 @@ def do(self) -> None:
r.cmpltn_reset()
else:
completions = [w for w in r.cmpltn_menu_choices
if w.startswith(stem)]
if stripcolor(w).startswith(stem)]
if completions:
r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
r.console, completions, 0,
Expand Down
210 changes: 210 additions & 0 deletions Lib/_pyrepl/fancycompleter.py
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

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
8 changes: 7 additions & 1 deletion Lib/_pyrepl/readline.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from .completing_reader import CompletingReader
from .console import Console as ConsoleType
from ._module_completer import ModuleCompleter, make_default_module_completer
from .fancycompleter import Completer as FancyCompleter

Console: type[ConsoleType]
_error: tuple[type[Exception], ...] | type[Exception]
Expand Down Expand Up @@ -609,7 +610,12 @@ def _setup(namespace: Mapping[str, Any]) -> None:
if not isinstance(namespace, dict):
namespace = dict(namespace)
_wrapper.config.module_completer = ModuleCompleter(namespace)
_wrapper.config.readline_completer = RLCompleter(namespace).complete
use_basic_completer = (
not sys.flags.ignore_environment
and os.getenv("PYTHON_BASIC_COMPLETER")
)
completer_cls = RLCompleter if use_basic_completer else FancyCompleter
_wrapper.config.readline_completer = completer_cls(namespace).complete

# this is not really what readline.c does. Better than nothing I guess
import builtins
Expand Down
Loading
Loading