Skip to content

Commit

Permalink
Merge pull request #144 from robamu-org/git-deps-not-allowed
Browse files Browse the repository at this point in the history
git dependencies are not allowed
  • Loading branch information
robamu authored Nov 29, 2023
2 parents b455b81 + 68c6811 commit 0d1a879
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 5 deletions.
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ dependencies = [
"colorama~=0.4.0",
"colorlog~=6.6",
"cobs~=1.2",
# TODO: Use upstream again when the nested completer PR has been merged.
# "prompt-toolkit~=3.0",
"prompt-toolkit @ git+https://github.com/robamu/python-prompt-toolkit.git@nested-completer-custom-separator-based-on-latest",
"prompt-toolkit~=3.0",
"Deprecated~=1.2",
"pyserial~=3.5",
"dle-encoder~=0.2.3",
Expand Down
125 changes: 123 additions & 2 deletions tmtccmd/config/prompt.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
from __future__ import annotations
import logging
import os
import re
from typing import Optional
from typing import Iterable, Optional

import prompt_toolkit
from deprecated.sphinx import deprecated
from prompt_toolkit.completion import NestedCompleter, WordCompleter
from prompt_toolkit.completion import (
CompleteEvent,
Completer,
Completion,
WordCompleter,
)
from prompt_toolkit.completion.nested import NestedDict
from prompt_toolkit.document import Document
from prompt_toolkit.history import History
from prompt_toolkit.shortcuts import CompleteStyle

Expand Down Expand Up @@ -56,6 +64,119 @@ def prompt_service(
_LOGGER.warning("Invalid key, try again")


# TODO: There is an existing PR to merge this into prompt-toolkit.
# Delete this and use the package class as soon as https://github.com/prompt-toolkit/python-prompt-toolkit/pull/1815
# was merged and a new version was released.
class NestedCompleter(Completer):
"""
Completer which wraps around several other completers, and calls any the
one that corresponds with the first word of the input.
By combining multiple `NestedCompleter` instances, we can achieve multiple
hierarchical levels of autocompletion. This is useful when `WordCompleter`
is not sufficient. The separator to trigger completion on the previously
typed word is the Space character by default, but it is also possible
to set a custom separator.
If you need multiple levels, check out the `from_nested_dict` classmethod.
"""

def __init__(
self,
options: dict[str, Completer | None],
ignore_case: bool = True,
separator: str = " ",
) -> None:
self.options = options
self.ignore_case = ignore_case
self.separator = separator

def __repr__(self) -> str:
return (
f"NestedCompleter({self.options!r}, ignore_case={self.ignore_case!r}, "
f"separator={self.separator!r})"
)

@classmethod
def from_nested_dict(
cls, data: NestedDict, ignore_case: bool = True, separator: str = " "
) -> NestedCompleter:
"""
Create a `NestedCompleter`, starting from a nested dictionary data
structure, like this:
.. code::
data = {
'show': {
'version': None,
'interfaces': None,
'clock': None,
'ip': {'interface': {'brief'}}
},
'exit': None
'enable': None
}
The value should be `None` if there is no further completion at some
point. If all values in the dictionary are None, it is also possible to
use a set instead.
Values in this data structure can be a completers as well.
"""
options: dict[str, Completer | None] = {}
for key, value in data.items():
if isinstance(value, Completer):
options[key] = value
elif isinstance(value, dict):
options[key] = cls.from_nested_dict(
data=value, ignore_case=ignore_case, separator=separator
)
elif isinstance(value, set):
options[key] = cls.from_nested_dict(
data={item: None for item in value},
ignore_case=ignore_case,
separator=separator,
)
else:
assert value is None
options[key] = None

return cls(options=options, ignore_case=ignore_case, separator=separator)

def get_completions(
self, document: Document, complete_event: CompleteEvent
) -> Iterable[Completion]:
# Split document.
text = document.text_before_cursor.lstrip(self.separator)
stripped_len = len(document.text_before_cursor) - len(text)

# If there is a separator character, check for the first term, and use a
# subcompleter.
if self.separator in text:
first_term = text.split(self.separator)[0]
completer = self.options.get(first_term)

# If we have a sub completer, use this for the completions.
if completer is not None:
remaining_text = text[len(first_term) :].lstrip(self.separator)
move_cursor = len(text) - len(remaining_text) + stripped_len

new_document = Document(
remaining_text,
cursor_position=document.cursor_position - move_cursor,
)

yield from completer.get_completions(new_document, complete_event)

# No space in the input: behave exactly like `WordCompleter`.
else:
completer = WordCompleter(
list(self.options.keys()), ignore_case=self.ignore_case
)
yield from completer.get_completions(document, complete_event)


def prompt_cmd_path(
cmd_def_tree: CmdTreeNode,
history: Optional[History] = None,
Expand Down

0 comments on commit 0d1a879

Please sign in to comment.