8000 Added nested completer. · mxr/python-prompt-toolkit@d39e7f9 · GitHub
[go: up one dir, main page]

Skip to content

Commit d39e7f9

Browse files
Added nested completer.
1 parent 212c72e commit d39e7f9

File tree

5 files changed

+212
-2
lines changed

5 files changed

+212
-2
lines changed

docs/pages/asking_for_input.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,39 @@ completes the last word before the cursor with any of the given words.
276276
in a background thread.
277277

278278

279+
Nested completion
280+
^^^^^^^^^^^^^^^^^
281+
282+
Sometimes you have a command line interface where the completion depends on the
283+
previous words from the input. Examples are the CLIs from routers and switches.
284+
A simple :class:`~prompt_toolkit.completion.WordCompleter` is not enough in
285+
that case. We want to to be able to define completions at multiple hierarchical
286+
levels. :class:`~prompt_toolkit.completion.NestedCompleter` solves this issue:
287+
288+
.. code:: python
289+
290+
from prompt_toolkit import prompt
291+
from prompt_toolkit.completion import NestedCompleter
292+
293+
completer = NestedCompleter.from_nested_dict({
294+
'show': {
295+
'version': None,
296+
'clock': None,
297+
'ip': {
298+
'interface': {'brief'}
299+
}
300+
},
301+
'exit': None,
302+
})
303+
304+
text = prompt('# ', completer=completer)
305+
print('You said: %s' % text)
306+
307+
Whenever there is a ``None`` value in the dictionary, it means that there is no
308+
further nested completion at that point. When all values of a dictionary would
309+
be ``None``, it can also be replaced with a set.
310+
311+
279312
A custom completer
280313
^^^^^^^^^^^^^^^^^^
281314

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env python
2+
"""
3+
Example of nested autocompletion.
4+
"""
5+
from prompt_toolkit import prompt
6+
from prompt_toolkit.completion import NestedCompleter
7+
8+
9+
completer = NestedCompleter.from_nested_dict({
10+
'show': {
11+
'version': None,
12+
'clock': None,
13+
'ip': {
14+
'interface': {
15+
'brief': None
16+
}
17+
}
18+
},
19+
'exit': None,
20+
})
21+
22+
23+
def main():
24+
text = prompt('Type a command: ', completer=completer)
25+
print('You said: %s' % text)
26+
27+
28+
if __name__ == '__main__':
29+
main()

prompt_toolkit/completion/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
)
1111
from .filesystem import ExecutableCompleter, PathCompleter
1212
from .fuzzy_completer import FuzzyCompleter, FuzzyWordCompleter
13+
from .nested import NestedCompleter
1314
from .word_completer import WordCompleter
1415

1516
__all__ = [
@@ -27,8 +28,13 @@
2728
'PathCompleter',
2829
'ExecutableCompleter',
2930

30-
# Word completer.
31-
'WordCompleter',
31+
# Fuzzy
3232
'FuzzyCompleter',
3333
'FuzzyWordCompleter',
34+
35+
# Nested.
36+
'NestedCompleter',
37+
38+
# Word completer.
39+
'WordCompleter',
3440
]

prompt_toolkit/completion/nested.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
Nestedcompleter for completion of hierarchical data structures.
3+
"""
4+
from typing import Dict, Iterable, Mapping, Optional, Set, Union
5+
6+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
7+
from prompt_toolkit.completion.word_completer import WordCompleter
8+
from prompt_toolkit.document import Document
9+
10+
__all__ = [
11+
'NestedCompleter'
12+
]
13+
14+
NestedDict = Mapping[str, Union['NestedDict', Set[str], None, Completer]]
15+
16+
17+
class NestedCompleter(Completer):
18+
"""
19+
Completer which wraps around several other completers, and calls any the
20+
one that corresponds with the first word of the input.
21+
22+
By combining multiple `NestedCompleter` instances, we can achieve multiple
23+
hierarchical levels of autocompletion. This is useful when `WordCompleter`
24+
is not sufficient.
25+
26+
If you need multiple levels, check out the `from_nested_dict` classmethod.
27+
"""
28+
def __init__(self, options: Dict[str, Optional[Completer]],
29+
ignore_case: bool = True) -> None:
30+
31+
self.options = options
32+
self.ignore_case = ignore_case
33+
34+
def __repr__(self) -> str:
35+
return 'NestedCompleter(%r, ignore_case=%r)' % (self.options, self.ignore_case)
36+
37+
@classmethod
38+
def from_nested_dict(cls, data: NestedDict) -> 'NestedCompleter':
39+
"""
40+
Create a `NestedCompleter`, starting from a nested dictionary data
41+
structure, like this:
42+
43+
.. code::
44+
45+
data = {
46+
'show': {
47+
'version': None,
48+
'interfaces': None,
49+
'clock': None,
50+
'ip': {'interface': {'brief'}}
51+
},
52+
'exit': None
53+
'enable': None
54+
}
55+
56+
The value should be `None` if there is no further completion at some
57+
point. If all values in the dictionary are None, it is also possible to
58+
use a set instead.
59+
60+
Values in this data structure can be a completers as well.
61+
"""
62+
options: Dict[str, Optional[Completer]] = {}
63+
for key, value in data.items():
64+
if isinstance(value, Completer):
65+
options[key] = value
66+
elif isinstance(value, dict):
67+
options[key] = cls.from_nested_dict(value)
68+
elif isinstance(value, set):
69+
options[key] = cls.from_nested_dict({item: None for item in value})
70+
else:
71+
assert value is None
72+
options[key] = None
73+
74+
return cls(options)
75+
76+
def get_completions(self, document: Document,
77+
complete_event: CompleteEvent) -> Iterable[Completion]:
78+
# Split document.
79+
text = document.text_before_cursor.lstrip()
80+
81+
# If there is a space, check for the first term, and use a
82+
# subcompleter.
83+
if ' ' in text:
84+
first_term = text.split()[0]
85+
completer = self.options.get(first_term)
86+
87+
# If we have a sub completer, use this for the completions.
88+
if completer is not None:
89+
remaining_text = document.text[len(first_term):].lstrip()
90+
move_cursor = len(document.text) - len(remaining_text)
91+
92+
new_document = Document(
93+
remaining_text,
94+
cursor_position=document.cursor_position - move_cursor)
95+
96+
for c in completer.get_completions(new_document, complete_event):
97+
yield c
98+
99+
# No space in the input: behave exactly like `WordCompleter`.
100+
else:
101+
completer = WordCompleter(list(self.options.keys()), ignore_case=self.ignore_case)
102+
for c in completer.get_completions(document, complete_event):
103+
yield c

tests/test_completion.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from prompt_toolkit.completion import (
1212
CompleteEvent,
1313
FuzzyWordCompleter,
14+
NestedCompleter,
1415
PathCompleter,
1516
WordCompleter,
1617
)
@@ -377,3 +378,41 @@ def test_fuzzy_completer():
377378
# Multiple words. (Check last only.)
378379
completions = completer.get_completions(Document('test txt'), CompleteEvent())
379380
assert [c.text for c in completions] == ['users.txt', 'accounts.txt']
381+
382+
383+
def test_nested_completer():
384+
completer = NestedCompleter.from_nested_dict({
385+
'show': {
386+
'version': None,
387+
'clock': None,
388+
'interfaces': None,
389+
'ip': {
390+
'interface': {'brief'}
391+
}
392+
},
393+
'exit': None,
394+
})
395+
396+
# Empty input.
397+
completions = completer.get_completions(Document(''), CompleteEvent())
398+
assert {c.text for c in completions} == {'show', 'exit'}
399+
400+
# One character.
401+
completions = completer.get_completions(Document('s'), CompleteEvent())
402+
assert {c.text for c in completions} == {'show'}
403+
404+
# One word.
405+
completions = completer.get_completions(Document('show'), CompleteEvent())
406+
assert {c.text for c in completions} == {'show'}
407+
408+
# One word + space.
409+
completions = completer.get_completions(Document('show '), CompleteEvent())
410+
assert {c.text for c in completions} == {'version', 'clock', 'interfaces', 'ip'}
411+
412+
# One word + space + one character.
413+
completions = completer.get_completions(Document('show i'), CompleteEvent())
414+
assert {c.text for c in completions} == {'ip', 'interfaces'}
415+
416+
# Test nested set.
417+
completions = completer.get_completions(Document('show ip interface br'), CompleteEvent())
418+
assert {c.text for c in completions} == {'brief'}

0 commit comments

Comments
 (0)
0