Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
4eb226b
import fancycompleter from https://github.com/pdbpp/fancycompleter/co…
antocuni Feb 18, 2025
c5dfc85
add copyright notice
antocuni Feb 18, 2025
b561a2e
enable FancyCompleter by default, unless you set PYTHON_BASIC_COMPLETER
antocuni Feb 18, 2025
5f56673
WIP: kill a lot of code which is no longer necessary
antocuni Feb 18, 2025
40563f2
force colors for now
antocuni Feb 18, 2025
88181da
kill the logic to find a readline, we can always use _pyrepl.readline…
antocuni Feb 18, 2025
77c578a
kill LazyVersion
antocuni Feb 18, 2025
9069419
we surely don't need to support python 2.7 now :)
antocuni Feb 18, 2025
bdbe022
kill ConfigurableClass
antocuni Feb 18, 2025
0ea7c49
better name
antocuni Feb 18, 2025
6ddfa61
use _colorize instead of our own Color
antocuni Feb 18, 2025
30abd71
WIP: copy&adapt some tests from the original fancycompleter. They don…
antocuni Feb 18, 2025
983824a
edited by copilot: move from pytest-style to unittest-style
antocuni Feb 18, 2025
acbafe4
don't try to be too clever with exceptions: if a global name raises a…
antocuni Feb 18, 2025
5546b94
no longer needed
antocuni Feb 18, 2025
327648f
this doesn't test anything meaningful
antocuni Feb 18, 2025
44549b9
fix this test
antocuni Feb 18, 2025
c9c97f7
fix this test
antocuni Feb 18, 2025
c61bab6
Fix this test
antocuni Feb 18, 2025
1de48b6
Apply hugovk suggestions from code review
antocuni Sep 19, 2025
157f4b4
Apply suggestions from code review
antocuni Sep 19, 2025
722ec8a
Apply suggestions from code review
antocuni Sep 19, 2025
4554a9d
Merge branch 'main' into antocuni/fancycompleter
antocuni Sep 19, 2025
e2294ec
remove unneeded lazy import
antocuni Sep 19, 2025
4532850
Update Lib/_pyrepl/fancycompleter.py
antocuni Sep 19, 2025
1c3dd97
Merge branch 'antocuni/fancycompleter' of github.com:antocuni/cpython…
antocuni Sep 19, 2025
7089323
move import to module scope
antocuni Sep 19, 2025
926c1a3
move import
antocuni Sep 19, 2025
b6385e9
Merge branch 'main' into antocuni/fancycompleter
antocuni Sep 19, 2025
46db5d7
kill this for now, we can redintroduce it later if/when we enable fan…
antocuni Sep 19, 2025
c3ea737
this link is dead, add a comment to explain what it does instead
antocuni Sep 19, 2025
433ae06
fix precommit
antocuni Sep 19, 2025
38e8a08
we need to make this import lazy, else we get circular imports
antocuni Sep 19, 2025
4c3ad9c
now that we have themes, we can kill the config object
antocuni Sep 19, 2025
053da64
style
antocuni Sep 19, 2025
2ee47bc
📜🤖 Added by blurb_it.
blurb-it[bot] Sep 19, 2025
c3c663e
fix mypy
antocuni Sep 19, 2025
b710bce
document PYTHON_BASIC_COMPLETER
antocuni Sep 19, 2025
13a2698
try to manually fix the filename
antocuni Sep 19, 2025
72f30d9
Typo
antocuni Sep 19, 2025
b1f86ae
reword
antocuni Sep 19, 2025
af1d74e
force PYTHON_COLORS=1 for tests which expects to see colors. Hopefull…
antocuni Sep 19, 2025
c52fd7a
fix it in a different way: just look in the theme to find the expecte…
antocuni Sep 19, 2025
fdca77e
fix precommit
antocuni Sep 19, 2025
7f9d09c
Update Lib/_pyrepl/fancycompleter.py
antocuni Sep 21, 2025
3a6bcd3
put _colorize.FancyCompleter in alphabetical order w.r.t. the other s…
antocuni Sep 22, 2025
743e661
get_theme() is relatively expensive, fetch it early and cache it
antocuni Sep 26, 2025
2ebf50e
base is never used when calling commonprefix, remove it
antocuni Sep 26, 2025
bdf54ed
Update Lib/_pyrepl/fancycompleter.py
antocuni Sep 26, 2025
6a5bcfe
there is no need to sort words in advance, we can just sort names later
antocuni Sep 27, 2025
7d2c790
undo 6a5bcfe9ed: there IS actually a good reason to sort the words in…
antocuni Sep 27, 2025
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
4 changes: 4 additions & 0 deletions Lib/_colorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@

class ANSIColors:
BACKGROUND_YELLOW = "\x1b[43m"
BOLD_BLUE = "\x1b[1;34m"
BOLD_GREEN = "\x1b[1;32m"
BOLD_MAGENTA = "\x1b[1;35m"
BOLD_RED = "\x1b[1;31m"
BOLD_TEAL = "\x1b[1;36m"
BOLD_YELLOW = "\x1b[1;33m"
BLACK = "\x1b[30m"
GREEN = "\x1b[32m"
GREY = "\x1b[90m"
MAGENTA = "\x1b[35m"
RED = "\x1b[31m"
RESET = "\x1b[0m"
TEAL = "\x1b[36m"
YELLOW = "\x1b[33m"


Expand Down
205 changes: 205 additions & 0 deletions Lib/_pyrepl/fancycompleter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# Copyright 2010-2025 Antonio Cuni
# Daniel Hahler
#
# All Rights Reserved
Comment on lines +1 to +4
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this have a licence grant (e.g. BSD-3-Clause)? If so, we should add it here and to the included software licences section.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option, which might be slightly nicer, is for the contributors of the relevant bits of fancycompleter to sign the CLA and have this work relicenced under the PSF licence. This seems to include @blueyed, @theY4Kman, @singingwolfboy, and @slinkp.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to do so

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only made this one obsolete commit to fancycompleter 13 years ago, but i'll sign if it helps :) How do I do that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not expert in licensing issue. I'm happy to do/sign whatever is needed to land the PR

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLA can be signed here: https://cla.python.org/

"""
Colorful TAB completion for Python prompt
"""
from _pyrepl import readline
from _colorize import ANSIColors
import rlcompleter
import types
from itertools import count


class DefaultConfig:

consider_getitems = True
use_colors = 'auto'

color_by_type = {
types.BuiltinMethodType: ANSIColors.BOLD_TEAL,
types.MethodType: ANSIColors.BOLD_TEAL,
type((42).__add__): ANSIColors.BOLD_TEAL,
type(int.__add__): ANSIColors.BOLD_TEAL,
type(str.replace): ANSIColors.BOLD_TEAL,

types.FunctionType: ANSIColors.BOLD_BLUE,
types.BuiltinFunctionType: ANSIColors.BOLD_BLUE,

type: ANSIColors.BOLD_MAGENTA,

types.ModuleType: ANSIColors.TEAL,
type(None): ANSIColors.GREY,
str: ANSIColors.BOLD_GREEN,
bytes: ANSIColors.BOLD_GREEN,
int: ANSIColors.BOLD_YELLOW,
float: ANSIColors.BOLD_YELLOW,
complex: ANSIColors.BOLD_YELLOW,
bool: ANSIColors.BOLD_YELLOW,
}

def setup(self):
import _colorize
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You already import _colorize at module level, no point in a lazy import

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed by e2294ec

if self.use_colors == 'auto':
colors = _colorize.get_colors()
self.use_colors = colors.RED != ""


class Completer(rlcompleter.Completer):
"""
When doing someting like a.b.<TAB>, display only the attributes of
b instead of the full a.b.attr string.
Optionally, display the various completions in different colors
depending on the type.
"""
def __init__(self, namespace=None, Config=DefaultConfig):
rlcompleter.Completer.__init__(self, namespace)
self.config = Config()
self.config.setup()

# XXX: double check what happens in this case once fancycompleter works
if False and hasattr(readline, '_setup'):
# this is needed to offer pyrepl a better chance to patch
# raw_input. Usually, it does at import time, but is we are under
# pytest with output captured, at import time we don't have a
# terminal and thus the raw_input hook is not installed
readline._setup()

if self.config.use_colors:
readline.parse_and_bind('set dont-escape-ctrl-chars on')
if self.config.consider_getitems:
delims = readline.get_completer_delims()
delims = delims.replace('[', '')
delims = delims.replace(']', '')
readline.set_completer_delims(delims)

def complete(self, text, state):
"""
stolen from:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496812
"""
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:
# this method exists only in Python 2.6+
return word

def global_matches(self, text):
import keyword
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 clean_name in keyword.kwlist:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if clean_name in keyword.kwlist:
if keyword.iskeyword(clean_name):

values.append(None)
else:
try:
values.append(eval(name, self.namespace))
except Exception as exc:
values.append(None)
if self.config.use_colors and names:
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 []
try:
thisobject = eval(expr, self.namespace)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be the following?

Suggested change
thisobject = eval(expr, self.namespace)
thisobject = self.namespace.get(expr)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, it doesn't work in case of e.g. a.b.c.<TAB>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. In general I like to avoid using eval in code (because of safety and performance reasons). Not using eval here would add a few lines of code though. Leaving the decision on this to a core dev

except Exception:
return []

# get the content of the object, except __builtins__
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have to remove __builtins__? If I remove this part the unit tests still pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same answer as above, it's using the same logic as rlcompleter

words = set(dir(thisobject))
words.discard("__builtins__")

if hasattr(thisobject, '__class__'):
words.add('__class__')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remove this line, tests still pass. Why is this needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a valid question, and I admit I don't know the answer.
The code was originally introduced by this PR in fancycompleter, which merged some logic from CPython's rlcompleter.py:
pdbpp/fancycompleter#17

The current rlcompleter.py contains exactly the same lines:

cpython/Lib/rlcompleter.py

Lines 162 to 167 in 7d2c790

words = set(dir(thisobject))
words.discard("__builtins__")
if hasattr(thisobject, '__class__'):
words.add('__class__')
words.update(get_class_members(thisobject.__class__))

So, I think it's a good idea to keep them in sync.

Copy link
Contributor

@eendebakpt eendebakpt Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behaviour in the rlcompleter was added 25 years ago in 4e20de5 and 46bd9a6 (for the __builtins__). I will check later whether the behaviour still makes sense, but changing that would be a different PR.

Can you use the same style as in rlcompleter.py though? E.g. use words.discard.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was using words.discard, but then I committed this suggestion by @AA-Turner
#130473 (comment)

I'm happy with both 🤷‍♂️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can remove the line in fancycompleter and nothing breaks, we may as well do so.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both the words.add('__class__') and words.discard("__builtins__") can be removed in rlcompleter.py without tests breaking (locally for me). The words.add('__class__') seems rather safe to me (if __class__ is an attribute of thisobject, it should be in dir(thisobject)).

Removing words.discard("__builtins__") is also fine with me, but I have no knowledge of the history here. The commit states

Do not expose builtins name as a completion; this is an implementation detail that confuses too many people. Based on discussion in python-dev.

The commit is so old, I could not find the python-dev archives containing the rationale. On modern Python there are a few more dunder names the might be confusing (e.g. in the global namespace we have __spec__, __package__, __build_class__ and a few more), so why would __builtins__ be special. Maybe in the python version back then __builtins__ was more prominent? Again, it is so long ago I cannot install a python version from that time on my system.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: if we decide to remove these, I would do it in a separate PR.

words.update(rlcompleter.get_class_members(thisobject.__class__))
names = []
values = []
n = len(attr)
if attr == '':
noprefix = '_'
elif attr == '_':
noprefix = '__'
else:
noprefix = None
words = sorted(words)
while True:
for word in words:
if (word[:n] == attr and
not (noprefix and word[:n+1] == noprefix)):
try:
val = getattr(thisobject, word)
except Exception:
val = None # Include even if attribute not set

names.append(word)
values.append(val)
if names or not noprefix:
break
if noprefix == '_':
noprefix = '__'
else:
noprefix = None

if not names:
return []

if len(names) == 1:
return ['%s.%s' % (expr, names[0])] # only option, no coloring.

prefix = commonprefix(names)
if prefix and prefix != attr:
return ['%s.%s' % (expr, prefix)] # autocomplete prefix

if self.config.use_colors:
return self.colorize_matches(names, values)

if prefix:
names += [' ']
return names

def colorize_matches(self, names, values):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much time does this add to the autocompletion? I did a quick test with

>>> import sys
>>> sys.[TAB]

Then 86 names are processed with about 10 ms spend in colorize_matches alone. For modules or classes with more attributes the time would be longer. The processing does not seem needed since the response of the autocompletion is [ not unique].

Are there any benchmarks for the full autocompletion or rules-of-thumb how long it should take?

(maybe these questions are better suited for a separate issue or followup PR)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any benchmarks for the full autocompletion or rules-of-thumb how long it should take?

I don't think there are any but I think that as long as is not slow enough even if the module is too big I don't think it matters. We can always also deactivate if there are too many elements in the module

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then 86 names are processed with about 10 ms spend in colorize_matches alone.

this seems way too much.
I ran this super simple benchmarks on my machine:

import sys
import time
from _pyrepl.fancycompleter import Completer

def gendict(n):
    return {
        f'item{i}': i
        for i in range(n)
    }

class MyClass:
    pass

def main():
    obj = MyClass()
    comp_col = Completer({'obj': obj}, use_colors=True)
    comp_nocol = Completer({'obj': obj}, use_colors=False)

    for i in range(0, 10000, 1000):
        obj.__dict__ = gendict(i)
        a = time.perf_counter()
        comp_col.attr_matches('obj.')
        b = time.perf_counter()
        comp_nocol.attr_matches('obj.')
        c = time.perf_counter()

        t_col = b - a
        t_nocol = c - b
        diff_pct = (t_col - t_nocol) / t_nocol * 100
        print(f"{i:5d}: col={t_col*1000:.1f}ms  nocol={t_nocol*1000:.1f}ms  diff={diff_pct:+.2f}%")

main()

and I got this results:


❯ ./python bench_fancycompleter.py 
    0: col=0.1ms  nocol=0.1ms  diff=+18.30%
 1000: col=0.7ms  nocol=0.7ms  diff=+5.81%
 2000: col=1.6ms  nocol=1.4ms  diff=+13.75%
 3000: col=2.4ms  nocol=2.0ms  diff=+21.95%
 4000: col=2.9ms  nocol=2.4ms  diff=+19.63%
 5000: col=3.8ms  nocol=5.0ms  diff=-23.60%
 6000: col=5.3ms  nocol=4.5ms  diff=+15.84%
 7000: col=5.0ms  nocol=4.6ms  diff=+9.01%
 8000: col=6.9ms  nocol=7.6ms  diff=-9.75%
 9000: col=7.0ms  nocol=6.9ms  diff=+1.47%

so, on my machine it spends 7ms to process 9000 names, and the difference between colors and no colors is basically noise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example minimal example above never reaches colorize_matches because there is a common prefix.
If I replace comp_col.attr_matches('obj.') with comp_col.attr_matches('obj.item') I get this output

    0: col=0.1ms  nocol=0.0ms  diff=+95.88%
 2000: col=85.6ms  nocol=1.0ms  diff=+8489.94%
 4000: col=166.8ms  nocol=1.9ms  diff=+8763.17%
 6000: col=286.6ms  nocol=3.6ms  diff=+7940.23%
 8000: col=330.9ms  nocol=4.5ms  diff=+7297.60%
10000: col=429.9ms  nocol=5.1ms  diff=+8394.79%
12000: col=509.7ms  nocol=6.1ms  diff=+8308.30%
14000: col=632.8ms  nocol=9.4ms  diff=+6627.23%
16000: col=1308.4ms  nocol=29.5ms  diff=+4331.26%
18000: col=2131.1ms  nocol=35.8ms  diff=+5855.55%

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh right, sorry for the silly mistake. Indeed, this is what I get on my machine:

❯ ./python bench_fancycompleter.py 
    0: col=0.1ms  nocol=0.1ms  diff=+16.43%
 1000: col=9.6ms  nocol=1.0ms  diff=+826.21%
 2000: col=17.8ms  nocol=1.3ms  diff=+1293.67%
 3000: col=26.6ms  nocol=1.8ms  diff=+1377.77%
 4000: col=33.1ms  nocol=2.3ms  diff=+1335.15%
 5000: col=40.8ms  nocol=3.7ms  diff=+996.66%
 6000: col=54.1ms  nocol=3.7ms  diff=+1368.12%
 7000: col=56.0ms  nocol=4.1ms  diff=+1253.30%
 8000: col=63.4ms  nocol=6.7ms  diff=+851.07%
 9000: col=71.9ms  nocol=6.0ms  diff=+1095.03%

I'm not an UI expert at all, but I think that anything <100ms is perceived as "instantaneous" by the user, so unless we have modules with >9000 we should be safe 😅.

Jokes apart, I see that on your machine things are slower, but I still think that with "normal" number of completions the delay won't be noticeable (and, anecdotally, I never noticed in all these years).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anecdotally, the timings are from my fast machine, and I have noticed the new pyrepl being slower (not specifically this PR though). With the commit moving the gettheme() out of the loop performance is much better:

    0: col=0.1ms  nocol=0.0ms  diff=+114.87%
 1000: col=1.8ms  nocol=0.5ms  diff=+257.26%
 2000: col=3.2ms  nocol=1.1ms  diff=+187.30%
 3000: col=3.9ms  nocol=1.4ms  diff=+182.85%
 4000: col=4.9ms  nocol=2.0ms  diff=+143.63%
 5000: col=6.8ms  nocol=2.9ms  diff=+137.13%
 6000: col=8.0ms  nocol=3.2ms  diff=+152.49%
 7000: col=10.0ms  nocol=3.5ms  diff=+185.24%
 8000: col=10.9ms  nocol=4.3ms  diff=+153.73%
 9000: col=12.1ms  nocol=4.8ms  diff=+149.98%

So I am happy with the current performance of the PR!

matches = [self.color_for_obj(i, name, obj)
for i, name, obj
in zip(count(), names, values)]
# We add a space at the end to prevent the automatic completion of the
# common prefix, which is the ANSI ESCAPE sequence.
return matches + [' ']

def color_for_obj(self, i, name, value):
t = type(value)
color = self.config.color_by_type.get(t, ANSIColors.RESET)
# hack: prepend an (increasing) fake escape sequence,
# so that readline can sort the matches correctly.
N = f"\x1b[{i:03d};00m"
return f"{N}{color}{name}{ANSIColors.RESET}"


def commonprefix(names, base=''):
""" return the common prefix of all 'names' starting with 'base'
"""
if base:
names = [x for x in names if x.startswith(base)]
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 @@ -35,6 +35,7 @@
from site import gethistoryfile # type: ignore[attr-defined]
import sys
from rlcompleter import Completer as RLCompleter
from .fancycompleter import Completer as FancyCompleter

from . import commands, historical_reader
from .completing_reader import CompletingReader
Expand Down Expand Up @@ -587,7 +588,12 @@ def _setup(namespace: Mapping[str, Any]) -> None:
# set up namespace in rlcompleter, which requires it to be a bona fide dict
if not isinstance(namespace, dict):
namespace = dict(namespace)
_wrapper.config.readline_completer = RLCompleter(namespace).complete

if os.getenv('PYTHON_BASIC_COMPLETER'):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this variable be documented?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, totally! I think it's worth discussing what is the best way to disable fancycompleter, if anybody has other ideas I'm happy to hear

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

documentation added in b710bce

Completer = RLCompleter
else:
Completer = FancyCompleter
_wrapper.config.readline_completer = Completer(namespace).complete

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