File: autocomplete.py

package info (click to toggle)
python-questionary 2.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 960 kB
  • sloc: python: 3,917; makefile: 66
file content (214 lines) | stat: -rw-r--r-- 7,292 bytes parent folder | download | duplicates (3)
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
206
207
208
209
210
211
212
213
214
from typing import Any
from typing import Callable
from typing import Dict
from typing import Iterable
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union

from prompt_toolkit.completion import CompleteEvent
from prompt_toolkit.completion import Completer
from prompt_toolkit.completion import Completion
from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit.lexers import SimpleLexer
from prompt_toolkit.shortcuts.prompt import CompleteStyle
from prompt_toolkit.shortcuts.prompt import PromptSession
from prompt_toolkit.styles import Style

from questionary.constants import DEFAULT_QUESTION_PREFIX
from questionary.prompts.common import build_validator
from questionary.question import Question
from questionary.styles import merge_styles_default


class WordCompleter(Completer):
    choices_source: Union[List[str], Callable[[], List[str]]]
    ignore_case: bool
    meta_information: Dict[str, Any]
    match_middle: bool

    def __init__(
        self,
        choices: Union[List[str], Callable[[], List[str]]],
        ignore_case: bool = True,
        meta_information: Optional[Dict[str, Any]] = None,
        match_middle: bool = True,
    ) -> None:
        self.choices_source = choices
        self.ignore_case = ignore_case
        self.meta_information = meta_information or {}
        self.match_middle = match_middle

    def _choices(self) -> Iterable[str]:
        return (
            self.choices_source()
            if callable(self.choices_source)
            else self.choices_source
        )

    def _choice_matches(self, word_before_cursor: str, choice: str) -> int:
        """Match index if found, -1 if not."""

        if self.ignore_case:
            choice = choice.lower()

        if self.match_middle:
            return choice.find(word_before_cursor)
        elif choice.startswith(word_before_cursor):
            return 0
        else:
            return -1

    @staticmethod
    def _display_for_choice(choice: str, index: int, word_before_cursor: str) -> HTML:
        return HTML("{}<b><u>{}</u></b>{}").format(
            choice[:index],
            choice[index : index + len(word_before_cursor)],  # noqa: E203
            choice[index + len(word_before_cursor) : len(choice)],  # noqa: E203
        )

    def get_completions(
        self, document: Document, complete_event: CompleteEvent
    ) -> Iterable[Completion]:
        choices = self._choices()

        # Get word/text before cursor.
        word_before_cursor = document.text_before_cursor

        if self.ignore_case:
            word_before_cursor = word_before_cursor.lower()

        for choice in choices:
            index = self._choice_matches(word_before_cursor, choice)
            if index == -1:
                # didn't find a match
                continue

            display_meta = self.meta_information.get(choice, "")
            display = self._display_for_choice(choice, index, word_before_cursor)

            yield Completion(
                choice,
                start_position=-len(choice),
                display=display.formatted_text,
                display_meta=display_meta,
                style="class:answer",
                selected_style="class:selected",
            )


def autocomplete(
    message: str,
    choices: List[str],
    default: str = "",
    qmark: str = DEFAULT_QUESTION_PREFIX,
    completer: Optional[Completer] = None,
    meta_information: Optional[Dict[str, Any]] = None,
    ignore_case: bool = True,
    match_middle: bool = True,
    complete_style: CompleteStyle = CompleteStyle.COLUMN,
    validate: Any = None,
    style: Optional[Style] = None,
    **kwargs: Any,
) -> Question:
    """Prompt the user to enter a message with autocomplete help.

    Example:
        >>> import questionary
        >>> questionary.autocomplete(
        ...    'Choose ant species',
        ...    choices=[
        ...         'Camponotus pennsylvanicus',
        ...         'Linepithema humile',
        ...         'Eciton burchellii',
        ...         "Atta colombica",
        ...         'Polyergus lucidus',
        ...         'Polyergus rufescens',
        ...    ]).ask()
        ? Choose ant species Atta colombica
        'Atta colombica'

    .. image:: ../images/autocomplete.gif

    This is just a really basic example, the prompt can be customised using the
    parameters.


    Args:
        message: Question text

        choices: Items shown in the selection, this contains items as strings

        default: Default return value (single value).

        qmark: Question prefix displayed in front of the question.
               By default this is a ``?``

        completer: A prompt_toolkit :class:`prompt_toolkit.completion.Completion`
                   implementation. If not set, a questionary completer implementation
                   will be used.

        meta_information: A dictionary with information/anything about choices.

        ignore_case: If true autocomplete would ignore case.

        match_middle: If true autocomplete would search in every string position
                      not only in string begin.

        complete_style: How autocomplete menu would be shown, it could be ``COLUMN``
                        ``MULTI_COLUMN`` or ``READLINE_LIKE`` from
                        :class:`prompt_toolkit.shortcuts.CompleteStyle`.

        validate: Require the entered value to pass a validation. The
                  value can not be submitted until the validator accepts
                  it (e.g. to check minimum password length).

                  This can either be a function accepting the input and
                  returning a boolean, or an class reference to a
                  subclass of the prompt toolkit Validator class.

        style: A custom color and style for the question parts. You can
               configure colors as well as font types for different elements.

    Returns:
        :class:`Question`: Question instance, ready to be prompted (using ``.ask()``).
    """
    merged_style = merge_styles_default([style])

    def get_prompt_tokens() -> List[Tuple[str, str]]:
        return [("class:qmark", qmark), ("class:question", " {} ".format(message))]

    def get_meta_style(meta: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
        if meta:
            for key in meta:
                meta[key] = HTML("<text>{}</text>").format(meta[key])

        return meta

    validator = build_validator(validate)

    if completer is None:
        if not choices:
            raise ValueError("No choices is given, you should use Text question.")
        # use the default completer
        completer = WordCompleter(
            choices,
            ignore_case=ignore_case,
            meta_information=get_meta_style(meta_information),
            match_middle=match_middle,
        )

    p: PromptSession = PromptSession(
        get_prompt_tokens,
        lexer=SimpleLexer("class:answer"),
        style=merged_style,
        completer=completer,
        validator=validator,
        complete_style=complete_style,
        **kwargs,
    )
    p.default_buffer.reset(Document(default))

    return Question(p.app)