• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

domdfcoding / consolekit / 20520117960

26 Dec 2025 09:33AM UTC coverage: 94.189% (+0.007%) from 94.182%
20520117960

push

github

domdfcoding
Ignore some blocks from coverage.

778 of 826 relevant lines covered (94.19%)

0.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.36
/consolekit/input.py
1
#!/usr/bin/env python3
2
#
3
#  input.py
4
"""
5
Input functions (prompt, choice etc.).
6
"""
7
#
8
#  Copyright © 2020-2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
9
#
10
#  Permission is hereby granted, free of charge, to any person obtaining a copy
11
#  of this software and associated documentation files (the "Software"), to deal
12
#  in the Software without restriction, including without limitation the rights
13
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
#  copies of the Software, and to permit persons to whom the Software is
15
#  furnished to do so, subject to the following conditions:
16
#
17
#  The above copyright notice and this permission notice shall be included in all
18
#  copies or substantial portions of the Software.
19
#
20
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
24
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
25
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
26
#  OR OTHER DEALINGS IN THE SOFTWARE.
27
#
28
#  prompt and confirm based on https://github.com/pallets/click
29
#  Copyright 2014 Pallets
30
#  |  Redistribution and use in source and binary forms, with or without modification,
31
#  |  are permitted provided that the following conditions are met:
32
#  |
33
#  |      * Redistributions of source code must retain the above copyright notice,
34
#  |        this list of conditions and the following disclaimer.
35
#  |      * Redistributions in binary form must reproduce the above copyright notice,
36
#  |        this list of conditions and the following disclaimer in the documentation
37
#  |        and/or other materials provided with the distribution.
38
#  |      * Neither the name of the copyright holder nor the names of its contributors
39
#  |        may be used to endorse or promote products derived from this software without
40
#  |        specific prior written permission.
41
#  |
42
#  |  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
43
#  |  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
44
#  |  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
45
#  |  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
46
#  |  OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
47
#  |  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
48
#  |  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
49
#  |  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
50
#  |  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
51
#  |  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
52
#  |  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
53
#  |
54
#
55
#  stderr_input based on raw_input from https://foss.heptapod.net/pypy/pypy
56
#  PyPy Copyright holders 2003-2020
57
#  MIT Licenced
58
#
59

60
# stdlib
61
import sys
1✔
62
from typing import IO, Any, Callable, List, Mapping, Optional, Union, overload
1✔
63

64
# 3rd party
65
import click
1✔
66
from click.termui import _build_prompt, hidden_prompt_func
1✔
67
from click.types import Path, convert_type
1✔
68

69
# this package
70
from consolekit import _readline  # noqa: F401
1✔
71
from consolekit._types import _ConvertibleType
1✔
72

73
__all__ = (
1✔
74
                "prompt",
75
                "confirm",
76
                "stderr_input",
77
                "choice",
78
                )
79

80

81
def prompt(  # noqa: MAN002
1✔
82
                text: str,
83
                default: Optional[str] = None,
84
                hide_input: bool = False,
85
                confirmation_prompt: Union[bool, str] = False,
86
                type: Optional[_ConvertibleType] = None,  # noqa: A002  # pylint: disable=redefined-builtin
87
                value_proc: Optional[Callable[[Optional[str]], Any]] = None,
88
                prompt_suffix: str = ": ",
89
                show_default: bool = True,
90
                err: bool = False,
91
                show_choices: bool = True,
92
                ):
93
        """
94
        Prompts a user for input.
95

96
        If the user aborts the input by sending an interrupt signal,
97
        this function will catch it and raise a :exc:`click.Abort` exception.
98

99
        :param text: The text to show for the prompt.
100
        :param default: The default value to use if no input happens.
101
                If this is not given it will prompt until it is aborted.
102
        :param hide_input: If :py:obj:`True` then the input value will be hidden.
103
        :param confirmation_prompt: Asks for confirmation for the value.
104
                Can be set to a string instead of :py:obj:`True` to customize the message.
105
        :param type: The type to check the value against.
106
        :param value_proc: If this parameter is provided it must be a function that
107
                is invoked instead of the type conversion to convert a value.
108
        :param prompt_suffix: A suffix that should be added to the prompt.
109
        :param show_default: Shows or hides the default value in the prompt.
110
        :param err: If :py:obj:`True` the file defaults to ``stderr`` instead of
111
                ``stdout``, the same as with :func:`click.echo`.
112
        :param show_choices: Show or hide choices if the passed type is a :class:`click.Choice`.
113
                For example, if the choice is either ``day`` or ``week``,
114
                ``show_choices`` is :py:obj:`True` and ``text`` is ``'Group by'`` then the
115
                prompt will be ``'Group by (day, week): '``.
116
        """
117

118
        result = None  # noqa
1✔
119

120
        def prompt_func(text: Any) -> Any:
1✔
121
                try:
1✔
122
                        return _prompt(text, err=err, hide_input=hide_input)
1✔
123
                except (KeyboardInterrupt, EOFError):
1✔
124
                        if hide_input:
1✔
125
                                click.echo(None, err=err)
×
126
                        raise click.Abort()
1✔
127

128
        if value_proc is None:
1✔
129
                value_proc = convert_type(type, default)
1✔
130

131
        prompt = _build_prompt(
1✔
132
                        text,
133
                        prompt_suffix,
134
                        show_default,
135
                        default,
136
                        show_choices,
137
                        type,  # type: ignore[arg-type]
138
                        )
139

140
        has_default = default is not None
1✔
141

142
        while True:
143
                while True:
144
                        value = prompt_func(prompt)
1✔
145

146
                        if value:
1✔
147
                                break
1✔
148
                        elif has_default:
1✔
149
                                if isinstance(value_proc, Path):  # pylint: disable=loop-invariant-statement
×
150
                                        # validate Path default value (exists, dir_okay etc.)
151
                                        value = default
×
152
                                        break
×
153
                                return default
×
154

155
                try:  # pylint: disable=loop-try-except-usage
1✔
156
                        result = value_proc(value)
1✔
157
                except click.UsageError as e:
1✔
158
                        click.echo(f"Error: {e.message}", err=err)  # pylint: disable=loop-invariant-statement
1✔
159
                        continue
1✔
160

161
                if not confirmation_prompt:
1✔
162
                        return result
1✔
163

164
                if confirmation_prompt is True:
1✔
165
                        confirmation_prompt = "Repeat for confirmation: "
1✔
166

167
                while True:
168
                        value2 = prompt_func(confirmation_prompt)
1✔
169
                        if value2:
1✔
170
                                break
1✔
171

172
                if value == value2:  # pylint: disable=loop-invariant-statement
1✔
173
                        return result
1✔
174

175
                click.echo("Error: the two entered values do not match", err=err)
1✔
176

177

178
def confirm(  # noqa: MAN002
1✔
179
                text: str,
180
                default: bool = False,
181
                abort: bool = False,
182
                prompt_suffix: str = ": ",
183
                show_default: bool = True,
184
                err: bool = False,
185
                ):
186
        """
187
        Prompts for confirmation (yes/no question).
188

189
        If the user aborts the input by sending a interrupt signal this
190
        function will catch it and raise a :exc:`click.Abort` exception.
191

192
        .. latex:clearpage::
193

194
        :param text: The question to ask.
195
        :param default: The default for the prompt.
196
        :param abort: If :py:obj:`True` a negative answer aborts the exception by raising :exc:`click.Abort`.
197
        :param prompt_suffix: A suffix that should be added to the prompt.
198
        :param show_default: Shows or hides the default value in the prompt.
199
        :param err: If :py:obj:`True` the file defaults to ``stderr`` instead of ``stdout``, the same as with echo.
200
        """
201

202
        prompt = _build_prompt(text, prompt_suffix, show_default, "Y/n" if default else "y/N")
1✔
203

204
        while True:
205
                try:  # pylint: disable=loop-try-except-usage
1✔
206
                        value = _prompt(prompt, err=err, hide_input=False).lower().strip()
1✔
207
                except (KeyboardInterrupt, EOFError):
1✔
208
                        raise click.Abort()
1✔
209

210
                if value in ('y', "yes"):
1✔
211
                        rv = True
1✔
212
                elif value in ('n', "no"):
1✔
213
                        rv = False
1✔
214
                elif value == '':
1✔
215
                        rv = default
1✔
216
                else:
217
                        click.echo("Error: invalid input", err=err)
1✔
218
                        continue
1✔
219
                break
1✔
220

221
        if abort and not rv:
1✔
222
                raise click.Abort()
×
223

224
        return rv
1✔
225

226

227
def stderr_input(prompt: str = '', file: IO = sys.stdout) -> str:  # pragma: no cover
228
        """
229
        Read a string from standard input, but prompt to standard error.
230

231
        The trailing newline is stripped.
232
        If the user hits EOF (Unix: :kbd:`Ctrl-D`, Windows: :kbd:`Ctrl-Z+Return`), raise :exc:`EOFError`.
233

234
        On Unix, GNU readline is used if enabled.
235

236
        :param prompt: If given, is printed to stderr without a trailing newline before reading.
237
        :param file:
238
        """
239

240
        if file is sys.stdout:
241
                return input(prompt)
242

243
        try:
244
                stdin = sys.stdin
245
        except AttributeError:
246
                raise RuntimeError("stderr_input: lost sys.stdin")
247

248
        file.write(prompt)
249

250
        try:
251
                flush = file.flush
252
        except AttributeError:
253
                pass
254
        else:
255
                flush()
256

257
        try:
258
                file.softspace = 0  # type: ignore[attr-defined]
259
        except (AttributeError, TypeError):
260
                pass
261

262
        line = stdin.readline()
263

264
        if not line:  # inputting an empty line gives line == '\n'
265
                raise EOFError
266
        elif line[-1] == '\n':
267
                return line[:-1]
268

269
        return line
270

271

272
def _prompt(text: Any, err: bool, hide_input: bool):  # noqa: MAN002
1✔
273
        if sys.platform != "linux":
1✔
274
                # Write the prompt separately so that we get nice
275
                # coloring through colorama on Windows
276
                click.echo(text, nl=False, err=err)
×
277
                text = ''
×
278

279
        if hide_input:
1✔
280
                return hidden_prompt_func(text)
×
281
        elif err:
1✔
282
                return stderr_input(text, file=sys.stderr)
×
283
        else:
284
                return click.termui.visible_prompt_func(text)
1✔
285

286

287
@overload
1✔
288
def choice(
1✔
289
                options: List[str],
290
                text: str = ...,
291
                default: Optional[str] = ...,
292
                prompt_suffix: str = ...,
293
                show_default: bool = ...,
294
                err: bool = ...,
295
                start_index: int = ...,
296
                ) -> int: ...
297

298

299
@overload
1✔
300
def choice(
1✔
301
                options: Mapping[str, str],
302
                text: str = ...,
303
                default: Optional[str] = ...,
304
                prompt_suffix: str = ...,
305
                show_default: bool = ...,
306
                err: bool = ...,
307
                start_index: int = ...,
308
                ) -> str: ...
309

310

311
def choice(
1✔
312
                options: Union[List[str], Mapping[str, str]],
313
                text: str = '',
314
                default: Optional[str] = None,
315
                prompt_suffix: str = ": ",
316
                show_default: bool = True,
317
                err: bool = False,
318
                start_index: int = 0,
319
                ) -> Union[str, int]:
320
        """
321
        Prompts a user for input.
322

323
        If the user aborts the input by sending an interrupt signal, this
324
        function will catch it and raise a :exc:`click.Abort` exception.
325

326
        :param options:
327
        :param text: The text to show for the prompt.
328
        :param default: The index of the default value to use if the user does not enter anything.
329
                If this is not given it will prompt the user until aborted.
330
        :param prompt_suffix: A suffix that should be added to the prompt.
331
        :param show_default: Shows or hides the default value in the prompt.
332
        :param err: If :py:obj:`True` the file defaults to ``stderr`` instead of
333
                ``stdout``, the same as with echo.
334
        :param start_index: If ``options`` is a list of values, sets the start index.
335
        """
336

337
        # TODO: completer for numbers?
338

339
        type_: click.ParamType
340

341
        if isinstance(options, Mapping):
1✔
342
                # (Y/I/N/O/D/Z) [default=N]
343

344
                text = f"{text} ({'/'.join(options.keys())})"
1✔
345
                type_ = click.STRING
1✔
346

347
                for choice, descripton in options.items():
1✔
348
                        click.echo(f" {choice} : {descripton}")
1✔
349

350
        else:
351
                type_ = click.IntRange(start_index, len(options) + 1 - start_index)
1✔
352

353
                for idx, descripton in enumerate(options):
1✔
354
                        idx += start_index
1✔
355
                        click.echo(f" [{idx}] {descripton}")
1✔
356

357
        if default is not None and show_default:
1✔
358
                text += f" [default={default}]"
1✔
359

360
        while True:
361
                selection = prompt(
1✔
362
                                text=text,
363
                                default=default,
364
                                type=type_,
365
                                prompt_suffix=prompt_suffix,
366
                                show_default=False,
367
                                err=err,
368
                                )
369
                # pylint: disable=loop-invariant-statement
370
                if isinstance(options, Mapping):
1✔
371
                        selection = selection.strip().upper()
1✔
372
                        if selection not in options:
1✔
373
                                click.echo("Please enter a valid option.")
1✔
374
                        else:
375
                                return selection
1✔
376
                else:
377
                        return selection - start_index
1✔
378
                # pylint: enable=loop-invariant-statement
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc