Skip to content

Commit 8e75eb6

Browse files
committed
Colorize import completions
- Make module completer return both names and values (dummy `sys` module in case of module completions) - Colorize completions using `colorize_matches` from FancyCompleter
1 parent 2b05c47 commit 8e75eb6

3 files changed

Lines changed: 62 additions & 23 deletions

File tree

Lib/_pyrepl/_module_completer.py

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from dataclasses import dataclass
1414
from itertools import chain
1515
from tokenize import TokenInfo
16+
from .fancycompleter import safe_getattr
1617

1718
TYPE_CHECKING = False
1819

@@ -71,7 +72,7 @@ def __init__(self, namespace: Mapping[str, Any] | None = None) -> None:
7172
self._curr_sys_path: list[str] = sys.path[:]
7273
self._stdlib_path = os.path.dirname(importlib.__path__[0])
7374

74-
def get_completions(self, line: str) -> tuple[list[str], CompletionAction | None] | None:
75+
def get_completions(self, line: str) -> tuple[list[str], list[Any], CompletionAction | None] | None:
7576
"""Return the next possible import completions for 'line'.
7677
7778
For attributes completion, if the module to complete from is not
@@ -86,26 +87,40 @@ def get_completions(self, line: str) -> tuple[list[str], CompletionAction | None
8687
except Exception:
8788
# Some unexpected error occurred, make it look like
8889
# no completions are available
89-
return [], None
90+
return [], [], None
9091

91-
def complete(self, from_name: str | None, name: str | None) -> tuple[list[str], CompletionAction | None]:
92+
def complete(self, from_name: str | None, name: str | None) -> tuple[list[str], list[Any], CompletionAction | None]:
9293
if from_name is None:
9394
# import x.y.z<tab>
9495
assert name is not None
9596
path, prefix = self.get_path_and_prefix(name)
9697
modules = self.find_modules(path, prefix)
97-
return [self.format_completion(path, module) for module in modules], None
98+
names = [self.format_completion(path, module) for module in modules]
99+
# These are always modules, use dummy values to get the right color
100+
values = [sys] * len(names)
101+
return names, values, None
98102

99103
if name is None:
100104
# from x.y.z<tab>
101105
path, prefix = self.get_path_and_prefix(from_name)
102106
modules = self.find_modules(path, prefix)
103-
return [self.format_completion(path, module) for module in modules], None
107+
names = [self.format_completion(path, module) for module in modules]
108+
# These are always modules, use dummy values to get the right color
109+
values = [sys] * len(names)
110+
return names, values, None
104111

105112
# from x.y import z<tab>
106113
submodules = self.find_modules(from_name, name)
107-
attributes, action = self.find_attributes(from_name, name)
108-
return sorted({*submodules, *attributes}), action
114+
attr_names, attr_values, action = self.find_attributes(from_name, name)
115+
all_names = sorted({*submodules, *attr_names})
116+
# Build values list matching the sorted order:
117+
# submodules use `sys` as a dummy value so they get the 'module' color,
118+
# attributes use their actual value.
119+
submodule_set = set(submodules)
120+
attr_map = dict(zip(attr_names, attr_values))
121+
all_values = [attr_map.get(n) if n not in submodule_set else sys
122+
for n in all_names]
123+
return all_names, all_values, action
109124

110125
def find_modules(self, path: str, prefix: str) -> list[str]:
111126
"""Find all modules under 'path' that start with 'prefix'."""
@@ -166,31 +181,43 @@ def _is_stdlib_module(self, module_info: pkgutil.ModuleInfo) -> bool:
166181
return (isinstance(module_info.module_finder, FileFinder)
167182
and module_info.module_finder.path == self._stdlib_path)
168183

169-
def find_attributes(self, path: str, prefix: str) -> tuple[list[str], CompletionAction | None]:
184+
def find_attributes(self, path: str, prefix: str) -> tuple[list[str], list[Any], CompletionAction | None]:
170185
"""Find all attributes of module 'path' that start with 'prefix'."""
171-
attributes, action = self._find_attributes(path, prefix)
186+
attributes, values, action = self._find_attributes(path, prefix)
172187
# Filter out invalid attribute names
173188
# (for example those containing dashes that cannot be imported with 'import')
174-
return [attr for attr in attributes if attr.isidentifier()], action
175-
176-
def _find_attributes(self, path: str, prefix: str) -> tuple[list[str], CompletionAction | None]:
189+
filtered_names = []
190+
filtered_values = []
191+
for attr, val in zip(attributes, values):
192+
if attr.isidentifier():
193+
filtered_names.append(attr)
194+
filtered_values.append(val)
195+
return filtered_names, filtered_values, action
196+
197+
def _find_attributes(self, path: str, prefix: str) -> tuple[list[str], list[Any], CompletionAction | None]:
177198
path = self._resolve_relative_path(path) # type: ignore[assignment]
178199
if path is None:
179-
return [], None
200+
return [], [], None
180201

181202
imported_module = sys.modules.get(path)
182203
if not imported_module:
183204
if path in self._failed_imports: # Do not propose to import again
184-
return [], None
205+
return [], [], None
185206
imported_module = self._maybe_import_module(path)
186207
if not imported_module:
187-
return [], self._get_import_completion_action(path)
208+
return [], [], self._get_import_completion_action(path)
188209
try:
189210
module_attributes = dir(imported_module)
190211
except Exception:
191212
module_attributes = []
192-
return [attr_name for attr_name in module_attributes
193-
if self.is_suggestion_match(attr_name, prefix)], None
213+
names = []
214+
values = []
215+
for attr_name in module_attributes:
216+
if not self.is_suggestion_match(attr_name, prefix):
217+
continue
218+
names.append(attr_name)
219+
values.append(safe_getattr(imported_module, attr_name))
220+
return names, values, None
194221

195222
def is_suggestion_match(self, module_name: str, prefix: str) -> bool:
196223
if prefix:

Lib/_pyrepl/readline.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
from .completing_reader import CompletingReader, stripcolor
4141
from .console import Console as ConsoleType
4242
from ._module_completer import ModuleCompleter, make_default_module_completer
43-
from .fancycompleter import Completer as FancyCompleter
43+
from .fancycompleter import Completer as FancyCompleter, colorize_matches
4444

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

108109
@dataclass(kw_only=True)
109110
class ReadlineAlikeReader(historical_reader.HistoricalReader, CompletingReader):
@@ -169,8 +170,14 @@ def get_completions(self, stem: str) -> tuple[list[str], CompletionAction | None
169170
return result, None
170171

171172
def get_module_completions(self) -> tuple[list[str], CompletionAction | None] | None:
172-
line = self.get_line()
173-
return self.config.module_completer.get_completions(line)
173+
line = stripcolor(self.get_line())
174+
result = self.config.module_completer.get_completions(line)
175+
if result is None:
176+
return None
177+
names, values, action = result
178+
if len(names) > 1 and self.config.colorize_completions:
179+
names = self.config.colorize_completions(names, values)
180+
return names, action
174181

175182
def get_trimmed_history(self, maxlength: int) -> list[str]:
176183
if maxlength >= 0:
@@ -609,13 +616,18 @@ def _setup(namespace: Mapping[str, Any]) -> None:
609616
# set up namespace in rlcompleter, which requires it to be a bona fide dict
610617
if not isinstance(namespace, dict):
611618
namespace = dict(namespace)
612-
_wrapper.config.module_completer = ModuleCompleter(namespace)
613619
use_basic_completer = (
614620
not sys.flags.ignore_environment
615621
and os.getenv("PYTHON_BASIC_COMPLETER")
616622
)
617623
completer_cls = RLCompleter if use_basic_completer else FancyCompleter
618-
_wrapper.config.readline_completer = completer_cls(namespace).complete
624+
completer = completer_cls(namespace)
625+
_wrapper.config.readline_completer = completer.complete
626+
if getattr(completer, 'use_colors', False):
627+
def _colorize(names: list[str], values: list[object]) -> list[str]:
628+
return colorize_matches(names, values, completer.theme)
629+
_wrapper.config.colorize_completions = _colorize
630+
_wrapper.config.module_completer = ModuleCompleter(namespace)
619631

620632
# this is not really what readline.c does. Better than nothing I guess
621633
import builtins

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1629,7 +1629,7 @@ def test_suggestions_and_messages(self) -> None:
16291629
result = completer.get_completions(code)
16301630
self.assertEqual(result is None, expected is None)
16311631
if result:
1632-
compl, act = result
1632+
compl, _values, act = result
16331633
self.assertEqual(compl, expected[0])
16341634
self.assertEqual(act is None, expected[1] is None)
16351635
if act:

0 commit comments

Comments
 (0)