-
-
Notifications
You must be signed in to change notification settings - Fork 34.5k
Expand file tree
/
Copy pathfancycompleter.py
More file actions
205 lines (175 loc) · 6.66 KB
/
fancycompleter.py
File metadata and controls
205 lines (175 loc) · 6.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# Copyright 2010-2025 Antonio Cuni
# Daniel Hahler
#
# All Rights Reserved
"""Colorful tab completion for Python prompt"""
from _colorize import ANSIColors, get_colors, get_theme
import rlcompleter
import keyword
import types
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, values, theme):
return [
_color_for_obj(name, obj, theme)
for name, obj in zip(names, values)
]
def _color_for_obj(name, value, theme):
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
stem so readline-style completion can keep refining the menu as you type.
Optionally, display the various completions in different colors
depending on the type.
"""
def __init__(
self,
namespace=None,
*,
use_colors='auto',
consider_getitems=True,
):
from _pyrepl import readline
rlcompleter.Completer.__init__(self, namespace)
if use_colors == 'auto':
# use colors only if we can
use_colors = get_colors().RED != ""
self.use_colors = use_colors
self.consider_getitems = consider_getitems
if self.use_colors:
# In GNU readline, this prevents escaping of ANSI control
# characters in completion results. pyrepl's parse_and_bind()
# is a no-op, but pyrepl handles ANSI sequences natively
# via real_len()/stripcolor().
readline.parse_and_bind('set dont-escape-ctrl-chars on')
self.theme = get_theme()
else:
self.theme = None
if self.consider_getitems:
delims = readline.get_completer_delims()
delims = delims.replace('[', '')
delims = delims.replace(']', '')
readline.set_completer_delims(delims)
def complete(self, text, state):
# if you press <tab> at the beginning of a line, insert an actual
# \t. Else, trigger completion.
if text == "":
return ('\t', None)[state]
else:
return rlcompleter.Completer.complete(self, text, state)
def _callable_postfix(self, val, word):
# disable automatic insertion of '(' for global callables
return word
def _callable_attr_postfix(self, val, word):
return rlcompleter.Completer._callable_postfix(self, val, word)
def global_matches(self, text):
names = rlcompleter.Completer.global_matches(self, text)
prefix = commonprefix(names)
if prefix and prefix != text:
return [prefix]
names.sort()
values = []
for name in names:
clean_name = name.rstrip(': ')
if keyword.iskeyword(clean_name) or keyword.issoftkeyword(clean_name):
values.append(None)
else:
try:
values.append(eval(name, self.namespace))
except Exception:
values.append(None)
if self.use_colors and names:
return self.colorize_matches(names, values)
return names
def attr_matches(self, text):
try:
expr, attr, names, values = self._attr_matches(text)
except ValueError:
return []
if not names:
return []
if len(names) == 1:
# No coloring: when returning a single completion, readline
# inserts it directly into the prompt, so ANSI codes would
# appear as literal characters.
return [self._callable_attr_postfix(values[0], f'{expr}.{names[0]}')]
prefix = commonprefix(names)
if prefix and prefix != attr:
return [f'{expr}.{prefix}'] # autocomplete prefix
names = [f'{expr}.{name}' for name in names]
if self.use_colors:
return self.colorize_matches(names, values)
return names
def _attr_matches(self, text):
expr, attr = text.rsplit('.', 1)
if '(' in expr or ')' in expr: # don't call functions
return expr, attr, [], []
try:
thisobject = eval(expr, self.namespace)
except Exception:
return expr, attr, [], []
# get the content of the object, except __builtins__
words = set(dir(thisobject)) - {'__builtins__'}
if hasattr(thisobject, '__class__'):
words.add('__class__')
words.update(rlcompleter.get_class_members(thisobject.__class__))
names = []
values = []
n = len(attr)
if attr == '':
noprefix = '_'
elif attr == '_':
noprefix = '__'
else:
noprefix = None
# sort the words now to make sure to return completions in
# alphabetical order. It's easier to do it now, else we would need to
# sort 'names' later but make sure that 'values' in kept in sync,
# which is annoying.
words = sorted(words)
while True:
for word in words:
if (
word[:n] == attr
and not (noprefix and word[:n+1] == noprefix)
):
value = safe_getattr(thisobject, word)
names.append(word)
values.append(value)
if names or not noprefix:
break
if noprefix == '_':
noprefix = '__'
else:
noprefix = None
return expr, attr, names, values
def colorize_matches(self, names, values):
return colorize_matches(names, values, self.theme)
def commonprefix(names):
"""Return the common prefix of all 'names'"""
if not names:
return ''
s1 = min(names)
s2 = max(names)
for i, c in enumerate(s1):
if c != s2[i]:
return s1[:i]
return s1