gh-130472: Use fancycompleter in import completions by tomasr8 · Pull Request #148188 · python/cpython · GitHub
Skip to content
Merged
81 changes: 61 additions & 20 deletions Lib/_pyrepl/_module_completer.py
76 changes: 44 additions & 32 deletions Lib/_pyrepl/fancycompleter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,53 @@
#
# All Rights Reserved
"""Colorful tab completion for Python prompt"""
from __future__ import annotations

from _colorize import ANSIColors, get_colors, get_theme
import rlcompleter
import keyword
import types

TYPE_CHECKING = False

if TYPE_CHECKING:
from typing import Any
from _colorize import Theme


def safe_getattr(obj, name):
# Mirror rlcompleter's safeguards so completion does not
# call properties or reify lazy module attributes.
if isinstance(getattr(type(obj), name, None), property):
return None
if (isinstance(obj, types.ModuleType)
and isinstance(obj.__dict__.get(name), types.LazyImportType)
):
return obj.__dict__.get(name)
return getattr(obj, name, None)


def colorize_matches(names: list[str], values: list[Any], theme: Theme) -> list[str]:
return [
_color_for_obj(name, obj, theme)
for name, obj in zip(names, values)
]

def _color_for_obj(name: str, value: Any, theme: Theme) -> str:
t = type(value)
color = _color_by_type(t, theme)
return f"{color}{name}{ANSIColors.RESET}"


def _color_by_type(t, 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)


class Completer(rlcompleter.Completer):
"""
When doing something like a.b.<tab>, keep the full a.b.attr completion
Expand Down Expand Up @@ -143,21 +185,7 @@ def _attr_matches(self, text):
word[:n] == attr
and not (noprefix and word[:n+1] == noprefix)
):
# Mirror rlcompleter's safeguards so completion does not
# call properties or reify lazy module attributes.
if isinstance(getattr(type(thisobject), word, None), property):
value = None
elif (
isinstance(thisobject, types.ModuleType)
and isinstance(
thisobject.__dict__.get(word),
types.LazyImportType,
)
):
value = thisobject.__dict__.get(word)
else:
value = getattr(thisobject, word, None)

value = safe_getattr(thisobject, word)
names.append(word)
values.append(value)
if names or not noprefix:
Expand All @@ -170,23 +198,7 @@ def _attr_matches(self, text):
return expr, attr, names, values

def colorize_matches(self, names, values):
return [
self._color_for_obj(name, obj)
for name, obj in zip(names, values)
]

def _color_for_obj(self, name, value):
t = type(value)
color = self._color_by_type(t)
return f"{color}{name}{ANSIColors.RESET}"

def _color_by_type(self, t):
typename = t.__name__
# this is needed e.g. to turn method-wrapper into method_wrapper,
# because if we want _colorize.FancyCompleter to be "dataclassable"
# our keys need to be valid identifiers.
typename = typename.replace('-', '_').replace('.', '_')
return getattr(self.theme.fancycompleter, typename, ANSIColors.RESET)
return colorize_matches(names, values, self.theme)


def commonprefix(names):
Expand Down
26 changes: 21 additions & 5 deletions Lib/_pyrepl/readline.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from .completing_reader import CompletingReader, stripcolor
from .console import Console as ConsoleType
from ._module_completer import ModuleCompleter, make_default_module_completer
from .fancycompleter import Completer as FancyCompleter
from .fancycompleter import Completer as FancyCompleter, colorize_matches

Console: type[ConsoleType]
_error: tuple[type[Exception], ...] | type[Exception]
Expand Down Expand Up @@ -104,6 +104,7 @@ class ReadlineConfig:
readline_completer: Completer | None = None
completer_delims: frozenset[str] = frozenset(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?")
module_completer: ModuleCompleter = field(default_factory=make_default_module_completer)
colorize_completions: Callable[[list[str], list[Any]], list[str]] | None = None

@dataclass(kw_only=True)
class ReadlineAlikeReader(historical_reader.HistoricalReader, CompletingReader):
Expand Down Expand Up @@ -169,8 +170,17 @@ def get_completions(self, stem: str) -> tuple[list[str], CompletionAction | None
return result, None

def get_module_completions(self) -> tuple[list[str], CompletionAction | None] | None:
line = self.get_line()
return self.config.module_completer.get_completions(line)
line = stripcolor(self.get_line())
colorize_completions = self.config.colorize_completions
result = self.config.module_completer.get_completions(
line, include_values=bool(colorize_completions)
)
if result is None:
return None
names, values, action = result
if colorize_completions:
names = colorize_completions(names, values)
return names, action

def get_trimmed_history(self, maxlength: int) -> list[str]:
if maxlength >= 0:
Expand Down Expand Up @@ -609,13 +619,19 @@ 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.module_completer = ModuleCompleter(namespace)
use_basic_completer = (
not sys.flags.ignore_environment
and os.getenv("PYTHON_BASIC_COMPLETER")
)
completer_cls = RLCompleter if use_basic_completer else FancyCompleter
_wrapper.config.readline_completer = completer_cls(namespace).complete
completer = completer_cls(namespace)
_wrapper.config.readline_completer = completer.complete
if isinstance(completer, FancyCompleter) and completer.use_colors:
theme = completer.theme
def _colorize(names: list[str], values: list[object]) -> list[str]:
return colorize_matches(names, values, theme)
_wrapper.config.colorize_completions = _colorize
_wrapper.config.module_completer = ModuleCompleter(namespace)

# this is not really what readline.c does. Better than nothing I guess
import builtins
Expand Down
17 changes: 14 additions & 3 deletions Lib/test/test_pyrepl/test_fancycompleter.py
Loading
Loading