From 4eb226bfead0f39a3702b677b327d1564a99098a Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 16:34:07 +0100 Subject: [PATCH 01/48] import fancycompleter from https://github.com/pdbpp/fancycompleter/commit/67e3ec128cf8d44be6e48e775234c07f4b23064e --- Lib/_pyrepl/fancycompleter.py | 545 ++++++++++++++++++++++++++++++++++ 1 file changed, 545 insertions(+) create mode 100644 Lib/_pyrepl/fancycompleter.py diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py new file mode 100644 index 00000000000000..f6319ca23d9674 --- /dev/null +++ b/Lib/_pyrepl/fancycompleter.py @@ -0,0 +1,545 @@ +""" +fancycompleter: colorful TAB completion for Python prompt +""" +from __future__ import with_statement +from __future__ import print_function + +import rlcompleter +import sys +import types +import os.path +from itertools import count + +PY3K = sys.version_info[0] >= 3 + +# python3 compatibility +# --------------------- +try: + from itertools import izip +except ImportError: + izip = zip + +try: + from types import ClassType +except ImportError: + ClassType = type + +try: + unicode +except NameError: + unicode = str + +# ---------------------- + + +class LazyVersion(object): + + def __init__(self, pkg): + self.pkg = pkg + self.__version = None + + @property + def version(self): + if self.__version is None: + self.__version = self._load_version() + return self.__version + + def _load_version(self): + try: + from pkg_resources import get_distribution, DistributionNotFound + except ImportError: + return 'N/A' + # + try: + return get_distribution(self.pkg).version + except DistributionNotFound: + # package is not installed + return 'N/A' + + def __repr__(self): + return self.version + + def __eq__(self, other): + return self.version == other + + def __ne__(self, other): + return not self == other + + +__version__ = LazyVersion(__name__) + +# ---------------------- + + +class Color: + black = '30' + darkred = '31' + darkgreen = '32' + brown = '33' + darkblue = '34' + purple = '35' + teal = '36' + lightgray = '37' + darkgray = '30;01' + red = '31;01' + green = '32;01' + yellow = '33;01' + blue = '34;01' + fuchsia = '35;01' + turquoise = '36;01' + white = '37;01' + + @classmethod + def set(cls, color, string): + try: + color = getattr(cls, color) + except AttributeError: + pass + return '\x1b[%sm%s\x1b[00m' % (color, string) + + +class DefaultConfig: + + consider_getitems = True + prefer_pyrepl = True + use_colors = 'auto' + readline = None # set by setup() + using_pyrepl = False # overwritten by find_pyrepl + + color_by_type = { + types.BuiltinMethodType: Color.turquoise, + types.MethodType: Color.turquoise, + type((42).__add__): Color.turquoise, + type(int.__add__): Color.turquoise, + type(str.replace): Color.turquoise, + + types.FunctionType: Color.blue, + types.BuiltinFunctionType: Color.blue, + + ClassType: Color.fuchsia, + type: Color.fuchsia, + + types.ModuleType: Color.teal, + type(None): Color.lightgray, + str: Color.green, + unicode: Color.green, + int: Color.yellow, + float: Color.yellow, + complex: Color.yellow, + bool: Color.yellow, + } + # Fallback to look up colors by `isinstance` when not matched + # via color_by_type. + color_by_baseclass = [ + ((BaseException,), Color.red), + ] + + def find_pyrepl(self): + try: + import pyrepl.readline + import pyrepl.completing_reader + except ImportError: + return None + self.using_pyrepl = True + if hasattr(pyrepl.completing_reader, 'stripcolor'): + # modern version of pyrepl + return pyrepl.readline, True + else: + return pyrepl.readline, False + + def find_pyreadline(self): + try: + import readline + import pyreadline # noqa: F401 # XXX: needed really? + from pyreadline.modes import basemode + except ImportError: + return None + if hasattr(basemode, 'stripcolor'): + # modern version of pyreadline; see: + # https://github.com/pyreadline/pyreadline/pull/48 + return readline, True + else: + return readline, False + + def find_best_readline(self): + if self.prefer_pyrepl: + result = self.find_pyrepl() + if result: + return result + if sys.platform == 'win32': + result = self.find_pyreadline() + if result: + return result + import readline + return readline, False # by default readline does not support colors + + def setup(self): + self.readline, supports_color = self.find_best_readline() + if self.use_colors == 'auto': + self.use_colors = supports_color + + +def my_execfile(filename, mydict): + with open(filename) as f: + code = compile(f.read(), filename, 'exec') + exec(code, mydict) + + +class ConfigurableClass: + DefaultConfig = None + config_filename = None + + def get_config(self, Config): + if Config is not None: + return Config() + # try to load config from the ~/filename file + filename = '~/' + self.config_filename + rcfile = os.path.normpath(os.path.expanduser(filename)) + if not os.path.exists(rcfile): + return self.DefaultConfig() + + mydict = {} + try: + my_execfile(rcfile, mydict) + except Exception as exc: + import traceback + + sys.stderr.write("** error when importing %s: %r **\n" % (filename, exc)) + traceback.print_tb(sys.exc_info()[2]) + return self.DefaultConfig() + + try: + Config = mydict["Config"] + except KeyError: + return self.DefaultConfig() + + try: + return Config() + except Exception as exc: + err = "error when setting up Config from %s: %s" % (filename, exc) + tb = sys.exc_info()[2] + if tb and tb.tb_next: + tb = tb.tb_next + err_fname = tb.tb_frame.f_code.co_filename + err_lnum = tb.tb_lineno + err += " (%s:%d)" % (err_fname, err_lnum,) + sys.stderr.write("** %s **\n" % err) + return self.DefaultConfig() + + +class Completer(rlcompleter.Completer, ConfigurableClass): + """ + When doing someting like a.b., 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. + """ + + DefaultConfig = DefaultConfig + config_filename = '.fancycompleterrc.py' + + def __init__(self, namespace=None, Config=None): + rlcompleter.Completer.__init__(self, namespace) + self.config = self.get_config(Config) + self.config.setup() + readline = self.config.readline + if 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: + values.append(None) + else: + try: + values.append(eval(name, self.namespace)) + except Exception as exc: + values.append(exc) + if self.config.use_colors and names: + return self.color_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) + except Exception: + return [] + + # get the content of the object, except __builtins__ + words = set(dir(thisobject)) + words.discard("__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 + 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 + + if not PY3K and isinstance(word, unicode): + # this is needed because pyrepl doesn't like unicode + # completions: as soon as it finds something which is not str, + # it stops. + word = word.encode('utf-8') + + 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.color_matches(names, values) + + if prefix: + names += [' '] + return names + + def color_matches(self, names, values): + matches = [self.color_for_obj(i, name, obj) + for i, name, obj + in izip(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, None) + if color is None: + for x, _color in self.config.color_by_baseclass: + if isinstance(value, x): + color = _color + break + else: + color = '00' + # hack: prepend an (increasing) fake escape sequence, + # so that readline can sort the matches correctly. + return '\x1b[%03d;00m' % i + Color.set(color, name) + + +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 + + +def has_leopard_libedit(config): + # Detect if we are using Leopard's libedit. + # Adapted from IPython's rlineimpl.py. + if config.using_pyrepl or sys.platform != 'darwin': + return False + + # Official Python docs state that 'libedit' is in the docstring for + # libedit readline. + return config.readline.__doc__ and 'libedit' in config.readline.__doc__ + + +def setup(): + """ + Install fancycompleter as the default completer for readline. + """ + completer = Completer() + readline = completer.config.readline + if has_leopard_libedit(completer.config): + readline.parse_and_bind("bind ^I rl_complete") + else: + readline.parse_and_bind('tab: complete') + readline.set_completer(completer.complete) + return completer + + +def interact_pyrepl(): + import sys + from pyrepl import readline + from pyrepl.simple_interact import run_multiline_interactive_console + sys.modules['readline'] = readline + run_multiline_interactive_console() + + +def setup_history(completer, persist_history): + import atexit + readline = completer.config.readline + # + if isinstance(persist_history, (str, unicode)): + filename = persist_history + else: + filename = '~/.history.py' + filename = os.path.expanduser(filename) + if os.path.isfile(filename): + readline.read_history_file(filename) + + def save_history(): + readline.write_history_file(filename) + atexit.register(save_history) + + +def interact(persist_history=None): + """ + Main entry point for fancycompleter: run an interactive Python session + after installing fancycompleter. + + This function is supposed to be called at the end of PYTHONSTARTUP: + + - if we are using pyrepl: install fancycompleter, run pyrepl multiline + prompt, and sys.exit(). The standard python prompt will never be + reached + + - if we are not using pyrepl: install fancycompleter and return. The + execution will continue as normal, and the standard python prompt will + be displayed. + + This is necessary because there is no way to tell the standard python + prompt to use the readline provided by pyrepl instead of the builtin one. + + By default, pyrepl is preferred and automatically used if found. + """ + import sys + completer = setup() + if persist_history: + setup_history(completer, persist_history) + if completer.config.using_pyrepl and '__pypy__' not in sys.builtin_module_names: + # if we are on PyPy, we don't need to run a "fake" interpeter, as the + # standard one is fake enough :-) + interact_pyrepl() + sys.exit() + + +class Installer(object): + """ + Helper to install fancycompleter in PYTHONSTARTUP + """ + + def __init__(self, basepath, force): + fname = os.path.join(basepath, 'python_startup.py') + self.filename = os.path.expanduser(fname) + self.force = force + + def check(self): + PYTHONSTARTUP = os.environ.get('PYTHONSTARTUP') + if PYTHONSTARTUP: + return 'PYTHONSTARTUP already defined: %s' % PYTHONSTARTUP + if os.path.exists(self.filename): + return '%s already exists' % self.filename + + def install(self): + import textwrap + error = self.check() + if error and not self.force: + print(error) + print('Use --force to overwrite.') + return False + with open(self.filename, 'w') as f: + f.write(textwrap.dedent(""" + import fancycompleter + fancycompleter.interact(persist_history=True) + """)) + self.set_env_var() + return True + + def set_env_var(self): + if sys.platform == 'win32': + os.system('SETX PYTHONSTARTUP "%s"' % self.filename) + print('%PYTHONSTARTUP% set to', self.filename) + else: + print('startup file written to', self.filename) + print('Append this line to your ~/.bashrc:') + print(' export PYTHONSTARTUP=%s' % self.filename) + + +if __name__ == '__main__': + def usage(): + print('Usage: python -m fancycompleter install [-f|--force]') + sys.exit(1) + + cmd = None + force = False + for item in sys.argv[1:]: + if item in ('install',): + cmd = item + elif item in ('-f', '--force'): + force = True + else: + usage() + # + if cmd == 'install': + installer = Installer('~', force) + installer.install() + else: + usage() From c5dfc851a825dfa550c0ad0bab3776802d33b852 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 16:45:15 +0100 Subject: [PATCH 02/48] add copyright notice --- Lib/_pyrepl/fancycompleter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index f6319ca23d9674..f1da6916ab741c 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -1,5 +1,9 @@ +# Copyright 2010-2025 Antonio Cuni +# Daniel Hahler +# +# All Rights Reserved """ -fancycompleter: colorful TAB completion for Python prompt +Colorful TAB completion for Python prompt """ from __future__ import with_statement from __future__ import print_function From b561a2eace8daf11612a2b41fbfcf012adfc60fb Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 16:55:15 +0100 Subject: [PATCH 03/48] enable FancyCompleter by default, unless you set PYTHON_BASIC_COMPLETER --- Lib/_pyrepl/readline.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 888185eb03be66..bd5a2152b34997 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -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 @@ -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'): + 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 From 5f566737bf76dec4cc6a661fd333761da9b6e1de Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 17:03:54 +0100 Subject: [PATCH 04/48] WIP: kill a lot of code which is no longer necessary --- Lib/_pyrepl/fancycompleter.py | 145 ---------------------------------- 1 file changed, 145 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index f1da6916ab741c..9148da47dfec70 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -402,148 +402,3 @@ def commonprefix(names, base=''): if c != s2[i]: return s1[:i] return s1 - - -def has_leopard_libedit(config): - # Detect if we are using Leopard's libedit. - # Adapted from IPython's rlineimpl.py. - if config.using_pyrepl or sys.platform != 'darwin': - return False - - # Official Python docs state that 'libedit' is in the docstring for - # libedit readline. - return config.readline.__doc__ and 'libedit' in config.readline.__doc__ - - -def setup(): - """ - Install fancycompleter as the default completer for readline. - """ - completer = Completer() - readline = completer.config.readline - if has_leopard_libedit(completer.config): - readline.parse_and_bind("bind ^I rl_complete") - else: - readline.parse_and_bind('tab: complete') - readline.set_completer(completer.complete) - return completer - - -def interact_pyrepl(): - import sys - from pyrepl import readline - from pyrepl.simple_interact import run_multiline_interactive_console - sys.modules['readline'] = readline - run_multiline_interactive_console() - - -def setup_history(completer, persist_history): - import atexit - readline = completer.config.readline - # - if isinstance(persist_history, (str, unicode)): - filename = persist_history - else: - filename = '~/.history.py' - filename = os.path.expanduser(filename) - if os.path.isfile(filename): - readline.read_history_file(filename) - - def save_history(): - readline.write_history_file(filename) - atexit.register(save_history) - - -def interact(persist_history=None): - """ - Main entry point for fancycompleter: run an interactive Python session - after installing fancycompleter. - - This function is supposed to be called at the end of PYTHONSTARTUP: - - - if we are using pyrepl: install fancycompleter, run pyrepl multiline - prompt, and sys.exit(). The standard python prompt will never be - reached - - - if we are not using pyrepl: install fancycompleter and return. The - execution will continue as normal, and the standard python prompt will - be displayed. - - This is necessary because there is no way to tell the standard python - prompt to use the readline provided by pyrepl instead of the builtin one. - - By default, pyrepl is preferred and automatically used if found. - """ - import sys - completer = setup() - if persist_history: - setup_history(completer, persist_history) - if completer.config.using_pyrepl and '__pypy__' not in sys.builtin_module_names: - # if we are on PyPy, we don't need to run a "fake" interpeter, as the - # standard one is fake enough :-) - interact_pyrepl() - sys.exit() - - -class Installer(object): - """ - Helper to install fancycompleter in PYTHONSTARTUP - """ - - def __init__(self, basepath, force): - fname = os.path.join(basepath, 'python_startup.py') - self.filename = os.path.expanduser(fname) - self.force = force - - def check(self): - PYTHONSTARTUP = os.environ.get('PYTHONSTARTUP') - if PYTHONSTARTUP: - return 'PYTHONSTARTUP already defined: %s' % PYTHONSTARTUP - if os.path.exists(self.filename): - return '%s already exists' % self.filename - - def install(self): - import textwrap - error = self.check() - if error and not self.force: - print(error) - print('Use --force to overwrite.') - return False - with open(self.filename, 'w') as f: - f.write(textwrap.dedent(""" - import fancycompleter - fancycompleter.interact(persist_history=True) - """)) - self.set_env_var() - return True - - def set_env_var(self): - if sys.platform == 'win32': - os.system('SETX PYTHONSTARTUP "%s"' % self.filename) - print('%PYTHONSTARTUP% set to', self.filename) - else: - print('startup file written to', self.filename) - print('Append this line to your ~/.bashrc:') - print(' export PYTHONSTARTUP=%s' % self.filename) - - -if __name__ == '__main__': - def usage(): - print('Usage: python -m fancycompleter install [-f|--force]') - sys.exit(1) - - cmd = None - force = False - for item in sys.argv[1:]: - if item in ('install',): - cmd = item - elif item in ('-f', '--force'): - force = True - else: - usage() - # - if cmd == 'install': - installer = Installer('~', force) - installer.install() - else: - usage() From 40563f2c578b4c645461f49c07d376b4a05809aa Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 17:08:26 +0100 Subject: [PATCH 05/48] force colors for now --- Lib/_pyrepl/fancycompleter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 9148da47dfec70..cfb907e8359031 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -180,7 +180,8 @@ def find_best_readline(self): def setup(self): self.readline, supports_color = self.find_best_readline() if self.use_colors == 'auto': - self.use_colors = supports_color + #self.use_colors = supports_color + self.use_colors = True def my_execfile(filename, mydict): From 88181da726dd519ef3e4e1d3aa5a3604367a4ba7 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 17:19:55 +0100 Subject: [PATCH 06/48] kill the logic to find a readline, we can always use _pyrepl.readline now --- Lib/_pyrepl/fancycompleter.py | 56 ++++++----------------------------- 1 file changed, 9 insertions(+), 47 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index cfb907e8359031..e7d869dea7ccc9 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -8,6 +8,7 @@ from __future__ import with_statement from __future__ import print_function +from _pyrepl import readline import rlcompleter import sys import types @@ -105,10 +106,7 @@ def set(cls, color, string): class DefaultConfig: consider_getitems = True - prefer_pyrepl = True use_colors = 'auto' - readline = None # set by setup() - using_pyrepl = False # overwritten by find_pyrepl color_by_type = { types.BuiltinMethodType: Color.turquoise, @@ -138,50 +136,12 @@ class DefaultConfig: ((BaseException,), Color.red), ] - def find_pyrepl(self): - try: - import pyrepl.readline - import pyrepl.completing_reader - except ImportError: - return None - self.using_pyrepl = True - if hasattr(pyrepl.completing_reader, 'stripcolor'): - # modern version of pyrepl - return pyrepl.readline, True - else: - return pyrepl.readline, False - - def find_pyreadline(self): - try: - import readline - import pyreadline # noqa: F401 # XXX: needed really? - from pyreadline.modes import basemode - except ImportError: - return None - if hasattr(basemode, 'stripcolor'): - # modern version of pyreadline; see: - # https://github.com/pyreadline/pyreadline/pull/48 - return readline, True - else: - return readline, False - - def find_best_readline(self): - if self.prefer_pyrepl: - result = self.find_pyrepl() - if result: - return result - if sys.platform == 'win32': - result = self.find_pyreadline() - if result: - return result - import readline - return readline, False # by default readline does not support colors def setup(self): - self.readline, supports_color = self.find_best_readline() + import _colorize if self.use_colors == 'auto': - #self.use_colors = supports_color - self.use_colors = True + colors = _colorize.get_colors() + self.use_colors = colors.RED != "" def my_execfile(filename, mydict): @@ -242,19 +202,21 @@ class Completer(rlcompleter.Completer, ConfigurableClass): """ DefaultConfig = DefaultConfig - config_filename = '.fancycompleterrc.py' + config_filename = '.fancycompleterrc.py.xxx' def __init__(self, namespace=None, Config=None): rlcompleter.Completer.__init__(self, namespace) self.config = self.get_config(Config) self.config.setup() - readline = self.config.readline - if hasattr(readline, '_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: From 77c578af5cb3f1084d9a18b350f7d56bd0aa0188 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 17:22:36 +0100 Subject: [PATCH 07/48] kill LazyVersion --- Lib/_pyrepl/fancycompleter.py | 40 ----------------------------------- 1 file changed, 40 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index e7d869dea7ccc9..6769a0bbeb0ae7 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -36,46 +36,6 @@ # ---------------------- - -class LazyVersion(object): - - def __init__(self, pkg): - self.pkg = pkg - self.__version = None - - @property - def version(self): - if self.__version is None: - self.__version = self._load_version() - return self.__version - - def _load_version(self): - try: - from pkg_resources import get_distribution, DistributionNotFound - except ImportError: - return 'N/A' - # - try: - return get_distribution(self.pkg).version - except DistributionNotFound: - # package is not installed - return 'N/A' - - def __repr__(self): - return self.version - - def __eq__(self, other): - return self.version == other - - def __ne__(self, other): - return not self == other - - -__version__ = LazyVersion(__name__) - -# ---------------------- - - class Color: black = '30' darkred = '31' From 9069419d6068c6b9e6aef66addbb7dd5edab390d Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 17:27:04 +0100 Subject: [PATCH 08/48] we surely don't need to support python 2.7 now :) --- Lib/_pyrepl/fancycompleter.py | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 6769a0bbeb0ae7..9881f9e5acdaff 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -15,27 +15,6 @@ import os.path from itertools import count -PY3K = sys.version_info[0] >= 3 - -# python3 compatibility -# --------------------- -try: - from itertools import izip -except ImportError: - izip = zip - -try: - from types import ClassType -except ImportError: - ClassType = type - -try: - unicode -except NameError: - unicode = str - -# ---------------------- - class Color: black = '30' darkred = '31' @@ -78,13 +57,12 @@ class DefaultConfig: types.FunctionType: Color.blue, types.BuiltinFunctionType: Color.blue, - ClassType: Color.fuchsia, type: Color.fuchsia, types.ModuleType: Color.teal, type(None): Color.lightgray, str: Color.green, - unicode: Color.green, + bytes: Color.green, int: Color.yellow, float: Color.yellow, complex: Color.yellow, @@ -257,12 +235,6 @@ def attr_matches(self, text): except Exception: val = None # Include even if attribute not set - if not PY3K and isinstance(word, unicode): - # this is needed because pyrepl doesn't like unicode - # completions: as soon as it finds something which is not str, - # it stops. - word = word.encode('utf-8') - names.append(word) values.append(val) if names or not noprefix: @@ -292,7 +264,7 @@ def attr_matches(self, text): def color_matches(self, names, values): matches = [self.color_for_obj(i, name, obj) for i, name, obj - in izip(count(), names, values)] + 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 + [' '] From bdbe022f62dfa044df9d4a7d97b9232eee7c9c00 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 17:28:24 +0100 Subject: [PATCH 09/48] kill ConfigurableClass --- Lib/_pyrepl/fancycompleter.py | 54 ++--------------------------------- 1 file changed, 3 insertions(+), 51 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 9881f9e5acdaff..0848b09c85f505 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -82,55 +82,7 @@ def setup(self): self.use_colors = colors.RED != "" -def my_execfile(filename, mydict): - with open(filename) as f: - code = compile(f.read(), filename, 'exec') - exec(code, mydict) - - -class ConfigurableClass: - DefaultConfig = None - config_filename = None - - def get_config(self, Config): - if Config is not None: - return Config() - # try to load config from the ~/filename file - filename = '~/' + self.config_filename - rcfile = os.path.normpath(os.path.expanduser(filename)) - if not os.path.exists(rcfile): - return self.DefaultConfig() - - mydict = {} - try: - my_execfile(rcfile, mydict) - except Exception as exc: - import traceback - - sys.stderr.write("** error when importing %s: %r **\n" % (filename, exc)) - traceback.print_tb(sys.exc_info()[2]) - return self.DefaultConfig() - - try: - Config = mydict["Config"] - except KeyError: - return self.DefaultConfig() - - try: - return Config() - except Exception as exc: - err = "error when setting up Config from %s: %s" % (filename, exc) - tb = sys.exc_info()[2] - if tb and tb.tb_next: - tb = tb.tb_next - err_fname = tb.tb_frame.f_code.co_filename - err_lnum = tb.tb_lineno - err += " (%s:%d)" % (err_fname, err_lnum,) - sys.stderr.write("** %s **\n" % err) - return self.DefaultConfig() - - -class Completer(rlcompleter.Completer, ConfigurableClass): +class Completer(rlcompleter.Completer): """ When doing someting like a.b., display only the attributes of b instead of the full a.b.attr string. @@ -142,9 +94,9 @@ class Completer(rlcompleter.Completer, ConfigurableClass): DefaultConfig = DefaultConfig config_filename = '.fancycompleterrc.py.xxx' - def __init__(self, namespace=None, Config=None): + def __init__(self, namespace=None, Config=DefaultConfig): rlcompleter.Completer.__init__(self, namespace) - self.config = self.get_config(Config) + self.config = Config() self.config.setup() # XXX: double check what happens in this case once fancycompleter works From 0ea7c4951ebc2a429260eac2b84ded05d3930008 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 17:36:28 +0100 Subject: [PATCH 10/48] better name --- Lib/_pyrepl/fancycompleter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 0848b09c85f505..1b000dfe27e95c 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -207,13 +207,13 @@ def attr_matches(self, text): return ['%s.%s' % (expr, prefix)] # autocomplete prefix if self.config.use_colors: - return self.color_matches(names, values) + return self.colorize_matches(names, values) if prefix: names += [' '] return names - def color_matches(self, names, values): + def colorize_matches(self, names, values): matches = [self.color_for_obj(i, name, obj) for i, name, obj in zip(count(), names, values)] From 6ddfa611d83bf4a99ec40b3c72408bd9d5c20b4d Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 17:54:42 +0100 Subject: [PATCH 11/48] use _colorize instead of our own Color --- Lib/_colorize.py | 4 ++ Lib/_pyrepl/fancycompleter.py | 72 ++++++++++++----------------------- 2 files changed, 28 insertions(+), 48 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 41e818f2a747ff..412257314489aa 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -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" diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 1b000dfe27e95c..4ee2e3b12908b3 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -9,38 +9,13 @@ from __future__ import print_function from _pyrepl import readline +from _colorize import ANSIColors import rlcompleter import sys import types import os.path from itertools import count -class Color: - black = '30' - darkred = '31' - darkgreen = '32' - brown = '33' - darkblue = '34' - purple = '35' - teal = '36' - lightgray = '37' - darkgray = '30;01' - red = '31;01' - green = '32;01' - yellow = '33;01' - blue = '34;01' - fuchsia = '35;01' - turquoise = '36;01' - white = '37;01' - - @classmethod - def set(cls, color, string): - try: - color = getattr(cls, color) - except AttributeError: - pass - return '\x1b[%sm%s\x1b[00m' % (color, string) - class DefaultConfig: @@ -48,30 +23,30 @@ class DefaultConfig: use_colors = 'auto' color_by_type = { - types.BuiltinMethodType: Color.turquoise, - types.MethodType: Color.turquoise, - type((42).__add__): Color.turquoise, - type(int.__add__): Color.turquoise, - type(str.replace): Color.turquoise, - - types.FunctionType: Color.blue, - types.BuiltinFunctionType: Color.blue, - - type: Color.fuchsia, - - types.ModuleType: Color.teal, - type(None): Color.lightgray, - str: Color.green, - bytes: Color.green, - int: Color.yellow, - float: Color.yellow, - complex: Color.yellow, - bool: Color.yellow, + 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, } # Fallback to look up colors by `isinstance` when not matched # via color_by_type. color_by_baseclass = [ - ((BaseException,), Color.red), + ((BaseException,), ANSIColors.BOLD_RED), ] @@ -230,10 +205,11 @@ def color_for_obj(self, i, name, value): color = _color break else: - color = '00' + color = ANSIColors.RESET # hack: prepend an (increasing) fake escape sequence, # so that readline can sort the matches correctly. - return '\x1b[%03d;00m' % i + Color.set(color, name) + N = f"\x1b[{i:03d};00m" + return f"{N}{color}{name}{ANSIColors.RESET}" def commonprefix(names, base=''): From 30abd71ab94c4c305211ad9d45e4d7f71ed73070 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 18:25:01 +0100 Subject: [PATCH 12/48] WIP: copy&adapt some tests from the original fancycompleter. They don't work because they need to be ported from pytest to unittest --- Lib/_pyrepl/fancycompleter.py | 7 +- Lib/test/test_pyrepl/test_fancycompleter.py | 209 ++++++++++++++++++++ 2 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 Lib/test/test_pyrepl/test_fancycompleter.py diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 4ee2e3b12908b3..5ad17a47fc9f6e 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -5,15 +5,10 @@ """ Colorful TAB completion for Python prompt """ -from __future__ import with_statement -from __future__ import print_function - from _pyrepl import readline from _colorize import ANSIColors import rlcompleter -import sys import types -import os.path from itertools import count @@ -124,7 +119,7 @@ def global_matches(self, text): except Exception as exc: values.append(exc) if self.config.use_colors and names: - return self.color_matches(names, values) + return self.colorize_matches(names, values) return names def attr_matches(self, text): diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py new file mode 100644 index 00000000000000..ebd4684256f3b1 --- /dev/null +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -0,0 +1,209 @@ +import unittest +import sys + +import _pyrepl.readline +from _pyrepl.fancycompleter import Completer, DefaultConfig, commonprefix + + +class ConfigForTest(DefaultConfig): + use_colors = False + +class ColorConfig(DefaultConfig): + use_colors = True + +class FancyCompleterTests(unittest.TestCase): + + def test_commonprefix(self): + assert commonprefix(['isalpha', 'isdigit', 'foo']) == '' + assert commonprefix(['isalpha', 'isdigit']) == 'is' + assert commonprefix(['isalpha', 'isdigit', 'foo'], base='i') == 'is' + assert commonprefix([]) == '' + assert commonprefix(['aaa', 'bbb'], base='x') == '' + + + def test_complete_attribute(self): + compl = Completer({'a': None}, ConfigForTest) + assert compl.attr_matches('a.') == ['a.__'] + matches = compl.attr_matches('a.__') + assert 'a.__class__' not in matches + assert '__class__' in matches + assert compl.attr_matches('a.__class') == ['a.__class__'] + + + def test_complete_attribute_prefix(self): + class C(object): + attr = 1 + _attr = 2 + __attr__attr = 3 + compl = Completer({'a': C}, ConfigForTest) + assert compl.attr_matches('a.') == ['attr', 'mro'] + assert compl.attr_matches('a._') == ['_C__attr__attr', '_attr', ' '] + matches = compl.attr_matches('a.__') + assert 'a.__class__' not in matches + assert '__class__' in matches + assert compl.attr_matches('a.__class') == ['a.__class__'] + + compl = Completer({'a': None}, ConfigForTest) + assert compl.attr_matches('a._') == ['a.__'] + + + def test_complete_attribute_colored(self): + compl = Completer({'a': 42}, ColorConfig) + matches = compl.attr_matches('a.__') + assert len(matches) > 2 + expected_color = compl.config.color_by_type.get(type(compl.__class__)) + assert expected_color == '35;01' + expected_part = Color.set(expected_color, '__class__') + for match in matches: + if expected_part in match: + break + else: + assert False, matches + assert ' ' in matches + + + def test_complete_colored_single_match(self): + """No coloring, via commonprefix.""" + compl = Completer({'foobar': 42}, ColorConfig) + matches = compl.global_matches('foob') + assert matches == ['foobar'] + + + def test_does_not_color_single_match(self): + class obj: + msgs = [] + + compl = Completer({'obj': obj}, ColorConfig) + matches = compl.attr_matches('obj.msgs') + assert matches == ['obj.msgs'] + + + def test_complete_global(self): + compl = Completer({'foobar': 1, 'foobazzz': 2}, ConfigForTest) + assert compl.global_matches('foo') == ['fooba'] + matches = compl.global_matches('fooba') + assert set(matches) == set(['foobar', 'foobazzz']) + assert compl.global_matches('foobaz') == ['foobazzz'] + assert compl.global_matches('nothing') == [] + + + def test_complete_global_colored(self): + compl = Completer({'foobar': 1, 'foobazzz': 2}, ColorConfig) + assert compl.global_matches('foo') == ['fooba'] + matches = compl.global_matches('fooba') + assert set(matches) == { + ' ', + '\x1b[001;00m\x1b[33;01mfoobazzz\x1b[00m', + '\x1b[000;00m\x1b[33;01mfoobar\x1b[00m', + } + assert compl.global_matches('foobaz') == ['foobazzz'] + assert compl.global_matches('nothing') == [] + + + def test_complete_global_colored_exception(self): + compl = Completer({'tryme': ValueError()}, ColorConfig) + if sys.version_info >= (3, 6): + assert compl.global_matches('try') == [ + '\x1b[000;00m\x1b[37mtry:\x1b[00m', + '\x1b[001;00m\x1b[31;01mtryme\x1b[00m', + ' ' + ] + else: + assert compl.global_matches('try') == [ + '\x1b[000;00m\x1b[37mtry\x1b[00m', + '\x1b[001;00m\x1b[31;01mtryme\x1b[00m', + ' ' + ] + + + def test_complete_global_exception(monkeypatchself): + import rlcompleter + + def rlcompleter_global_matches(self, text): + return ['trigger_exception!', 'nameerror', 'valid'] + + monkeypatch.setattr(rlcompleter.Completer, 'global_matches', + rlcompleter_global_matches) + + compl = Completer({'valid': 42}, ColorConfig) + assert compl.global_matches("") == [ + "\x1b[000;00m\x1b[31;01mnameerror\x1b[00m", + "\x1b[001;00m\x1b[31;01mtrigger_exception!\x1b[00m", + "\x1b[002;00m\x1b[33;01mvalid\x1b[00m", + " ", + ] + + + def test_color_for_obj(monkeypatchself): + class Config(ColorConfig): + color_by_type = {} + + compl = Completer({}, Config) + assert compl.color_for_obj(1, "foo", "bar") == "\x1b[001;00m\x1b[00mfoo\x1b[00m" + + + def test_complete_with_indexer(self): + compl = Completer({'lst': [None, 2, 3]}, ConfigForTest) + assert compl.attr_matches('lst[0].') == ['lst[0].__'] + matches = compl.attr_matches('lst[0].__') + assert 'lst[0].__class__' not in matches + assert '__class__' in matches + assert compl.attr_matches('lst[0].__class') == ['lst[0].__class__'] + + + def test_autocomplete(self): + class A: + aaa = None + abc_1 = None + abc_2 = None + abc_3 = None + bbb = None + compl = Completer({'A': A}, ConfigForTest) + # + # in this case, we want to display all attributes which start with + # 'a'. MOREOVER, we also include a space to prevent readline to + # automatically insert the common prefix (which will the the ANSI escape + # sequence if we use colors) + matches = compl.attr_matches('A.a') + assert sorted(matches) == [' ', 'aaa', 'abc_1', 'abc_2', 'abc_3'] + # + # IF there is an actual common prefix, we return just it, so that readline + # will insert it into place + matches = compl.attr_matches('A.ab') + assert matches == ['A.abc_'] + # + # finally, at the next TAB, we display again all the completions available + # for this common prefix. Agai, we insert a spurious space to prevent the + # automatic completion of ANSI sequences + matches = compl.attr_matches('A.abc_') + assert sorted(matches) == [' ', 'abc_1', 'abc_2', 'abc_3'] + + + def test_complete_exception(self): + compl = Completer({}, ConfigForTest) + assert compl.attr_matches('xxx.') == [] + + + def test_complete_invalid_attr(self): + compl = Completer({'str': str}, ConfigForTest) + assert compl.attr_matches('str.xx') == [] + + + def test_complete_function_skipped(self): + compl = Completer({'str': str}, ConfigForTest) + assert compl.attr_matches('str.split().') == [] + + + def test_unicode_in___dir__(self): + class Foo(object): + def __dir__(self): + return [u'hello', 'world'] + + compl = Completer({'a': Foo()}, ConfigForTest) + matches = compl.attr_matches('a.') + assert matches == ['hello', 'world'] + assert type(matches[0]) is str + + +if __name__ == "__main__": + unittest.main() From 983824aade618eb7581062cd85c2cb7bc7141653 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 18:27:55 +0100 Subject: [PATCH 13/48] edited by copilot: move from pytest-style to unittest-style --- Lib/test/test_pyrepl/test_fancycompleter.py | 146 ++++++++++---------- 1 file changed, 75 insertions(+), 71 deletions(-) diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py index ebd4684256f3b1..1007dbd4179f97 100644 --- a/Lib/test/test_pyrepl/test_fancycompleter.py +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -11,24 +11,43 @@ class ConfigForTest(DefaultConfig): class ColorConfig(DefaultConfig): use_colors = True +class MockPatch: + def __init__(self): + self.original_values = {} + + def setattr(self, obj, name, value): + if obj not in self.original_values: + self.original_values[obj] = {} + if name not in self.original_values[obj]: + self.original_values[obj][name] = getattr(obj, name) + setattr(obj, name, value) + + def restore_all(self): + for obj, attrs in self.original_values.items(): + for name, value in attrs.items(): + setattr(obj, name, value) + class FancyCompleterTests(unittest.TestCase): + def setUp(self): + self.mock_patch = MockPatch() - def test_commonprefix(self): - assert commonprefix(['isalpha', 'isdigit', 'foo']) == '' - assert commonprefix(['isalpha', 'isdigit']) == 'is' - assert commonprefix(['isalpha', 'isdigit', 'foo'], base='i') == 'is' - assert commonprefix([]) == '' - assert commonprefix(['aaa', 'bbb'], base='x') == '' + def tearDown(self): + self.mock_patch.restore_all() + def test_commonprefix(self): + self.assertEqual(commonprefix(['isalpha', 'isdigit', 'foo']), '') + self.assertEqual(commonprefix(['isalpha', 'isdigit']), 'is') + self.assertEqual(commonprefix(['isalpha', 'isdigit', 'foo'], base='i'), 'is') + self.assertEqual(commonprefix([]), '') + self.assertEqual(commonprefix(['aaa', 'bbb'], base='x'), '') def test_complete_attribute(self): compl = Completer({'a': None}, ConfigForTest) - assert compl.attr_matches('a.') == ['a.__'] + self.assertEqual(compl.attr_matches('a.'), ['a.__']) matches = compl.attr_matches('a.__') - assert 'a.__class__' not in matches - assert '__class__' in matches - assert compl.attr_matches('a.__class') == ['a.__class__'] - + self.assertNotIn('a.__class__', matches) + self.assertIn('__class__', matches) + self.assertEqual(compl.attr_matches('a.__class'), ['a.__class__']) def test_complete_attribute_prefix(self): class C(object): @@ -36,38 +55,35 @@ class C(object): _attr = 2 __attr__attr = 3 compl = Completer({'a': C}, ConfigForTest) - assert compl.attr_matches('a.') == ['attr', 'mro'] - assert compl.attr_matches('a._') == ['_C__attr__attr', '_attr', ' '] + self.assertEqual(compl.attr_matches('a.'), ['attr', 'mro']) + self.assertEqual(compl.attr_matches('a._'), ['_C__attr__attr', '_attr', ' ']) matches = compl.attr_matches('a.__') - assert 'a.__class__' not in matches - assert '__class__' in matches - assert compl.attr_matches('a.__class') == ['a.__class__'] + self.assertNotIn('a.__class__', matches) + self.assertIn('__class__', matches) + self.assertEqual(compl.attr_matches('a.__class'), ['a.__class__']) compl = Completer({'a': None}, ConfigForTest) - assert compl.attr_matches('a._') == ['a.__'] - + self.assertEqual(compl.attr_matches('a._'), ['a.__']) def test_complete_attribute_colored(self): compl = Completer({'a': 42}, ColorConfig) matches = compl.attr_matches('a.__') - assert len(matches) > 2 + self.assertGreater(len(matches), 2) expected_color = compl.config.color_by_type.get(type(compl.__class__)) - assert expected_color == '35;01' + self.assertEqual(expected_color, '35;01') expected_part = Color.set(expected_color, '__class__') for match in matches: if expected_part in match: break else: - assert False, matches - assert ' ' in matches - + self.assertFalse(True, matches) + self.assertIn(' ', matches) def test_complete_colored_single_match(self): """No coloring, via commonprefix.""" compl = Completer({'foobar': 42}, ColorConfig) matches = compl.global_matches('foob') - assert matches == ['foobar'] - + self.assertEqual(matches, ['foobar']) def test_does_not_color_single_match(self): class obj: @@ -75,81 +91,73 @@ class obj: compl = Completer({'obj': obj}, ColorConfig) matches = compl.attr_matches('obj.msgs') - assert matches == ['obj.msgs'] - + self.assertEqual(matches, ['obj.msgs']) def test_complete_global(self): compl = Completer({'foobar': 1, 'foobazzz': 2}, ConfigForTest) - assert compl.global_matches('foo') == ['fooba'] + self.assertEqual(compl.global_matches('foo'), ['fooba']) matches = compl.global_matches('fooba') - assert set(matches) == set(['foobar', 'foobazzz']) - assert compl.global_matches('foobaz') == ['foobazzz'] - assert compl.global_matches('nothing') == [] - + self.assertEqual(set(matches), set(['foobar', 'foobazzz'])) + self.assertEqual(compl.global_matches('foobaz'), ['foobazzz']) + self.assertEqual(compl.global_matches('nothing'), []) def test_complete_global_colored(self): compl = Completer({'foobar': 1, 'foobazzz': 2}, ColorConfig) - assert compl.global_matches('foo') == ['fooba'] + self.assertEqual(compl.global_matches('foo'), ['fooba']) matches = compl.global_matches('fooba') - assert set(matches) == { + self.assertEqual(set(matches), { ' ', '\x1b[001;00m\x1b[33;01mfoobazzz\x1b[00m', '\x1b[000;00m\x1b[33;01mfoobar\x1b[00m', - } - assert compl.global_matches('foobaz') == ['foobazzz'] - assert compl.global_matches('nothing') == [] - + }) + self.assertEqual(compl.global_matches('foobaz'), ['foobazzz']) + self.assertEqual(compl.global_matches('nothing'), []) def test_complete_global_colored_exception(self): compl = Completer({'tryme': ValueError()}, ColorConfig) if sys.version_info >= (3, 6): - assert compl.global_matches('try') == [ + self.assertEqual(compl.global_matches('try'), [ '\x1b[000;00m\x1b[37mtry:\x1b[00m', '\x1b[001;00m\x1b[31;01mtryme\x1b[00m', ' ' - ] + ]) else: - assert compl.global_matches('try') == [ + self.assertEqual(compl.global_matches('try'), [ '\x1b[000;00m\x1b[37mtry\x1b[00m', '\x1b[001;00m\x1b[31;01mtryme\x1b[00m', ' ' - ] - - - def test_complete_global_exception(monkeypatchself): - import rlcompleter + ]) + def test_complete_global_exception(self): def rlcompleter_global_matches(self, text): return ['trigger_exception!', 'nameerror', 'valid'] - monkeypatch.setattr(rlcompleter.Completer, 'global_matches', - rlcompleter_global_matches) + self.mock_patch.setattr(rlcompleter.Completer, 'global_matches', + rlcompleter_global_matches) compl = Completer({'valid': 42}, ColorConfig) - assert compl.global_matches("") == [ + self.assertEqual(compl.global_matches(""), [ "\x1b[000;00m\x1b[31;01mnameerror\x1b[00m", "\x1b[001;00m\x1b[31;01mtrigger_exception!\x1b[00m", "\x1b[002;00m\x1b[33;01mvalid\x1b[00m", " ", - ] - + ]) - def test_color_for_obj(monkeypatchself): + def test_color_for_obj(self): class Config(ColorConfig): color_by_type = {} compl = Completer({}, Config) - assert compl.color_for_obj(1, "foo", "bar") == "\x1b[001;00m\x1b[00mfoo\x1b[00m" - + self.assertEqual(compl.color_for_obj(1, "foo", "bar"), + "\x1b[001;00m\x1b[00mfoo\x1b[00m") def test_complete_with_indexer(self): compl = Completer({'lst': [None, 2, 3]}, ConfigForTest) - assert compl.attr_matches('lst[0].') == ['lst[0].__'] + self.assertEqual(compl.attr_matches('lst[0].'), ['lst[0].__']) matches = compl.attr_matches('lst[0].__') - assert 'lst[0].__class__' not in matches - assert '__class__' in matches - assert compl.attr_matches('lst[0].__class') == ['lst[0].__class__'] - + self.assertNotIn('lst[0].__class__', matches) + self.assertIn('__class__', matches) + self.assertEqual(compl.attr_matches('lst[0].__class'), ['lst[0].__class__']) def test_autocomplete(self): class A: @@ -165,34 +173,30 @@ class A: # automatically insert the common prefix (which will the the ANSI escape # sequence if we use colors) matches = compl.attr_matches('A.a') - assert sorted(matches) == [' ', 'aaa', 'abc_1', 'abc_2', 'abc_3'] + self.assertEqual(sorted(matches), [' ', 'aaa', 'abc_1', 'abc_2', 'abc_3']) # # IF there is an actual common prefix, we return just it, so that readline # will insert it into place matches = compl.attr_matches('A.ab') - assert matches == ['A.abc_'] + self.assertEqual(matches, ['A.abc_']) # # finally, at the next TAB, we display again all the completions available # for this common prefix. Agai, we insert a spurious space to prevent the # automatic completion of ANSI sequences matches = compl.attr_matches('A.abc_') - assert sorted(matches) == [' ', 'abc_1', 'abc_2', 'abc_3'] - + self.assertEqual(sorted(matches), [' ', 'abc_1', 'abc_2', 'abc_3']) def test_complete_exception(self): compl = Completer({}, ConfigForTest) - assert compl.attr_matches('xxx.') == [] - + self.assertEqual(compl.attr_matches('xxx.'), []) def test_complete_invalid_attr(self): compl = Completer({'str': str}, ConfigForTest) - assert compl.attr_matches('str.xx') == [] - + self.assertEqual(compl.attr_matches('str.xx'), []) def test_complete_function_skipped(self): compl = Completer({'str': str}, ConfigForTest) - assert compl.attr_matches('str.split().') == [] - + self.assertEqual(compl.attr_matches('str.split().'), []) def test_unicode_in___dir__(self): class Foo(object): @@ -201,8 +205,8 @@ def __dir__(self): compl = Completer({'a': Foo()}, ConfigForTest) matches = compl.attr_matches('a.') - assert matches == ['hello', 'world'] - assert type(matches[0]) is str + self.assertEqual(matches, ['hello', 'world']) + self.assertIs(type(matches[0]), str) if __name__ == "__main__": From acbafe4b4f05662dc7217b3734863c5a875ccc24 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 18:39:55 +0100 Subject: [PATCH 14/48] don't try to be too clever with exceptions: if a global name raises an exception when evaluated, just color it as None --- Lib/_pyrepl/fancycompleter.py | 2 +- Lib/test/test_pyrepl/test_fancycompleter.py | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 5ad17a47fc9f6e..7586d37ac7895d 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -117,7 +117,7 @@ def global_matches(self, text): try: values.append(eval(name, self.namespace)) except Exception as exc: - values.append(exc) + values.append(None) if self.config.use_colors and names: return self.colorize_matches(names, values) return names diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py index 1007dbd4179f97..a9167d128989b2 100644 --- a/Lib/test/test_pyrepl/test_fancycompleter.py +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -128,21 +128,6 @@ def test_complete_global_colored_exception(self): ' ' ]) - def test_complete_global_exception(self): - def rlcompleter_global_matches(self, text): - return ['trigger_exception!', 'nameerror', 'valid'] - - self.mock_patch.setattr(rlcompleter.Completer, 'global_matches', - rlcompleter_global_matches) - - compl = Completer({'valid': 42}, ColorConfig) - self.assertEqual(compl.global_matches(""), [ - "\x1b[000;00m\x1b[31;01mnameerror\x1b[00m", - "\x1b[001;00m\x1b[31;01mtrigger_exception!\x1b[00m", - "\x1b[002;00m\x1b[33;01mvalid\x1b[00m", - " ", - ]) - def test_color_for_obj(self): class Config(ColorConfig): color_by_type = {} From 5546b94262a69ef8a94f21e25d244a943f44a22d Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 18:40:43 +0100 Subject: [PATCH 15/48] no longer needed --- Lib/_pyrepl/fancycompleter.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 7586d37ac7895d..d13923677d3fb2 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -38,12 +38,6 @@ class DefaultConfig: complex: ANSIColors.BOLD_YELLOW, bool: ANSIColors.BOLD_YELLOW, } - # Fallback to look up colors by `isinstance` when not matched - # via color_by_type. - color_by_baseclass = [ - ((BaseException,), ANSIColors.BOLD_RED), - ] - def setup(self): import _colorize @@ -60,10 +54,6 @@ class Completer(rlcompleter.Completer): Optionally, display the various completions in different colors depending on the type. """ - - DefaultConfig = DefaultConfig - config_filename = '.fancycompleterrc.py.xxx' - def __init__(self, namespace=None, Config=DefaultConfig): rlcompleter.Completer.__init__(self, namespace) self.config = Config() @@ -193,14 +183,7 @@ def colorize_matches(self, names, values): def color_for_obj(self, i, name, value): t = type(value) - color = self.config.color_by_type.get(t, None) - if color is None: - for x, _color in self.config.color_by_baseclass: - if isinstance(value, x): - color = _color - break - else: - color = ANSIColors.RESET + 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" From 327648fbc97ca3cdd3a35b969182d50c957964c1 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 18:42:27 +0100 Subject: [PATCH 16/48] this doesn't test anything meaningful --- Lib/test/test_pyrepl/test_fancycompleter.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py index a9167d128989b2..832e2f7e7c9cbb 100644 --- a/Lib/test/test_pyrepl/test_fancycompleter.py +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -128,14 +128,6 @@ def test_complete_global_colored_exception(self): ' ' ]) - def test_color_for_obj(self): - class Config(ColorConfig): - color_by_type = {} - - compl = Completer({}, Config) - self.assertEqual(compl.color_for_obj(1, "foo", "bar"), - "\x1b[001;00m\x1b[00mfoo\x1b[00m") - def test_complete_with_indexer(self): compl = Completer({'lst': [None, 2, 3]}, ConfigForTest) self.assertEqual(compl.attr_matches('lst[0].'), ['lst[0].__']) From 44549b915c10bbae7605a25aa63994dc746d755b Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 18:45:19 +0100 Subject: [PATCH 17/48] fix this test --- Lib/test/test_pyrepl/test_fancycompleter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py index 832e2f7e7c9cbb..2039da428b44fe 100644 --- a/Lib/test/test_pyrepl/test_fancycompleter.py +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -1,6 +1,7 @@ import unittest import sys +from _colorize import ANSIColors import _pyrepl.readline from _pyrepl.fancycompleter import Completer, DefaultConfig, commonprefix @@ -70,8 +71,8 @@ def test_complete_attribute_colored(self): matches = compl.attr_matches('a.__') self.assertGreater(len(matches), 2) expected_color = compl.config.color_by_type.get(type(compl.__class__)) - self.assertEqual(expected_color, '35;01') - expected_part = Color.set(expected_color, '__class__') + self.assertEqual(expected_color, ANSIColors.BOLD_MAGENTA) + expected_part = f'{expected_color}__class__{ANSIColors.RESET}' for match in matches: if expected_part in match: break From c9c97f7ef6ebcc68a31bbb9f2c009b3f6f0ae781 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 18:49:54 +0100 Subject: [PATCH 18/48] fix this test --- Lib/test/test_pyrepl/test_fancycompleter.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py index 2039da428b44fe..f21b0ccbaa5365 100644 --- a/Lib/test/test_pyrepl/test_fancycompleter.py +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -106,10 +106,16 @@ def test_complete_global_colored(self): compl = Completer({'foobar': 1, 'foobazzz': 2}, ColorConfig) self.assertEqual(compl.global_matches('foo'), ['fooba']) matches = compl.global_matches('fooba') + + # these are the fake escape sequences which are needed so that + # readline displays the matches in the proper order + N0 = f"\x1b[000;00m" + N1 = f"\x1b[001;00m" + self.assertEqual(set(matches), { ' ', - '\x1b[001;00m\x1b[33;01mfoobazzz\x1b[00m', - '\x1b[000;00m\x1b[33;01mfoobar\x1b[00m', + f'{N0}{ANSIColors.BOLD_YELLOW}foobar{ANSIColors.RESET}', + f'{N1}{ANSIColors.BOLD_YELLOW}foobazzz{ANSIColors.RESET}', }) self.assertEqual(compl.global_matches('foobaz'), ['foobazzz']) self.assertEqual(compl.global_matches('nothing'), []) From c61bab60f316c4eae18f004b08b49afb228f5005 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Tue, 18 Feb 2025 18:52:27 +0100 Subject: [PATCH 19/48] Fix this test --- Lib/test/test_pyrepl/test_fancycompleter.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py index f21b0ccbaa5365..089710f33b1a49 100644 --- a/Lib/test/test_pyrepl/test_fancycompleter.py +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -121,19 +121,14 @@ def test_complete_global_colored(self): self.assertEqual(compl.global_matches('nothing'), []) def test_complete_global_colored_exception(self): - compl = Completer({'tryme': ValueError()}, ColorConfig) - if sys.version_info >= (3, 6): - self.assertEqual(compl.global_matches('try'), [ - '\x1b[000;00m\x1b[37mtry:\x1b[00m', - '\x1b[001;00m\x1b[31;01mtryme\x1b[00m', - ' ' - ]) - else: - self.assertEqual(compl.global_matches('try'), [ - '\x1b[000;00m\x1b[37mtry\x1b[00m', - '\x1b[001;00m\x1b[31;01mtryme\x1b[00m', - ' ' - ]) + compl = Completer({'tryme': 42}, ColorConfig) + N0 = f"\x1b[000;00m" + N1 = f"\x1b[001;00m" + self.assertEqual(compl.global_matches('try'), [ + f'{N0}{ANSIColors.GREY}try:{ANSIColors.RESET}', + f'{N1}{ANSIColors.BOLD_YELLOW}tryme{ANSIColors.RESET}', + ' ' + ]) def test_complete_with_indexer(self): compl = Completer({'lst': [None, 2, 3]}, ConfigForTest) From 1de48b6909152c5dd78a932bd6eb05dec490e17c Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 12:06:45 +0200 Subject: [PATCH 20/48] Apply hugovk suggestions from code review Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Lib/_colorize.py | 4 ++-- Lib/_pyrepl/fancycompleter.py | 24 ++++++++++----------- Lib/test/test_pyrepl/test_fancycompleter.py | 14 ++++++------ 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 412257314489aa..269f15483038ad 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -8,18 +8,18 @@ class ANSIColors: BACKGROUND_YELLOW = "\x1b[43m" BOLD_BLUE = "\x1b[1;34m" + BOLD_CYAN = "\x1b[1;36m" 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" + CYAN = "\x1b[36m" GREEN = "\x1b[32m" GREY = "\x1b[90m" MAGENTA = "\x1b[35m" RED = "\x1b[31m" RESET = "\x1b[0m" - TEAL = "\x1b[36m" YELLOW = "\x1b[33m" diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index d13923677d3fb2..b15feff9a921b5 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -3,7 +3,7 @@ # # All Rights Reserved """ -Colorful TAB completion for Python prompt +Colorful tab completion for Python prompt """ from _pyrepl import readline from _colorize import ANSIColors @@ -18,18 +18,18 @@ class DefaultConfig: 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.BuiltinMethodType: ANSIColors.BOLD_CYAN, + types.MethodType: ANSIColors.BOLD_CYAN, + type((42).__add__): ANSIColors.BOLD_CYAN, + type(int.__add__): ANSIColors.BOLD_CYAN, + type(str.replace): ANSIColors.BOLD_CYAN, types.FunctionType: ANSIColors.BOLD_BLUE, types.BuiltinFunctionType: ANSIColors.BOLD_BLUE, type: ANSIColors.BOLD_MAGENTA, - types.ModuleType: ANSIColors.TEAL, + types.ModuleType: ANSIColors.CYAN, type(None): ANSIColors.GREY, str: ANSIColors.BOLD_GREEN, bytes: ANSIColors.BOLD_GREEN, @@ -48,7 +48,7 @@ def setup(self): class Completer(rlcompleter.Completer): """ - When doing someting like a.b., display only the attributes of + When doing someting like a.b., display only the attributes of b instead of the full a.b.attr string. Optionally, display the various completions in different colors @@ -61,10 +61,10 @@ def __init__(self, namespace=None, Config=DefaultConfig): # 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 + # This is needed to offer PyREPL a better chance to patch + # raw_input. Usually, it does at import time, but if 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 + # terminal and thus the raw_input hook is not installed. readline._setup() if self.config.use_colors: @@ -178,7 +178,7 @@ def colorize_matches(self, names, values): 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. + # common prefix, which is the ANSI escape sequence. return matches + [' '] def color_for_obj(self, i, name, value): diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py index 089710f33b1a49..4f99f157dec5bf 100644 --- a/Lib/test/test_pyrepl/test_fancycompleter.py +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -147,21 +147,21 @@ class A: bbb = None compl = Completer({'A': A}, ConfigForTest) # - # in this case, we want to display all attributes which start with - # 'a'. MOREOVER, we also include a space to prevent readline to + # In this case, we want to display all attributes which start with + # 'a'. Moreover, we also include a space to prevent readline to # automatically insert the common prefix (which will the the ANSI escape - # sequence if we use colors) + # sequence if we use colors). matches = compl.attr_matches('A.a') self.assertEqual(sorted(matches), [' ', 'aaa', 'abc_1', 'abc_2', 'abc_3']) # - # IF there is an actual common prefix, we return just it, so that readline + # If there is an actual common prefix, we return just it, so that readline # will insert it into place matches = compl.attr_matches('A.ab') self.assertEqual(matches, ['A.abc_']) # - # finally, at the next TAB, we display again all the completions available - # for this common prefix. Agai, we insert a spurious space to prevent the - # automatic completion of ANSI sequences + # Finally, at the next tab, we display again all the completions available + # for this common prefix. Again, we insert a spurious space to prevent the + # automatic completion of ANSI sequences. matches = compl.attr_matches('A.abc_') self.assertEqual(sorted(matches), [' ', 'abc_1', 'abc_2', 'abc_3']) From 157f4b4306a5d28cbdc3072a18804460ec755aa9 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 12:08:55 +0200 Subject: [PATCH 21/48] Apply suggestions from code review Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Co-authored-by: Tomas R. --- Lib/_pyrepl/fancycompleter.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index b15feff9a921b5..56bbcc205cc1a3 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -9,7 +9,6 @@ from _colorize import ANSIColors import rlcompleter import types -from itertools import count class DefaultConfig: @@ -30,7 +29,7 @@ class DefaultConfig: type: ANSIColors.BOLD_MAGENTA, types.ModuleType: ANSIColors.CYAN, - type(None): ANSIColors.GREY, + types.NoneType: ANSIColors.GREY, str: ANSIColors.BOLD_GREEN, bytes: ANSIColors.BOLD_GREEN, int: ANSIColors.BOLD_YELLOW, @@ -81,7 +80,7 @@ def complete(self, text, state): http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496812 """ if text == "": - return ['\t', None][state] + return ('\t', None)[state] else: return rlcompleter.Completer.complete(self, text, state) @@ -122,8 +121,7 @@ def attr_matches(self, text): return [] # get the content of the object, except __builtins__ - words = set(dir(thisobject)) - words.discard("__builtins__") + words = set(dir(thisobject)) - {'__builtins__'} if hasattr(thisobject, '__class__'): words.add('__class__') @@ -140,8 +138,10 @@ def attr_matches(self, text): words = sorted(words) while True: for word in words: - if (word[:n] == attr and - not (noprefix and word[:n+1] == noprefix)): + if ( + word[:n] == attr + and not (noprefix and word[:n+1] == noprefix) + ): try: val = getattr(thisobject, word) except Exception: @@ -175,8 +175,8 @@ def attr_matches(self, text): def colorize_matches(self, names, values): matches = [self.color_for_obj(i, name, obj) - for i, name, obj - in zip(count(), names, values)] + 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. return matches + [' '] @@ -191,8 +191,7 @@ def color_for_obj(self, i, name, value): def commonprefix(names, base=''): - """ return the common prefix of all 'names' starting with '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: From 722ec8a9109d5da972c9fd6e410b35344752e20f Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 12:10:57 +0200 Subject: [PATCH 22/48] Apply suggestions from code review Co-authored-by: Tomas R. --- Lib/_pyrepl/fancycompleter.py | 4 ++-- Lib/test/test_pyrepl/test_fancycompleter.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 56bbcc205cc1a3..2af4c31d81fd63 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -160,11 +160,11 @@ def attr_matches(self, text): return [] if len(names) == 1: - return ['%s.%s' % (expr, names[0])] # only option, no coloring. + return [f'{expr}.{names[0]}'] # only option, no coloring. prefix = commonprefix(names) if prefix and prefix != attr: - return ['%s.%s' % (expr, prefix)] # autocomplete prefix + return [f'{expr}.{prefix}'] # autocomplete prefix if self.config.use_colors: return self.colorize_matches(names, values) diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py index 4f99f157dec5bf..e30d8b287e5a6b 100644 --- a/Lib/test/test_pyrepl/test_fancycompleter.py +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -180,7 +180,7 @@ def test_complete_function_skipped(self): def test_unicode_in___dir__(self): class Foo(object): def __dir__(self): - return [u'hello', 'world'] + return ['hello', 'world'] compl = Completer({'a': Foo()}, ConfigForTest) matches = compl.attr_matches('a.') From e2294ec8eabe91d751952bdfb098e07c31a2cf7b Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 12:51:13 +0200 Subject: [PATCH 23/48] remove unneeded lazy import --- Lib/_pyrepl/fancycompleter.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 2af4c31d81fd63..04072dfaecf720 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -6,7 +6,7 @@ Colorful tab completion for Python prompt """ from _pyrepl import readline -from _colorize import ANSIColors +from _colorize import ANSIColors, get_colors import rlcompleter import types @@ -39,9 +39,8 @@ class DefaultConfig: } def setup(self): - import _colorize if self.use_colors == 'auto': - colors = _colorize.get_colors() + colors = get_colors() self.use_colors = colors.RED != "" @@ -139,7 +138,7 @@ def attr_matches(self, text): while True: for word in words: if ( - word[:n] == attr + word[:n] == attr and not (noprefix and word[:n+1] == noprefix) ): try: From 4532850db9614120a199d47d6d5da6c8b702f385 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 12:55:08 +0200 Subject: [PATCH 24/48] Update Lib/_pyrepl/fancycompleter.py Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Lib/_pyrepl/fancycompleter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 2af4c31d81fd63..7e6b7a69890fec 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -85,8 +85,7 @@ def complete(self, text, state): 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+ + # disable automatic insertion of '(' for global callables return word def global_matches(self, text): From 70893239cf364dbe5a5515e576d6097f98bd8ce3 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 12:56:48 +0200 Subject: [PATCH 25/48] move import to module scope --- Lib/_pyrepl/fancycompleter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 44da3d53664767..fcb064d9c782f4 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -9,7 +9,7 @@ from _colorize import ANSIColors, get_colors import rlcompleter import types - +import keyword class DefaultConfig: @@ -88,7 +88,6 @@ def _callable_postfix(self, val, word): return word def global_matches(self, text): - import keyword names = rlcompleter.Completer.global_matches(self, text) prefix = commonprefix(names) if prefix and prefix != text: From 926c1a3611fa9652f0de8d677ed9c4c65ad80fd9 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 12:57:56 +0200 Subject: [PATCH 26/48] move import --- Lib/_pyrepl/readline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 0c7ae42422303c..e45162c17e36e7 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -35,12 +35,12 @@ from site import gethistoryfile import sys from rlcompleter import Completer as RLCompleter -from .fancycompleter import Completer as FancyCompleter from . import commands, historical_reader 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] From 46db5d764a8ded41ebe8a576eabe4edb24745444 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 14:57:50 +0200 Subject: [PATCH 27/48] kill this for now, we can redintroduce it later if/when we enable fancycompleter+pdb --- Lib/_pyrepl/fancycompleter.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index fcb064d9c782f4..f1058faec1156f 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -57,14 +57,6 @@ def __init__(self, namespace=None, Config=DefaultConfig): 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 if 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: From c3ea737f4e4e3c8ae8b39561d433931d1937f493 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 14:59:04 +0200 Subject: [PATCH 28/48] this link is dead, add a comment to explain what it does instead --- Lib/_pyrepl/fancycompleter.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index f1058faec1156f..699b41e981b234 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -66,10 +66,8 @@ def __init__(self, namespace=None, Config=DefaultConfig): readline.set_completer_delims(delims) def complete(self, text, state): - """ - stolen from: - http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496812 - """ + # if you press at the beginning of a line, insert an actual + # \t. Else, trigger completion. if text == "": return ('\t', None)[state] else: From 433ae06d4f19a2173b07299265c52a00577aacef Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 15:02:35 +0200 Subject: [PATCH 29/48] fix precommit --- Lib/test/test_pyrepl/test_fancycompleter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py index e30d8b287e5a6b..95b3e826bde5bf 100644 --- a/Lib/test/test_pyrepl/test_fancycompleter.py +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -1,8 +1,6 @@ import unittest -import sys from _colorize import ANSIColors -import _pyrepl.readline from _pyrepl.fancycompleter import Completer, DefaultConfig, commonprefix From 38e8a08a0c8579b02d172f07bad8d4d0e53896d5 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 15:24:02 +0200 Subject: [PATCH 30/48] we need to make this import lazy, else we get circular imports --- Lib/_pyrepl/fancycompleter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 699b41e981b234..2d5e37b54227da 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -5,7 +5,6 @@ """ Colorful tab completion for Python prompt """ -from _pyrepl import readline from _colorize import ANSIColors, get_colors import rlcompleter import types @@ -53,6 +52,7 @@ class Completer(rlcompleter.Completer): depending on the type. """ def __init__(self, namespace=None, Config=DefaultConfig): + from _pyrepl import readline rlcompleter.Completer.__init__(self, namespace) self.config = Config() self.config.setup() From 4c3ad9c192f37d493d62811eb84950a46969f29e Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 15:40:53 +0200 Subject: [PATCH 31/48] now that we have themes, we can kill the config object --- Lib/_colorize.py | 28 +++++++++ Lib/_pyrepl/fancycompleter.py | 69 ++++++++------------- Lib/test/test_pyrepl/test_fancycompleter.py | 45 ++++++-------- 3 files changed, 74 insertions(+), 68 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index d35486296f2684..9030ecf30f9ab9 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -219,6 +219,30 @@ class Unittest(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 + str: str = ANSIColors.BOLD_GREEN + bytes: str = ANSIColors.BOLD_GREEN + + @dataclass(frozen=True, kw_only=True) class Theme: """A suite of themes for all sections of Python. @@ -231,6 +255,7 @@ class Theme: syntax: Syntax = field(default_factory=Syntax) traceback: Traceback = field(default_factory=Traceback) unittest: Unittest = field(default_factory=Unittest) + fancycompleter: FancyCompleter = field(default_factory=FancyCompleter) def copy_with( self, @@ -240,6 +265,7 @@ def copy_with( syntax: Syntax | None = None, traceback: Traceback | None = None, unittest: Unittest | None = None, + fancycompleter: FancyCompleter | None = None, ) -> Self: """Return a new Theme based on this instance with some sections replaced. @@ -252,6 +278,7 @@ def copy_with( syntax=syntax or self.syntax, traceback=traceback or self.traceback, unittest=unittest or self.unittest, + fancycompleter=fancycompleter or self.fancycompleter, ) @classmethod @@ -268,6 +295,7 @@ def no_colors(cls) -> Self: syntax=Syntax.no_colors(), traceback=Traceback.no_colors(), unittest=Unittest.no_colors(), + fancycompleter=FancyCompleter.no_colors(), ) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 2d5e37b54227da..df0e32f4adabe0 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -5,44 +5,11 @@ """ Colorful tab completion for Python prompt """ -from _colorize import ANSIColors, get_colors +from _colorize import ANSIColors, get_colors, get_theme import rlcompleter import types import keyword -class DefaultConfig: - - consider_getitems = True - use_colors = 'auto' - - color_by_type = { - types.BuiltinMethodType: ANSIColors.BOLD_CYAN, - types.MethodType: ANSIColors.BOLD_CYAN, - type((42).__add__): ANSIColors.BOLD_CYAN, - type(int.__add__): ANSIColors.BOLD_CYAN, - type(str.replace): ANSIColors.BOLD_CYAN, - - types.FunctionType: ANSIColors.BOLD_BLUE, - types.BuiltinFunctionType: ANSIColors.BOLD_BLUE, - - type: ANSIColors.BOLD_MAGENTA, - - types.ModuleType: ANSIColors.CYAN, - types.NoneType: 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): - if self.use_colors == 'auto': - colors = get_colors() - self.use_colors = colors.RED != "" - - class Completer(rlcompleter.Completer): """ When doing someting like a.b., display only the attributes of @@ -51,15 +18,24 @@ class Completer(rlcompleter.Completer): Optionally, display the various completions in different colors depending on the type. """ - def __init__(self, namespace=None, Config=DefaultConfig): + def __init__( + self, + namespace=None, + *, + use_colors='auto', + consider_getitems=True, + ): from _pyrepl import readline rlcompleter.Completer.__init__(self, namespace) - self.config = Config() - self.config.setup() + 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.config.use_colors: + if self.use_colors: readline.parse_and_bind('set dont-escape-ctrl-chars on') - if self.config.consider_getitems: + if self.consider_getitems: delims = readline.get_completer_delims() delims = delims.replace('[', '') delims = delims.replace(']', '') @@ -94,7 +70,7 @@ def global_matches(self, text): values.append(eval(name, self.namespace)) except Exception as exc: values.append(None) - if self.config.use_colors and names: + if self.use_colors and names: return self.colorize_matches(names, values) return names @@ -153,7 +129,7 @@ def attr_matches(self, text): if prefix and prefix != attr: return [f'{expr}.{prefix}'] # autocomplete prefix - if self.config.use_colors: + if self.use_colors: return self.colorize_matches(names, values) if prefix: @@ -170,12 +146,21 @@ def colorize_matches(self, names, values): def color_for_obj(self, i, name, value): t = type(value) - color = self.config.color_by_type.get(t, ANSIColors.RESET) + color = self.color_by_type(t) # 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 color_by_type(self, t): + theme = get_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) + def commonprefix(names, base=''): """Return the common prefix of all 'names' starting with 'base'""" diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py index 95b3e826bde5bf..b7271a04ad7dff 100644 --- a/Lib/test/test_pyrepl/test_fancycompleter.py +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -1,14 +1,7 @@ import unittest -from _colorize import ANSIColors -from _pyrepl.fancycompleter import Completer, DefaultConfig, commonprefix - - -class ConfigForTest(DefaultConfig): - use_colors = False - -class ColorConfig(DefaultConfig): - use_colors = True +from _colorize import ANSIColors, get_theme +from _pyrepl.fancycompleter import Completer, commonprefix class MockPatch: def __init__(self): @@ -41,7 +34,7 @@ def test_commonprefix(self): self.assertEqual(commonprefix(['aaa', 'bbb'], base='x'), '') def test_complete_attribute(self): - compl = Completer({'a': None}, ConfigForTest) + compl = Completer({'a': None}, use_colors=False) self.assertEqual(compl.attr_matches('a.'), ['a.__']) matches = compl.attr_matches('a.__') self.assertNotIn('a.__class__', matches) @@ -53,7 +46,7 @@ class C(object): attr = 1 _attr = 2 __attr__attr = 3 - compl = Completer({'a': C}, ConfigForTest) + compl = Completer({'a': C}, use_colors=False) self.assertEqual(compl.attr_matches('a.'), ['attr', 'mro']) self.assertEqual(compl.attr_matches('a._'), ['_C__attr__attr', '_attr', ' ']) matches = compl.attr_matches('a.__') @@ -61,15 +54,15 @@ class C(object): self.assertIn('__class__', matches) self.assertEqual(compl.attr_matches('a.__class'), ['a.__class__']) - compl = Completer({'a': None}, ConfigForTest) + compl = Completer({'a': None}, use_colors=False) self.assertEqual(compl.attr_matches('a._'), ['a.__']) def test_complete_attribute_colored(self): - compl = Completer({'a': 42}, ColorConfig) + theme = get_theme() + compl = Completer({'a': 42}, use_colors=True) matches = compl.attr_matches('a.__') self.assertGreater(len(matches), 2) - expected_color = compl.config.color_by_type.get(type(compl.__class__)) - self.assertEqual(expected_color, ANSIColors.BOLD_MAGENTA) + expected_color = theme.fancycompleter.type expected_part = f'{expected_color}__class__{ANSIColors.RESET}' for match in matches: if expected_part in match: @@ -80,7 +73,7 @@ def test_complete_attribute_colored(self): def test_complete_colored_single_match(self): """No coloring, via commonprefix.""" - compl = Completer({'foobar': 42}, ColorConfig) + compl = Completer({'foobar': 42}, use_colors=True) matches = compl.global_matches('foob') self.assertEqual(matches, ['foobar']) @@ -88,12 +81,12 @@ def test_does_not_color_single_match(self): class obj: msgs = [] - compl = Completer({'obj': obj}, ColorConfig) + compl = Completer({'obj': obj}, use_colors=True) matches = compl.attr_matches('obj.msgs') self.assertEqual(matches, ['obj.msgs']) def test_complete_global(self): - compl = Completer({'foobar': 1, 'foobazzz': 2}, ConfigForTest) + compl = Completer({'foobar': 1, 'foobazzz': 2}, use_colors=False) self.assertEqual(compl.global_matches('foo'), ['fooba']) matches = compl.global_matches('fooba') self.assertEqual(set(matches), set(['foobar', 'foobazzz'])) @@ -101,7 +94,7 @@ def test_complete_global(self): self.assertEqual(compl.global_matches('nothing'), []) def test_complete_global_colored(self): - compl = Completer({'foobar': 1, 'foobazzz': 2}, ColorConfig) + compl = Completer({'foobar': 1, 'foobazzz': 2}, use_colors=True) self.assertEqual(compl.global_matches('foo'), ['fooba']) matches = compl.global_matches('fooba') @@ -119,7 +112,7 @@ def test_complete_global_colored(self): self.assertEqual(compl.global_matches('nothing'), []) def test_complete_global_colored_exception(self): - compl = Completer({'tryme': 42}, ColorConfig) + compl = Completer({'tryme': 42}, use_colors=True) N0 = f"\x1b[000;00m" N1 = f"\x1b[001;00m" self.assertEqual(compl.global_matches('try'), [ @@ -129,7 +122,7 @@ def test_complete_global_colored_exception(self): ]) def test_complete_with_indexer(self): - compl = Completer({'lst': [None, 2, 3]}, ConfigForTest) + compl = Completer({'lst': [None, 2, 3]}, use_colors=False) self.assertEqual(compl.attr_matches('lst[0].'), ['lst[0].__']) matches = compl.attr_matches('lst[0].__') self.assertNotIn('lst[0].__class__', matches) @@ -143,7 +136,7 @@ class A: abc_2 = None abc_3 = None bbb = None - compl = Completer({'A': A}, ConfigForTest) + compl = Completer({'A': A}, use_colors=False) # # In this case, we want to display all attributes which start with # 'a'. Moreover, we also include a space to prevent readline to @@ -164,15 +157,15 @@ class A: self.assertEqual(sorted(matches), [' ', 'abc_1', 'abc_2', 'abc_3']) def test_complete_exception(self): - compl = Completer({}, ConfigForTest) + compl = Completer({}, use_colors=False) self.assertEqual(compl.attr_matches('xxx.'), []) def test_complete_invalid_attr(self): - compl = Completer({'str': str}, ConfigForTest) + compl = Completer({'str': str}, use_colors=False) self.assertEqual(compl.attr_matches('str.xx'), []) def test_complete_function_skipped(self): - compl = Completer({'str': str}, ConfigForTest) + compl = Completer({'str': str}, use_colors=False) self.assertEqual(compl.attr_matches('str.split().'), []) def test_unicode_in___dir__(self): @@ -180,7 +173,7 @@ class Foo(object): def __dir__(self): return ['hello', 'world'] - compl = Completer({'a': Foo()}, ConfigForTest) + compl = Completer({'a': Foo()}, use_colors=False) matches = compl.attr_matches('a.') self.assertEqual(matches, ['hello', 'world']) self.assertIs(type(matches[0]), str) From 053da64c8b853cd9b419bc5ba2fb397f58b349e9 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 15:50:23 +0200 Subject: [PATCH 32/48] style --- Lib/_pyrepl/fancycompleter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index df0e32f4adabe0..c88a2858c36cfb 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -2,9 +2,7 @@ # Daniel Hahler # # All Rights Reserved -""" -Colorful tab completion for Python prompt -""" +"""Colorful tab completion for Python prompt""" from _colorize import ANSIColors, get_colors, get_theme import rlcompleter import types From 2ee47bc4355da2de75e82847f1b7809ee0aa0174 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:54:55 +0000 Subject: [PATCH 33/48] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Misc/NEWS.d/next/Library/2025-09-19-13-54-54.gh-issue- | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-09-19-13-54-54.gh-issue- diff --git a/Misc/NEWS.d/next/Library/2025-09-19-13-54-54.gh-issue- b/Misc/NEWS.d/next/Library/2025-09-19-13-54-54.gh-issue- new file mode 100644 index 00000000000000..3d2a7f00d3e6a8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-19-13-54-54.gh-issue- @@ -0,0 +1 @@ +Add fancycompleter and enable it by default when using pyrepl. This gives colored tab completion. From c3c663e1d4e4c4c108f6bd12648453254b3aff29 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 15:59:10 +0200 Subject: [PATCH 34/48] fix mypy --- Lib/_colorize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 9030ecf30f9ab9..20f504b6bbf069 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -239,8 +239,8 @@ class FancyCompleter(ThemeSection): type: str = ANSIColors.BOLD_MAGENTA module: str = ANSIColors.CYAN NoneType: str = ANSIColors.GREY - str: str = ANSIColors.BOLD_GREEN bytes: str = ANSIColors.BOLD_GREEN + str: str = ANSIColors.BOLD_GREEN @dataclass(frozen=True, kw_only=True) From b710bce38ded27a85fa51903977c69d9e6f9a977 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 16:04:40 +0200 Subject: [PATCH 35/48] document PYTHON_BASIC_COMPLETER --- Doc/using/cmdline.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index 74c18c2a6ede9c..75983a3e218d33 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -1283,6 +1283,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 complition, instead of the default :mod:`_pyrepl.fancycompleter`. + + .. versionadded:: 3.15 + .. envvar:: PYTHON_HISTORY This environment variable can be used to set the location of a From 13a269807d24c4df0ec7bdca7b04369171c0db16 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 16:10:14 +0200 Subject: [PATCH 36/48] try to manually fix the filename --- ...4.gh-issue- => 2025-09-19-13-54-54.gh-issue-130472.LODfdk.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Misc/NEWS.d/next/Library/{2025-09-19-13-54-54.gh-issue- => 2025-09-19-13-54-54.gh-issue-130472.LODfdk.rst} (100%) diff --git a/Misc/NEWS.d/next/Library/2025-09-19-13-54-54.gh-issue- b/Misc/NEWS.d/next/Library/2025-09-19-13-54-54.gh-issue-130472.LODfdk.rst similarity index 100% rename from Misc/NEWS.d/next/Library/2025-09-19-13-54-54.gh-issue- rename to Misc/NEWS.d/next/Library/2025-09-19-13-54-54.gh-issue-130472.LODfdk.rst From 72f30d996be3e67b1f1724f01d4203f845bb75d1 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 16:41:10 +0200 Subject: [PATCH 37/48] Typo Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --- Doc/using/cmdline.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index 75983a3e218d33..57031607ba4aef 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -1286,7 +1286,7 @@ conflict. .. envvar:: PYTHON_BASIC_COMPLETER If this variable is set to any value, PyREPL will use :mod:`rlcompleter` to - implement tab complition, instead of the default :mod:`_pyrepl.fancycompleter`. + implement tab completion, instead of the default :mod:`_pyrepl.fancycompleter`. .. versionadded:: 3.15 From b1f86aefd4f955c630f8b99093126cc58637447c Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 16:42:11 +0200 Subject: [PATCH 38/48] reword --- Doc/using/cmdline.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index 57031607ba4aef..e89c7f862914af 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -1286,7 +1286,7 @@ conflict. .. 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 :mod:`_pyrepl.fancycompleter`. + implement tab completion, instead of the default one which uses colors. .. versionadded:: 3.15 From af1d74e1580a9ef7cdfe4903c563d5c9fb0cc556 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 18:37:17 +0200 Subject: [PATCH 39/48] force PYTHON_COLORS=1 for tests which expects to see colors. Hopefully this fixes the failures on Android CI --- Lib/test/test_pyrepl/test_fancycompleter.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py index b7271a04ad7dff..517bf814c28175 100644 --- a/Lib/test/test_pyrepl/test_fancycompleter.py +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -1,4 +1,6 @@ import unittest +import os +from unittest.mock import patch from _colorize import ANSIColors, get_theme from _pyrepl.fancycompleter import Completer, commonprefix @@ -57,6 +59,7 @@ class C(object): compl = Completer({'a': None}, use_colors=False) self.assertEqual(compl.attr_matches('a._'), ['a.__']) + @patch.dict(os.environ, {'PYTHON_COLORS': '1'}) def test_complete_attribute_colored(self): theme = get_theme() compl = Completer({'a': 42}, use_colors=True) @@ -93,6 +96,7 @@ def test_complete_global(self): self.assertEqual(compl.global_matches('foobaz'), ['foobazzz']) self.assertEqual(compl.global_matches('nothing'), []) + @patch.dict(os.environ, {'PYTHON_COLORS': '1'}) def test_complete_global_colored(self): compl = Completer({'foobar': 1, 'foobazzz': 2}, use_colors=True) self.assertEqual(compl.global_matches('foo'), ['fooba']) @@ -111,6 +115,7 @@ def test_complete_global_colored(self): self.assertEqual(compl.global_matches('foobaz'), ['foobazzz']) self.assertEqual(compl.global_matches('nothing'), []) + @patch.dict(os.environ, {'PYTHON_COLORS': '1'}) def test_complete_global_colored_exception(self): compl = Completer({'tryme': 42}, use_colors=True) N0 = f"\x1b[000;00m" From c52fd7a4bbe1ea0f84f86a316ad388f2db10c873 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 19:27:30 +0200 Subject: [PATCH 40/48] fix it in a different way: just look in the theme to find the expected color. This is both more robust and more correct --- Lib/test/test_pyrepl/test_fancycompleter.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py index 517bf814c28175..69566acaeaba46 100644 --- a/Lib/test/test_pyrepl/test_fancycompleter.py +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -1,6 +1,5 @@ import unittest import os -from unittest.mock import patch from _colorize import ANSIColors, get_theme from _pyrepl.fancycompleter import Completer, commonprefix @@ -59,7 +58,6 @@ class C(object): compl = Completer({'a': None}, use_colors=False) self.assertEqual(compl.attr_matches('a._'), ['a.__']) - @patch.dict(os.environ, {'PYTHON_COLORS': '1'}) def test_complete_attribute_colored(self): theme = get_theme() compl = Completer({'a': 42}, use_colors=True) @@ -96,8 +94,8 @@ def test_complete_global(self): self.assertEqual(compl.global_matches('foobaz'), ['foobazzz']) self.assertEqual(compl.global_matches('nothing'), []) - @patch.dict(os.environ, {'PYTHON_COLORS': '1'}) def test_complete_global_colored(self): + theme = get_theme() compl = Completer({'foobar': 1, 'foobazzz': 2}, use_colors=True) self.assertEqual(compl.global_matches('foo'), ['fooba']) matches = compl.global_matches('fooba') @@ -106,26 +104,15 @@ def test_complete_global_colored(self): # readline displays the matches in the proper order N0 = f"\x1b[000;00m" N1 = f"\x1b[001;00m" - + int_color = theme.fancycompleter.int self.assertEqual(set(matches), { ' ', - f'{N0}{ANSIColors.BOLD_YELLOW}foobar{ANSIColors.RESET}', - f'{N1}{ANSIColors.BOLD_YELLOW}foobazzz{ANSIColors.RESET}', + f'{N0}{int_color}foobar{ANSIColors.RESET}', + f'{N1}{int_color}foobazzz{ANSIColors.RESET}', }) self.assertEqual(compl.global_matches('foobaz'), ['foobazzz']) self.assertEqual(compl.global_matches('nothing'), []) - @patch.dict(os.environ, {'PYTHON_COLORS': '1'}) - def test_complete_global_colored_exception(self): - compl = Completer({'tryme': 42}, use_colors=True) - N0 = f"\x1b[000;00m" - N1 = f"\x1b[001;00m" - self.assertEqual(compl.global_matches('try'), [ - f'{N0}{ANSIColors.GREY}try:{ANSIColors.RESET}', - f'{N1}{ANSIColors.BOLD_YELLOW}tryme{ANSIColors.RESET}', - ' ' - ]) - def test_complete_with_indexer(self): compl = Completer({'lst': [None, 2, 3]}, use_colors=False) self.assertEqual(compl.attr_matches('lst[0].'), ['lst[0].__']) From fdca77efa21b44cd52b89fc49f03333a94a7d200 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 19 Sep 2025 19:30:34 +0200 Subject: [PATCH 41/48] fix precommit --- Lib/test/test_pyrepl/test_fancycompleter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py index 69566acaeaba46..3deb96c99c79a6 100644 --- a/Lib/test/test_pyrepl/test_fancycompleter.py +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -1,5 +1,4 @@ import unittest -import os from _colorize import ANSIColors, get_theme from _pyrepl.fancycompleter import Completer, commonprefix From 7f9d09cfe5a3551b8b53f1245c32211adad4b90c Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sun, 21 Sep 2025 22:03:06 +0200 Subject: [PATCH 42/48] Update Lib/_pyrepl/fancycompleter.py Co-authored-by: Pieter Eendebak --- Lib/_pyrepl/fancycompleter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index c88a2858c36cfb..b29ebfe5746d50 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -140,7 +140,8 @@ def colorize_matches(self, names, values): 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. - return matches + [' '] + matches.append(' ') + return matches def color_for_obj(self, i, name, value): t = type(value) From 3a6bcd324bb6780d16ffdf5323ec504e016f6137 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Mon, 22 Sep 2025 09:58:07 +0200 Subject: [PATCH 43/48] put _colorize.FancyCompleter in alphabetical order w.r.t. the other sections --- Lib/_colorize.py | 56 ++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 20f504b6bbf069..81b3a618fd858a 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -183,6 +183,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 Syntax(ThemeSection): prompt: str = ANSIColors.BOLD_MAGENTA @@ -219,30 +243,6 @@ class Unittest(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 Theme: """A suite of themes for all sections of Python. @@ -252,20 +252,20 @@ class Theme: """ argparse: Argparse = field(default_factory=Argparse) difflib: Difflib = field(default_factory=Difflib) + fancycompleter: FancyCompleter = field(default_factory=FancyCompleter) syntax: Syntax = field(default_factory=Syntax) traceback: Traceback = field(default_factory=Traceback) unittest: Unittest = field(default_factory=Unittest) - fancycompleter: FancyCompleter = field(default_factory=FancyCompleter) def copy_with( self, *, argparse: Argparse | None = None, difflib: Difflib | None = None, + fancycompleter: FancyCompleter | None = None, syntax: Syntax | None = None, traceback: Traceback | None = None, unittest: Unittest | None = None, - fancycompleter: FancyCompleter | None = None, ) -> Self: """Return a new Theme based on this instance with some sections replaced. @@ -275,10 +275,10 @@ def copy_with( return type(self)( argparse=argparse or self.argparse, difflib=difflib or self.difflib, + fancycompleter=fancycompleter or self.fancycompleter, syntax=syntax or self.syntax, traceback=traceback or self.traceback, unittest=unittest or self.unittest, - fancycompleter=fancycompleter or self.fancycompleter, ) @classmethod @@ -292,10 +292,10 @@ def no_colors(cls) -> Self: return cls( argparse=Argparse.no_colors(), difflib=Difflib.no_colors(), + fancycompleter=FancyCompleter.no_colors(), syntax=Syntax.no_colors(), traceback=Traceback.no_colors(), unittest=Unittest.no_colors(), - fancycompleter=FancyCompleter.no_colors(), ) From 743e661dc291a8e08c86b028de4ed37d5d748e65 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 26 Sep 2025 23:23:57 +0200 Subject: [PATCH 44/48] get_theme() is relatively expensive, fetch it early and cache it --- Lib/_pyrepl/fancycompleter.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index b29ebfe5746d50..27a7534b5784c8 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -33,6 +33,10 @@ def __init__( if self.use_colors: 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('[', '') @@ -152,13 +156,12 @@ def color_for_obj(self, i, name, value): return f"{N}{color}{name}{ANSIColors.RESET}" def color_by_type(self, t): - theme = get_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) + return getattr(self.theme.fancycompleter, typename, ANSIColors.RESET) def commonprefix(names, base=''): From 2ebf50e3bd44d2570d55e3fe68718b2278a372ce Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 26 Sep 2025 23:26:00 +0200 Subject: [PATCH 45/48] base is never used when calling commonprefix, remove it --- Lib/_pyrepl/fancycompleter.py | 6 ++---- Lib/test/test_pyrepl/test_fancycompleter.py | 2 -- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 27a7534b5784c8..46e58768ee0355 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -164,10 +164,8 @@ def color_by_type(self, t): return getattr(self.theme.fancycompleter, typename, 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)] +def commonprefix(names): + """Return the common prefix of all 'names'""" if not names: return '' s1 = min(names) diff --git a/Lib/test/test_pyrepl/test_fancycompleter.py b/Lib/test/test_pyrepl/test_fancycompleter.py index 3deb96c99c79a6..88c5dfd306602f 100644 --- a/Lib/test/test_pyrepl/test_fancycompleter.py +++ b/Lib/test/test_pyrepl/test_fancycompleter.py @@ -29,9 +29,7 @@ def tearDown(self): def test_commonprefix(self): self.assertEqual(commonprefix(['isalpha', 'isdigit', 'foo']), '') self.assertEqual(commonprefix(['isalpha', 'isdigit']), 'is') - self.assertEqual(commonprefix(['isalpha', 'isdigit', 'foo'], base='i'), 'is') self.assertEqual(commonprefix([]), '') - self.assertEqual(commonprefix(['aaa', 'bbb'], base='x'), '') def test_complete_attribute(self): compl = Completer({'a': None}, use_colors=False) From bdf54edf533096b29060888c7d0f05a8572021f2 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 26 Sep 2025 23:38:36 +0200 Subject: [PATCH 46/48] Update Lib/_pyrepl/fancycompleter.py Co-authored-by: Pieter Eendebak --- Lib/_pyrepl/fancycompleter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 46e58768ee0355..ca1e7b45ee8eb2 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -135,7 +135,7 @@ def attr_matches(self, text): return self.colorize_matches(names, values) if prefix: - names += [' '] + names.append(' ') return names def colorize_matches(self, names, values): From 6a5bcfe9ed2e7a10f3484fa4d25a82f143c25e77 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 27 Sep 2025 10:12:13 +0200 Subject: [PATCH 47/48] there is no need to sort words in advance, we can just sort names later --- Lib/_pyrepl/fancycompleter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index ca1e7b45ee8eb2..11ef38273d10c2 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -100,7 +100,6 @@ def attr_matches(self, text): noprefix = '__' else: noprefix = None - words = sorted(words) while True: for word in words: if ( @@ -131,6 +130,7 @@ def attr_matches(self, text): if prefix and prefix != attr: return [f'{expr}.{prefix}'] # autocomplete prefix + names.sort() if self.use_colors: return self.colorize_matches(names, values) From 7d2c790281797769b6cfa5e76f3c16aad2c17ff6 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 27 Sep 2025 22:39:34 +0200 Subject: [PATCH 48/48] undo 6a5bcfe9ed: there IS actually a good reason to sort the words in advance, let's add a comment to explain why --- Lib/_pyrepl/fancycompleter.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/fancycompleter.py b/Lib/_pyrepl/fancycompleter.py index 11ef38273d10c2..c0c2081e66b52b 100644 --- a/Lib/_pyrepl/fancycompleter.py +++ b/Lib/_pyrepl/fancycompleter.py @@ -100,6 +100,12 @@ def attr_matches(self, text): 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 ( @@ -130,7 +136,6 @@ def attr_matches(self, text): if prefix and prefix != attr: return [f'{expr}.{prefix}'] # autocomplete prefix - names.sort() if self.use_colors: return self.colorize_matches(names, values)