Coverage for src\derivepassphrase\_internals\cli_machinery.py: 100.000%
339 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-23 12:17 +0200
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-23 12:17 +0200
1# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>
2#
3# SPDX-License-Identifier: Zlib
6"""Command-line machinery for derivepassphrase.
8Warning:
9 Non-public module (implementation detail), provided for didactical and
10 educational purposes only. Subject to change without notice, including
11 removal.
13"""
15from __future__ import annotations
17import collections
18import importlib.metadata
19import inspect
20import logging
21import socket
22import warnings
23from typing import TYPE_CHECKING, Callable, Literal, TextIO, TypeVar
25import click
26import click.shell_completion
27from typing_extensions import Any, ParamSpec, override
29from derivepassphrase import _internals, _types
30from derivepassphrase._internals import cli_messages as _msg
32if TYPE_CHECKING:
33 import types
34 from collections.abc import (
35 MutableSequence,
36 )
38 from typing_extensions import Self
40PROG_NAME = _internals.PROG_NAME
41VERSION = _internals.VERSION
42VERSION_OUTPUT_WRAPPING_WIDTH = 72
44# Error messages
45NOT_AN_INTEGER = 'not an integer'
46NOT_A_NONNEGATIVE_INTEGER = 'not a non-negative integer'
47NOT_A_POSITIVE_INTEGER = 'not a positive integer'
50# Logging
51# =======
54class ClickEchoStderrHandler(logging.Handler):
55 """A [`logging.Handler`][] for `click` applications.
57 Outputs log messages to [`sys.stderr`][] via [`click.echo`][].
59 """
61 def emit(self, record: logging.LogRecord) -> None:
62 """Emit a log record.
64 Format the log record, then emit it via [`click.echo`][] to
65 [`sys.stderr`][].
67 """
68 click.echo( 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4
69 self.format(record),
70 err=True,
71 color=getattr(record, 'color', None),
72 )
75class CLIofPackageFormatter(logging.Formatter):
76 """A [`logging.LogRecord`][] formatter for the CLI of a Python package.
78 Assuming a package `PKG` and loggers within the same hierarchy
79 `PKG`, format all log records from that hierarchy for proper user
80 feedback on the console. Intended for use with [`click`][CLICK] and
81 when `PKG` provides a command-line tool `PKG` and when logs from
82 that package should show up as output of the command-line tool.
84 Essentially, this prepends certain short strings to the log message
85 lines to make them readable as standard error output.
87 Because this log output is intended to be displayed on standard
88 error as high-level diagnostic output, you are strongly discouraged
89 from changing the output format to include more tokens besides the
90 log message. Use a dedicated log file handler instead, without this
91 formatter.
93 [CLICK]: https://pypi.org/projects/click/
95 """
97 def __init__(
98 self,
99 *,
100 prog_name: str = PROG_NAME,
101 package_name: str | None = None,
102 ) -> None:
103 self.prog_name = prog_name
104 self.package_name = (
105 package_name
106 if package_name is not None
107 else prog_name.lower().replace(' ', '_').replace('-', '_')
108 )
110 def format(self, record: logging.LogRecord) -> str:
111 """Format a log record suitably for standard error console output.
113 Prepend the formatted string `"PROG_NAME: LABEL"` to each line
114 of the message, where `PROG_NAME` is the program name, and
115 `LABEL` depends on the record's level and on the logger name as
116 follows:
118 * For records at level [`logging.DEBUG`][], `LABEL` is
119 `"Debug: "`.
120 * For records at level [`logging.INFO`][], `LABEL` is the
121 empty string.
122 * For records at level [`logging.WARNING`][], `LABEL` is
123 `"Deprecation warning: "` if the logger is named
124 `PKG.deprecation` (where `PKG` is the package name), else
125 `"Warning: "`.
126 * For records at level [`logging.ERROR`][] and
127 [`logging.CRITICAL`][] `"Error: "`, `LABEL` is the empty
128 string.
130 The level indication strings at level `WARNING` or above are
131 highlighted. Use [`click.echo`][] to output them and remove
132 color output if necessary.
134 Args:
135 record: A log record.
137 Returns:
138 A formatted log record.
140 Raises:
141 AssertionError:
142 The log level is not supported.
144 """
145 preliminary_result = record.getMessage() 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4
146 prefix = f'{self.prog_name}: ' 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4
147 if record.levelname == 'DEBUG': # pragma: no cover 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4
148 # Defensive programming, and currently not in production
149 # use, so no coverage.
150 level_indicator = 'Debug: '
151 elif record.levelname == 'INFO': 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4
152 level_indicator = '' 1T
153 elif record.levelname == 'WARNING': 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4
154 level_indicator = ( 2q W s X P F z j v T Y eb; l m Z n 0 1 2 3 4
155 f'{click.style("Deprecation warning", bold=True)}: '
156 if record.name.endswith('.deprecation')
157 else f'{click.style("Warning", bold=True)}: '
158 )
159 elif record.levelname in {'ERROR', 'CRITICAL'}: 1GHIAJKLtMurNBCODQRESFUV
160 level_indicator = '' 1GHIAJKLtMurNBCODQRESFUV
161 else: # pragma: no cover
162 # Defensive programming, so no coverage.
163 msg = f'Unsupported logging level: {record.levelname}'
164 raise AssertionError(msg)
165 parts = [ 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4
166 ''.join(
167 prefix + level_indicator + line
168 for line in preliminary_result.splitlines(True) # noqa: FBT003
169 )
170 ]
171 if record.exc_info: 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4
172 parts.append(self.formatException(record.exc_info) + '\n') 1ArBCDE
173 return ''.join(parts) 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4
176class StandardCLILogging:
177 """Set up CLI logging handlers upon instantiation."""
179 prog_name = PROG_NAME
180 package_name = PROG_NAME.lower().replace(' ', '_').replace('-', '_')
181 cli_formatter = CLIofPackageFormatter(
182 prog_name=prog_name, package_name=package_name
183 )
184 cli_handler = ClickEchoStderrHandler()
185 cli_handler.addFilter(logging.Filter(name=package_name))
186 cli_handler.setFormatter(cli_formatter)
187 cli_handler.setLevel(logging.WARNING)
188 warnings_handler = ClickEchoStderrHandler()
189 warnings_handler.addFilter(logging.Filter(name='py.warnings'))
190 warnings_handler.setFormatter(cli_formatter)
191 warnings_handler.setLevel(logging.WARNING)
193 @classmethod
194 def ensure_standard_logging(cls) -> StandardLoggingContextManager:
195 """Return a context manager to ensure standard logging is set up."""
196 return StandardLoggingContextManager( 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ebab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
197 handler=cls.cli_handler,
198 root_logger=cls.package_name,
199 )
201 @classmethod
202 def ensure_standard_warnings_logging(
203 cls,
204 ) -> StandardWarningsLoggingContextManager:
205 """Return a context manager to ensure warnings logging is set up."""
206 return StandardWarningsLoggingContextManager( 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
207 handler=cls.warnings_handler,
208 )
211class StandardLoggingContextManager:
212 """A reentrant context manager setting up standard CLI logging.
214 Ensures that the given handler (defaulting to the CLI logging
215 handler) is added to the named logger (defaulting to the root
216 logger), and if it had to be added, then that it will be removed
217 upon exiting the context.
219 Reentrant, but not thread safe, because it temporarily modifies
220 global state.
222 """
224 def __init__(
225 self,
226 handler: logging.Handler,
227 root_logger: str | None = None,
228 ) -> None:
229 self.handler = handler 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
230 self.root_logger_name = root_logger 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
231 self.base_logger = logging.getLogger(self.root_logger_name) 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
232 self.action_required: MutableSequence[bool] = collections.deque() 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
234 def __enter__(self) -> Self:
235 self.action_required.append( 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
236 self.handler not in self.base_logger.handlers
237 )
238 if self.action_required[-1]: 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
239 self.base_logger.addHandler(self.handler) 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
240 return self 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
242 def __exit__(
243 self,
244 exc_type: type[BaseException] | None,
245 exc_value: BaseException | None,
246 exc_tb: types.TracebackType | None,
247 ) -> Literal[False]:
248 if self.action_required[-1]: 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
249 self.base_logger.removeHandler(self.handler) 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
250 self.action_required.pop() 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
251 return False 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
254class StandardWarningsLoggingContextManager(StandardLoggingContextManager):
255 """A reentrant context manager setting up standard warnings logging.
257 Ensures that warnings are being diverted to the logging system, and
258 that the given handler (defaulting to the CLI logging handler) is
259 added to the warnings logger. If the handler had to be added, then
260 it will be removed upon exiting the context.
262 Reentrant, but not thread safe, because it temporarily modifies
263 global state.
265 """
267 def __init__(
268 self,
269 handler: logging.Handler,
270 ) -> None:
271 super().__init__(handler=handler, root_logger='py.warnings') 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
272 self.stack: MutableSequence[ 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
273 tuple[
274 Callable[
275 [
276 type[BaseException] | None,
277 BaseException | None,
278 types.TracebackType | None,
279 ],
280 None,
281 ],
282 Callable[
283 [
284 str | Warning,
285 type[Warning],
286 str,
287 int,
288 TextIO | None,
289 str | None,
290 ],
291 None,
292 ],
293 ]
294 ] = collections.deque()
296 def __enter__(self) -> Self:
297 def showwarning( # noqa: PLR0913,PLR0917 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
298 message: str | Warning,
299 category: type[Warning],
300 filename: str,
301 lineno: int,
302 file: TextIO | None = None,
303 line: str | None = None,
304 ) -> None:
305 if file is not None: # pragma: no cover 1;
306 # Defensive programming (API defined by external source,
307 # and this code path is not triggered by our code), so
308 # no coverage.
309 self.stack[0][1](
310 message, category, filename, lineno, file, line
311 )
312 else:
313 logging.getLogger('py.warnings').warning( 1;
314 str(
315 warnings.formatwarning(
316 message, category, filename, lineno, line
317 )
318 )
319 )
321 ctx = warnings.catch_warnings() 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
322 exit_func = ctx.__exit__ 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
323 ctx.__enter__() 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
324 self.stack.append((exit_func, warnings.showwarning)) 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
325 warnings.showwarning = showwarning 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
326 return super().__enter__() 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
328 def __exit__(
329 self,
330 exc_type: type[BaseException] | None,
331 exc_value: BaseException | None,
332 exc_tb: types.TracebackType | None,
333 ) -> Literal[False]:
334 ret = super().__exit__(exc_type, exc_value, exc_tb) 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
335 val = self.stack.pop()[0](exc_type, exc_value, exc_tb) 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
336 assert not val 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
337 return ret 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4
340P = ParamSpec('P')
341R = TypeVar('R')
344def adjust_logging_level(
345 ctx: click.Context,
346 /,
347 param: click.Parameter | None = None,
348 value: int | None = None,
349) -> None:
350 """Change the logs that are emitted to standard error.
352 This modifies the [`StandardCLILogging`][] settings such that log
353 records at the respective level are emitted, based on the `param`
354 and the `value`.
356 """
357 # Note: If multiple options use this callback, then we will be
358 # called multiple times. Ensure the runs are idempotent.
359 if param is None or value is None or ctx.resilient_parsing: 2b 5 c f e a q i h k ! # ( ) * + W x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbob
360 return 2b 5 c f e a q i h k ! # ( ) * + W x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbob
361 StandardCLILogging.cli_handler.setLevel(value) 1P
362 logging.getLogger(StandardCLILogging.package_name).setLevel(value) 1P
365# Option parsing and grouping
366# ===========================
369class OptionGroupOption(click.Option):
370 """A [`click.Option`][] with an associated group name and group epilog.
372 Used by [`CommandWithHelpGroups`][] to print help sections. Each
373 subclass contains its own group name and epilog.
375 Attributes:
376 option_group_name:
377 The name of the option group. Used as a heading on the help
378 text for options in this section.
379 epilog:
380 An epilog to print after listing the options in this
381 section.
383 """
385 option_group_name: object = ''
386 """"""
387 epilog: object = ''
388 """"""
390 def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401
391 if self.__class__ == __class__: # type: ignore[name-defined] 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob
392 raise NotImplementedError
393 # Though click 8.1 mostly defers help text processing until the
394 # `BaseCommand.format_*` methods are called, the Option
395 # constructor still preprocesses the help text, and asserts that
396 # the help text is a string. Work around this by removing the
397 # help text from the constructor arguments and re-adding it,
398 # unprocessed, after constructor finishes.
399 unset = object() 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob
400 help = kwargs.pop('help', unset) # noqa: A001 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob
401 super().__init__(*args, **kwargs) 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob
402 if help is not unset: # pragma: no branch 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob
403 self.help = help 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob
406class StandardOption(OptionGroupOption):
407 pass
410# Portions of this class are based directly on code from click 8.1.
411# (This does not in general include docstrings, unless otherwise noted.)
412# They are subject to the 3-clause BSD license in the following
413# paragraphs. Modifications to their code are marked with respective
414# comments; they too are released under the same license below. The
415# original code did not contain any "noqa" or "pragma" comments.
416#
417# Copyright 2024 Pallets
418#
419# Redistribution and use in source and binary forms, with or
420# without modification, are permitted provided that the
421# following conditions are met:
422#
423# 1. Redistributions of source code must retain the above
424# copyright notice, this list of conditions and the
425# following disclaimer.
426#
427# 2. Redistributions in binary form must reproduce the above
428# copyright notice, this list of conditions and the
429# following disclaimer in the documentation and/or other
430# materials provided with the distribution.
431#
432# 3. Neither the name of the copyright holder nor the names
433# of its contributors may be used to endorse or promote
434# products derived from this software without specific
435# prior written permission.
436#
437# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
438# CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES,
439# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
440# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
441# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
442# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
443# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
444# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
445# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
446# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
447# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
448# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
449# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
450class CommandWithHelpGroups(click.Command):
451 """A [`click.Command`][] with support for some help text customizations.
453 Supports help/option groups, group epilogs, and help text objects
454 (objects that stringify to help texts). The latter is primarily
455 used to implement translations.
457 Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE] for
458 help/option group support, and further modified to include group
459 epilogs and help text objects.
461 [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
463 """
465 @staticmethod
466 def _text(text: object, /) -> str:
467 if isinstance(text, (list, tuple)): 1dcfeag]
468 return '\n\n'.join(str(x) for x in text) 1dcfeag]
469 return str(text) 1dcfeag
471 # This method is based on click 8.1; see the comment above the class
472 # declaration for license details.
473 def collect_usage_pieces(self, ctx: click.Context) -> list[str]:
474 """Return the pieces for the usage string.
476 Args:
477 ctx:
478 The click context.
480 """
481 rv = [str(self.options_metavar)] if self.options_metavar else [] 2d c f e a g w x y t 7 % ' z v fb
482 for param in self.get_params(ctx): 2d c f e a g w x y t 7 % ' z v fb
483 rv.extend(str(x) for x in param.get_usage_pieces(ctx)) 2d c f e a g w x y t 7 % ' z v fb
484 return rv 2d c f e a g w x y t 7 % ' z v fb
486 # This method is based on click 8.1; see the comment above the class
487 # declaration for license details.
488 def get_help_option(
489 self,
490 ctx: click.Context,
491 ) -> click.Option | None:
492 """Return a standard help option object.
494 Args:
495 ctx:
496 The click context.
498 """
499 help_options = self.get_help_option_names(ctx) 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob
501 if not help_options or not self.add_help_option: # pragma: no cover 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob
502 # Defensive programming (API defined by external source, and
503 # this code path is not triggered by our code), so no
504 # coverage.
505 return None
507 def show_help( 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob
508 ctx: click.Context,
509 param: click.Parameter, # noqa: ARG001
510 value: str,
511 ) -> None:
512 if value and not ctx.resilient_parsing: 2b 5 d c f e a q i h k g ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob
513 click.echo(ctx.get_help(), color=ctx.color) 1dcfeag
514 ctx.exit() 1dcfeag
516 # Modified from click 8.1: We use StandardOption and a non-str
517 # object as the help string.
518 return StandardOption( 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob
519 help_options,
520 is_flag=True,
521 is_eager=True,
522 expose_value=False,
523 callback=show_help,
524 help=_msg.TranslatedString(_msg.Label.HELP_OPTION_HELP_TEXT),
525 )
527 # This method is based on click 8.1; see the comment above the class
528 # declaration for license details.
529 def get_short_help_str(
530 self,
531 limit: int = 45,
532 ) -> str:
533 """Return the short help string for a command.
535 If only a long help string is given, shorten it.
537 Args:
538 limit:
539 The maximum width of the short help string.
541 """
542 # Modification against click 8.1: Call `_text()` on `self.help`
543 # to allow help texts to be general objects, not just strings.
544 # Used to implement translatable strings, as objects that
545 # stringify to the translation.
546 if self.short_help: # pragma: no cover 1dca]
547 # Defensive programming (API defined by external source, and
548 # this code path is not triggered by our code), so no
549 # coverage.
550 text = inspect.cleandoc(self._text(self.short_help))
551 elif self.help: 1dca]
552 text = click.utils.make_default_short_help( 1dca]
553 self._text(self.help), limit
554 )
555 else: # pragma: no cover
556 # Defensive programming (API defined by external source, and
557 # this code path is not triggered by our code), so no
558 # coverage.
559 text = ''
560 if self.deprecated: # pragma: no cover 1dca]
561 # Defensive programming (API defined by external source, and
562 # this code path is not triggered by our code), so no
563 # coverage.
564 #
565 # Modification against click 8.1: The translated string is
566 # looked up in the derivepassphrase message domain, not the
567 # gettext default domain.
568 text = str(
569 _msg.TranslatedString(_msg.Label.DEPRECATED_COMMAND_LABEL)
570 ).format(text=text)
571 return text.strip() 1dca]
573 # This method is based on click 8.1; see the comment above the class
574 # declaration for license details.
575 def format_help_text(
576 self,
577 ctx: click.Context,
578 formatter: click.HelpFormatter,
579 ) -> None:
580 """Format the help text prologue, if any.
582 Args:
583 ctx:
584 The click context.
585 formatter:
586 The formatter for the `--help` listing.
588 """
589 del ctx 1dcfeag
590 # Modification against click 8.1: Call `_text()` on `self.help`
591 # to allow help texts to be general objects, not just strings.
592 # Used to implement translatable strings, as objects that
593 # stringify to the translation.
594 text = ( 1dcfeag
595 inspect.cleandoc(self._text(self.help).partition('\f')[0])
596 if self.help is not None
597 else ''
598 )
599 if self.deprecated: # pragma: no cover 1dcfeag
600 # Defensive programming (API defined by external source, and
601 # this code path is not triggered by our code), so no
602 # coverage.
603 #
604 # Modification against click 8.1: The translated string is
605 # looked up in the derivepassphrase message domain, not the
606 # gettext default domain.
607 text = str(
608 _msg.TranslatedString(_msg.Label.DEPRECATED_COMMAND_LABEL)
609 ).format(text=text)
610 if text: # pragma: no branch 1dcfeag
611 formatter.write_paragraph() 1dcfeag
612 with formatter.indentation(): 1dcfeag
613 formatter.write_text(text) 1dcfeag
615 # This method is based on click 8.1; see the comment above the class
616 # declaration for license details. Consider the whole section
617 # marked as modified; the code modifications are too numerous to
618 # mark individually.
619 def format_options(
620 self,
621 ctx: click.Context,
622 formatter: click.HelpFormatter,
623 ) -> None:
624 r"""Format options on the help listing, grouped into sections.
626 This is a callback for [`click.Command.get_help`][] that
627 implements the `--help` listing, by calling appropriate methods
628 of the `formatter`. We list all options (like the base
629 implementation), but grouped into sections according to the
630 concrete [`click.Option`][] subclass being used. If the option
631 is an instance of some subclass of [`OptionGroupOption`][], then
632 the section heading and the epilog are taken from the
633 [`option_group_name`] [OptionGroupOption.option_group_name] and
634 [`epilog`] [OptionGroupOption.epilog] attributes; otherwise, the
635 section heading is "Options" (or "Other options" if there are
636 other option groups) and the epilog is empty.
638 We unconditionally call [`format_commands`][], and rely on it to
639 act as a no-op if we aren't actually a [`click.MultiCommand`][].
641 Args:
642 ctx:
643 The click context.
644 formatter:
645 The formatter for the `--help` listing.
647 """
648 default_group_name = '' 1dcfeag
649 help_records: dict[str, list[tuple[str, str]]] = {} 1dcfeag
650 epilogs: dict[str, str] = {} 1dcfeag
651 params = self.params[:] 1dcfeag
652 if ( # pragma: no branch 1dcfeag
653 (help_opt := self.get_help_option(ctx)) is not None
654 and help_opt not in params
655 ):
656 params.append(help_opt) 1dcfeag
657 for param in params: 1dcfeag
658 rec = param.get_help_record(ctx) 1dcfeag
659 if rec is not None: 1dcfeag
660 rec = (rec[0], self._text(rec[1])) 1dcfeag
661 if isinstance(param, OptionGroupOption): 1dcfeag
662 group_name = self._text(param.option_group_name) 1dcfeag
663 epilogs.setdefault(group_name, self._text(param.epilog)) 1dcfeag
664 else: # pragma: no cover
665 # Defensive programming (API defined by external
666 # source, and this code path is not triggered by our
667 # code), so no coverage.
668 group_name = default_group_name
669 help_records.setdefault(group_name, []).append(rec) 1dcfeag
670 if default_group_name in help_records: # pragma: no branch 1dcfeag
671 default_group = help_records.pop(default_group_name) 1dcfeag
672 default_group_label = ( 1dcfeag
673 _msg.Label.OTHER_OPTIONS_LABEL
674 if len(default_group) > 1
675 else _msg.Label.OPTIONS_LABEL
676 )
677 default_group_name = self._text( 1dcfeag
678 _msg.TranslatedString(default_group_label)
679 )
680 help_records[default_group_name] = default_group 1dcfeag
681 for group_name, records in help_records.items(): 1dcfeag
682 with formatter.section(group_name): 1dcfeag
683 formatter.write_dl(records) 1dcfeag
684 epilog = inspect.cleandoc(epilogs.get(group_name, '')) 1dcfeag
685 if epilog: 1dcfeag
686 formatter.write_paragraph() 1eag
687 with formatter.indentation(): 1eag
688 formatter.write_text(epilog) 1eag
689 self.format_commands(ctx, formatter) 1dcfeag
691 # This method is based on click 8.1; see the comment above the class
692 # declaration for license details. Consider the whole section
693 # marked as modified; the code modifications are too numerous to
694 # mark individually.
695 def format_commands(
696 self,
697 ctx: click.Context,
698 formatter: click.HelpFormatter,
699 ) -> None:
700 """Format the subcommands, if any.
702 If called on a command object that isn't derived from
703 [`click.Group`][], then do nothing.
705 Args:
706 ctx:
707 The click context.
708 formatter:
709 The formatter for the `--help` listing.
711 """
712 if not isinstance(self, click.Group): 1dcfeag
713 return 1feag
714 commands: list[tuple[str, click.Command]] = [] 1dca
715 for subcommand in self.list_commands(ctx): 1dca
716 cmd = self.get_command(ctx, subcommand) 1dca
717 if cmd is None or cmd.hidden: # pragma: no cover 1dca
718 # Defensive programming (API defined by external source,
719 # and this code path is not triggered by our code), so
720 # no coverage.
721 continue
722 commands.append((subcommand, cmd)) 1dca
723 if commands: # pragma: no branch 1dca
724 longest_command = max((cmd[0] for cmd in commands), key=len) 1dca
725 limit = formatter.width - 6 - len(longest_command) 1dca
726 rows: list[tuple[str, str]] = [] 1dca
727 for subcommand, cmd in commands: 1dca
728 help_str = self._text(cmd.get_short_help_str(limit) or '') 1dca
729 rows.append((subcommand, help_str)) 1dca
730 if rows: # pragma: no branch 1dca
731 commands_label = self._text( 1dca
732 _msg.TranslatedString(_msg.Label.COMMANDS_LABEL)
733 )
734 with formatter.section(commands_label): 1dca
735 formatter.write_dl(rows) 1dca
737 # This method is based on click 8.1; see the comment above the class
738 # declaration for license details.
739 def format_epilog(
740 self,
741 ctx: click.Context,
742 formatter: click.HelpFormatter,
743 ) -> None:
744 """Format the epilog, if any.
746 Args:
747 ctx:
748 The click context.
749 formatter:
750 The formatter for the `--help` listing.
752 """
753 del ctx 1dcfeag
754 if self.epilog: # pragma: no branch 1dcfeag
755 # Modification against click 8.1: Call `str()` on
756 # `self.epilog` to allow help texts to be general objects,
757 # not just strings. Used to implement translatable strings,
758 # as objects that stringify to the translation.
759 epilog = inspect.cleandoc(self._text(self.epilog)) 1deag
760 formatter.write_paragraph() 1deag
761 with formatter.indentation(): 1deag
762 formatter.write_text(epilog) 1deag
765# Portions of this class are based directly on code from click 8.1.
766# (This does not in general include docstrings, unless otherwise noted.)
767# They are subject to the 3-clause BSD license in the following
768# paragraphs. Modifications to their code are marked with respective
769# comments; they too are released under the same license below. The
770# original code did not contain any "noqa" or "pragma" comments.
771#
772# Copyright 2024 Pallets
773#
774# Redistribution and use in source and binary forms, with or
775# without modification, are permitted provided that the
776# following conditions are met:
777#
778# 1. Redistributions of source code must retain the above
779# copyright notice, this list of conditions and the
780# following disclaimer.
781#
782# 2. Redistributions in binary form must reproduce the above
783# copyright notice, this list of conditions and the
784# following disclaimer in the documentation and/or other
785# materials provided with the distribution.
786#
787# 3. Neither the name of the copyright holder nor the names
788# of its contributors may be used to endorse or promote
789# products derived from this software without specific
790# prior written permission.
791#
792# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
793# CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES,
794# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
795# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
796# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
797# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
798# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
799# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
800# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
801# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
802# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
803# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
804# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
805#
806# TODO(the-13th-letter): Remove this class and license block in v1.0.
807# https://the13thletter.info/derivepassphrase/latest/upgrade-notes/#v1.0-implied-subcommands
808class DefaultToVaultGroup(CommandWithHelpGroups, click.Group):
809 """A helper class to implement the default-to-"vault"-subcommand behavior.
811 Modifies internal [`click.MultiCommand`][] methods, and thus is both
812 an implementation detail and a kludge.
814 """
816 def resolve_command(
817 self, ctx: click.Context, args: list[str]
818 ) -> tuple[str | None, click.Command | None, list[str]]:
819 """Resolve a command, defaulting to "vault" instead of erroring out.""" # noqa: DOC201
820 cmd_name = click.utils.make_str(args[0]) 1cfeaqihk8[zj?]@96l
822 # Get the command
823 cmd = self.get_command(ctx, cmd_name) 1cfeaqihk8[zj?]@96l
825 # If we can't find the command but there is a normalization
826 # function available, we try with that one.
827 if ( # pragma: no cover 1cfeaqihk8[zj?]@96l
828 cmd is None and ctx.token_normalize_func is not None
829 ):
830 # Defensive programming (API defined by external source, and
831 # this code path is not triggered by our code), so no
832 # coverage.
833 cmd_name = ctx.token_normalize_func(cmd_name)
834 cmd = self.get_command(ctx, cmd_name)
836 # If we don't find the command we want to show an error message
837 # to the user that it was not provided. However, there is
838 # something else we should do: if the first argument looks like
839 # an option we want to kick off parsing again for arguments to
840 # resolve things like --help which now should go to the main
841 # place.
842 if cmd is None and not ctx.resilient_parsing: 1cfeaqihk8[zj?]@96l
843 ####
844 # BEGIN modifications for derivepassphrase
845 #
846 # Instead of using
847 #
848 # if click.parsers.split_opt(cmd_name)[0]
849 #
850 # which splits the option prefix (typically `-` or `--`) from
851 # the option name, but triggers deprecation warnings in click
852 # 8.2.0 and later, we check directly for a `-` prefix.
853 #
854 # END modifications for derivepassphrase
855 ####
856 if cmd_name.startswith('-'): 1[j
857 self.parse_args(ctx, ctx.args) 1j
858 ####
859 # BEGIN modifications for derivepassphrase
860 #
861 # Instead of calling ctx.fail here, default to "vault", and
862 # issue a deprecation warning.
863 deprecation = logging.getLogger(f'{PROG_NAME}.deprecation') 1[j
864 deprecation.warning( 1[j
865 _msg.TranslatedString(
866 _msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED
867 )
868 )
869 cmd_name = 'vault' 1[j
870 cmd = self.get_command(ctx, cmd_name) 1[j
871 assert cmd is not None, 'Mandatory subcommand "vault" missing!' 1[j
872 args = [cmd_name, *args] 1[j
873 #
874 # END modifications for derivepassphrase
875 ####
876 return cmd_name if cmd else None, cmd, args[1:] 1cfeaqihk8[zj?]@96l
879# TODO(the-13th-letter): Base this class on CommandWithHelpGroups and
880# click.Group in v1.0.
881# https://the13thletter.info/derivepassphrase/latest/upgrade-notes/#v1.0-implied-subcommands
882class TopLevelCLIEntryPoint(DefaultToVaultGroup):
883 """A minor variation of DefaultToVaultGroup for the top-level command.
885 When called as a function, this sets up the environment properly
886 before invoking the actual callbacks. Currently, this means setting
887 up the logging subsystem and the delegation of Python warnings to
888 the logging subsystem.
890 The environment setup can be bypassed by calling the `.main` method
891 directly.
893 """
895 def __call__( # pragma: no cover
896 self,
897 *args: Any, # noqa: ANN401
898 **kwargs: Any, # noqa: ANN401
899 ) -> Any: # noqa: ANN401
900 """""" # noqa: D419
901 # Coverage testing is done with the `click.testing` module,
902 # which does not use the `__call__` shortcut. So it is normal
903 # that this function is never called, and thus should be
904 # excluded from coverage.
905 with (
906 StandardCLILogging.ensure_standard_logging(),
907 StandardCLILogging.ensure_standard_warnings_logging(),
908 ):
909 return self.main(*args, **kwargs)
912# Actual option groups and callbacks used by derivepassphrase
913# ===========================================================
916def color_forcing_callback(
917 ctx: click.Context,
918 param: click.Parameter,
919 value: Any, # noqa: ANN401
920) -> None:
921 """Disable automatic color (and text highlighting).
923 Ideally, we would default to color and text styling if outputting to
924 a TTY, or monochrome/unstyled otherwise. We would also support the
925 `NO_COLOR` and `FORCE_COLOR` environment variables to override this
926 auto-detection, and perhaps the `TTY_COMPATIBLE` variable too.
928 Alas, this is not sensible to support at the moment, because the
929 conventions are still in flux. And settling on a specific
930 interpretation of the conventions would likely prove very difficult
931 to change later on in a backward-compatible way. We thus opt for
932 a conservative approach and use device-indepedendent text output
933 without any color or text styling whatsoever.
935 """
936 del param, value 2b 5 d c f e a q i h k g ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob
937 ctx.color = False 2b 5 d c f e a q i h k g ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob
940def validate_occurrence_constraint(
941 ctx: click.Context,
942 param: click.Parameter,
943 value: Any, # noqa: ANN401
944) -> int | None:
945 """Check that the occurrence constraint is valid (int, 0 or larger).
947 Args:
948 ctx: The `click` context.
949 param: The current command-line parameter.
950 value: The parameter value to be checked.
952 Returns:
953 The parsed parameter value.
955 Raises:
956 click.BadParameter: The parameter value is invalid.
958 """
959 del ctx # Unused. 1b5q!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4
960 del param # Unused. 1b5q!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4
961 if value is None: 1b5q!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4
962 return value 1b5q!#()*+WxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4
963 if isinstance(value, int): 1b5!#wxyj=mn
964 int_value = value 1=
965 else:
966 try: 1b5!#wxyjmn
967 int_value = int(value, 10) 1b5!#wxyjmn
968 except ValueError as exc: 1w
969 raise click.BadParameter(NOT_AN_INTEGER) from exc 1w
970 if int_value < 0: 1b5!#wxyj=mn
971 raise click.BadParameter(NOT_A_NONNEGATIVE_INTEGER) 1w
972 return int_value 1b5!#xyj=mn
975def validate_length(
976 ctx: click.Context,
977 param: click.Parameter,
978 value: Any, # noqa: ANN401
979) -> int | None:
980 """Check that the length is valid (int, 1 or larger).
982 Args:
983 ctx: The `click` context.
984 param: The current command-line parameter.
985 value: The parameter value to be checked.
987 Returns:
988 The parsed parameter value.
990 Raises:
991 click.BadParameter: The parameter value is invalid.
993 """
994 del ctx # Unused. 1b5q!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4
995 del param # Unused. 1b5q!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4
996 if value is None: 1b5q!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4
997 return value 1b5q!#()*+WxsyXGHI,8AJKL$tMNBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4
998 if isinstance(value, int): 1b5wxsy$ur7=lmn
999 int_value = value 1=
1000 else:
1001 try: 1b5wxsy$ur7lmn
1002 int_value = int(value, 10) 1b5wxsy$ur7lmn
1003 except ValueError as exc: 1w
1004 raise click.BadParameter(NOT_AN_INTEGER) from exc 1w
1005 if int_value < 1: 1b5wxsy$ur7=lmn
1006 raise click.BadParameter(NOT_A_POSITIVE_INTEGER) 1w
1007 return int_value 1b5xsy$ur7=lmn
1010def common_version_output(
1011 ctx: click.Context,
1012 param: click.Parameter,
1013 value: bool, # noqa: FBT001
1014) -> None:
1015 del param, value 1aoihkp
1016 major_dependencies: list[str] = [] 1aoihkp
1017 try: 1aoihkp
1018 cryptography_version = importlib.metadata.version('cryptography') 1aoihkp
1019 except ModuleNotFoundError: 1aoihkp
1020 pass 1aoihkp
1021 else:
1022 major_dependencies.append(f'cryptography {cryptography_version}') 1aoihkp
1023 major_dependencies.append(f'click {importlib.metadata.version("click")}') 1aoihkp
1025 click.echo( 1aoihkp
1026 ' '.join([
1027 click.style(PROG_NAME, bold=True),
1028 VERSION,
1029 ]),
1030 color=ctx.color,
1031 )
1032 for dependency in major_dependencies: 1aoihkp
1033 click.echo( 1aoihkp
1034 str(
1035 _msg.TranslatedString(
1036 _msg.Label.VERSION_INFO_MAJOR_LIBRARY_TEXT,
1037 dependency_name_and_version=dependency,
1038 )
1039 ),
1040 color=ctx.color,
1041 )
1044def print_version_info_types(
1045 version_info_types: dict[_msg.Label, list[str]],
1046 /,
1047 *,
1048 ctx: click.Context,
1049) -> None:
1050 for message_label, item_list in version_info_types.items(): 1aoihkp
1051 if item_list: 1aoihkp
1052 current_length = len(str(_msg.TranslatedString(message_label))) 1aoihkp
1053 formatted_item_list_pieces: list[str] = [] 1aoihkp
1054 n = len(item_list) 1aoihkp
1055 for i, item in enumerate(item_list, start=1): 1aoihkp
1056 space = ' ' 1aoihkp
1057 punctuation = '.' if i == n else ',' 1aoihkp
1058 if ( 1aoihkp
1059 current_length + len(space) + len(item) + len(punctuation)
1060 <= VERSION_OUTPUT_WRAPPING_WIDTH
1061 ):
1062 current_length += len(space) + len(item) + len(punctuation) 1aoihkp
1063 piece = f'{space}{item}{punctuation}' 1aoihkp
1064 else:
1065 space = ' ' 1aih
1066 current_length = len(space) + len(item) + len(punctuation) 1aih
1067 piece = f'\n{space}{item}{punctuation}' 1aih
1068 formatted_item_list_pieces.append(piece) 1aoihkp
1069 click.echo( 1aoihkp
1070 ''.join([
1071 click.style(
1072 str(_msg.TranslatedString(message_label)),
1073 bold=True,
1074 ),
1075 ''.join(formatted_item_list_pieces),
1076 ]),
1077 color=ctx.color,
1078 )
1081def derivepassphrase_version_option_callback(
1082 ctx: click.Context,
1083 param: click.Parameter,
1084 value: bool, # noqa: FBT001
1085) -> None:
1086 if value and not ctx.resilient_parsing: 1dcfeaqoihk8[zjv?]@96l
1087 common_version_output(ctx, param, value) 1ao
1088 derivation_schemes = dict.fromkeys(_types.DerivationScheme, True) 1ao
1089 supported_subcommands = set(_types.Subcommand) 1ao
1090 click.echo() 1ao
1091 version_info_types: dict[_msg.Label, list[str]] = { 1ao
1092 _msg.Label.SUPPORTED_DERIVATION_SCHEMES: [
1093 k for k, v in derivation_schemes.items() if v
1094 ],
1095 _msg.Label.UNAVAILABLE_DERIVATION_SCHEMES: [
1096 k for k, v in derivation_schemes.items() if not v
1097 ],
1098 _msg.Label.SUPPORTED_SUBCOMMANDS: sorted(supported_subcommands),
1099 }
1100 print_version_info_types(version_info_types, ctx=ctx) 1ao
1101 ctx.exit() 1ao
1104def export_version_option_callback(
1105 ctx: click.Context,
1106 param: click.Parameter,
1107 value: bool, # noqa: FBT001
1108) -> None:
1109 if value and not ctx.resilient_parsing: 1cfaih[z?]@
1110 common_version_output(ctx, param, value) 1ai
1111 supported_subcommands = set(_types.ExportSubcommand) 1ai
1112 foreign_configuration_formats = { 1ai
1113 _types.ForeignConfigurationFormat.VAULT_STOREROOM: False,
1114 _types.ForeignConfigurationFormat.VAULT_V02: False,
1115 _types.ForeignConfigurationFormat.VAULT_V03: False,
1116 }
1117 click.echo() 1ai
1118 version_info_types: dict[_msg.Label, list[str]] = { 1ai
1119 _msg.Label.UNAVAILABLE_FOREIGN_CONFIGURATION_FORMATS: [
1120 k for k, v in foreign_configuration_formats.items() if not v
1121 ],
1122 _msg.Label.SUPPORTED_SUBCOMMANDS: sorted(supported_subcommands),
1123 }
1124 print_version_info_types(version_info_types, ctx=ctx) 1ai
1125 ctx.exit() 1ai
1128def export_vault_version_option_callback(
1129 ctx: click.Context,
1130 param: click.Parameter,
1131 value: bool, # noqa: FBT001
1132) -> None:
1133 if value and not ctx.resilient_parsing: 2f a h [ z ? @ gbhbibjbkblbmbnbfbob
1134 common_version_output(ctx, param, value) 1ah
1135 foreign_configuration_formats = { 1ah
1136 _types.ForeignConfigurationFormat.VAULT_STOREROOM: False,
1137 _types.ForeignConfigurationFormat.VAULT_V02: False,
1138 _types.ForeignConfigurationFormat.VAULT_V03: False,
1139 }
1140 known_extras = { 1ah
1141 _types.PEP508Extra.EXPORT: False,
1142 }
1143 from derivepassphrase.exporter import storeroom, vault_native # noqa: I001,PLC0415 1ah
1145 foreign_configuration_formats[ 1ah
1146 _types.ForeignConfigurationFormat.VAULT_STOREROOM
1147 ] = not storeroom.STUBBED
1148 foreign_configuration_formats[ 1ah
1149 _types.ForeignConfigurationFormat.VAULT_V02
1150 ] = not vault_native.STUBBED
1151 foreign_configuration_formats[ 1ah
1152 _types.ForeignConfigurationFormat.VAULT_V03
1153 ] = not vault_native.STUBBED
1154 known_extras[_types.PEP508Extra.EXPORT] = ( 1ah
1155 not storeroom.STUBBED and not vault_native.STUBBED
1156 )
1157 click.echo() 1ah
1158 version_info_types: dict[_msg.Label, list[str]] = { 1ah
1159 _msg.Label.SUPPORTED_FOREIGN_CONFIGURATION_FORMATS: [
1160 k for k, v in foreign_configuration_formats.items() if v
1161 ],
1162 _msg.Label.UNAVAILABLE_FOREIGN_CONFIGURATION_FORMATS: [
1163 k for k, v in foreign_configuration_formats.items() if not v
1164 ],
1165 _msg.Label.ENABLED_PEP508_EXTRAS: [
1166 k for k, v in known_extras.items() if v
1167 ],
1168 }
1169 print_version_info_types(version_info_types, ctx=ctx) 1ah
1170 ctx.exit() 1ah
1173def vault_version_option_callback(
1174 ctx: click.Context,
1175 param: click.Parameter,
1176 value: bool, # noqa: FBT001
1177) -> None:
1178 if value and not ctx.resilient_parsing: 1b5eaqkgp!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4
1179 common_version_output(ctx, param, value) 1akp
1180 features = { 1akp
1181 _types.Feature.SSH_KEY: hasattr(socket, 'AF_UNIX'),
1182 }
1183 click.echo() 1akp
1184 version_info_types: dict[_msg.Label, list[str]] = { 1akp
1185 _msg.Label.SUPPORTED_FEATURES: [
1186 k for k, v in features.items() if v
1187 ],
1188 _msg.Label.UNAVAILABLE_FEATURES: [
1189 k for k, v in features.items() if not v
1190 ],
1191 }
1192 print_version_info_types(version_info_types, ctx=ctx) 1akp
1193 ctx.exit() 1akp
1196def version_option(
1197 version_option_callback: Callable[
1198 [click.Context, click.Parameter, Any], Any
1199 ],
1200) -> Callable[[Callable[P, R]], Callable[P, R]]:
1201 return click.option(
1202 '--version',
1203 is_flag=True,
1204 is_eager=True,
1205 expose_value=False,
1206 callback=version_option_callback,
1207 cls=StandardOption,
1208 help=_msg.TranslatedString(_msg.Label.VERSION_OPTION_HELP_TEXT),
1209 )
1212color_forcing_pseudo_option = click.option(
1213 '--_pseudo-option-color-forcing',
1214 '_color_forcing',
1215 is_flag=True,
1216 is_eager=True,
1217 expose_value=False,
1218 hidden=True,
1219 callback=color_forcing_callback,
1220 help='(pseudo-option)',
1221)
1224class PassphraseGenerationOption(OptionGroupOption):
1225 """Passphrase generation options for the CLI."""
1227 option_group_name = _msg.TranslatedString(
1228 _msg.Label.PASSPHRASE_GENERATION_LABEL
1229 )
1230 epilog = _msg.TranslatedString(
1231 _msg.Label.PASSPHRASE_GENERATION_EPILOG,
1232 metavar=_msg.TranslatedString(
1233 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER
1234 ),
1235 )
1238class ConfigurationOption(OptionGroupOption):
1239 """Configuration options for the CLI."""
1241 option_group_name = _msg.TranslatedString(_msg.Label.CONFIGURATION_LABEL)
1242 epilog = _msg.TranslatedString(_msg.Label.CONFIGURATION_EPILOG)
1245class StorageManagementOption(OptionGroupOption):
1246 """Storage management options for the CLI."""
1248 option_group_name = _msg.TranslatedString(
1249 _msg.Label.STORAGE_MANAGEMENT_LABEL
1250 )
1251 epilog = _msg.TranslatedString(
1252 _msg.Label.STORAGE_MANAGEMENT_EPILOG,
1253 metavar=_msg.TranslatedString(
1254 _msg.Label.STORAGE_MANAGEMENT_METAVAR_PATH
1255 ),
1256 )
1259class CompatibilityOption(OptionGroupOption):
1260 """Compatibility and incompatibility options for the CLI."""
1262 option_group_name = _msg.TranslatedString(
1263 _msg.Label.COMPATIBILITY_OPTION_LABEL
1264 )
1267class LoggingOption(OptionGroupOption):
1268 """Logging options for the CLI."""
1270 option_group_name = _msg.TranslatedString(_msg.Label.LOGGING_LABEL)
1271 epilog = ''
1274debug_option = click.option(
1275 '--debug',
1276 'logging_level',
1277 is_flag=True,
1278 flag_value=logging.DEBUG,
1279 expose_value=False,
1280 callback=adjust_logging_level,
1281 help=_msg.TranslatedString(_msg.Label.DEBUG_OPTION_HELP_TEXT),
1282 cls=LoggingOption,
1283)
1284verbose_option = click.option(
1285 '-v',
1286 '--verbose',
1287 'logging_level',
1288 is_flag=True,
1289 flag_value=logging.INFO,
1290 expose_value=False,
1291 callback=adjust_logging_level,
1292 help=_msg.TranslatedString(_msg.Label.VERBOSE_OPTION_HELP_TEXT),
1293 cls=LoggingOption,
1294)
1295quiet_option = click.option(
1296 '-q',
1297 '--quiet',
1298 'logging_level',
1299 is_flag=True,
1300 flag_value=logging.ERROR,
1301 expose_value=False,
1302 callback=adjust_logging_level,
1303 help=_msg.TranslatedString(_msg.Label.QUIET_OPTION_HELP_TEXT),
1304 cls=LoggingOption,
1305)
1308def standard_logging_options(f: Callable[P, R]) -> Callable[P, R]:
1309 """Decorate the function with standard logging click options.
1311 Adds the three click options `-v`/`--verbose`, `-q`/`--quiet` and
1312 `--debug`, which calls back into the [`adjust_logging_level`][]
1313 function (with different argument values).
1315 Args:
1316 f: A callable to decorate.
1318 Returns:
1319 The decorated callable.
1321 """
1322 return debug_option(verbose_option(quiet_option(f)))
1325# Shell completion
1326# ================
1329# TODO(the-13th-letter): Remove this once upstream click's Zsh completion
1330# script properly supports colons.
1331#
1332# https://github.com/pallets/click/pull/2846
1333class ZshComplete(click.shell_completion.ZshComplete):
1334 """Zsh completion class that supports colons.
1336 `click`'s Zsh completion class (at least v8.1.7 and v8.1.8) uses
1337 some completion helper functions (provided by Zsh) that parse each
1338 completion item into value-description pairs, separated by a colon.
1339 Other completion helper functions don't. Correspondingly, any
1340 internal colons in the completion item's value sometimes need to be
1341 escaped, and sometimes don't.
1343 The "right" way to fix this is to modify the Zsh completion script
1344 to only use one type of serialization: either escaped, or unescaped.
1345 However, the Zsh completion script itself may already be installed
1346 in the user's Zsh settings, and we have no way of knowing that.
1347 Therefore, it is better to change the `format_completion` method to
1348 adaptively and "smartly" emit colon-escaped output or not, based on
1349 whether the completion script will be using it.
1351 """
1353 @override
1354 def format_completion(
1355 self,
1356 item: click.shell_completion.CompletionItem,
1357 ) -> str:
1358 """Return a suitable serialization of the CompletionItem.
1360 This serialization ensures colons in the item value are properly
1361 escaped if and only if the completion script will attempt to
1362 pass a colon-separated key/description pair to the underlying
1363 Zsh machinery. This is the case if and only if the help text is
1364 non-degenerate.
1366 """
1367 help_ = item.help or '_' 16
1368 value = item.value.replace(':', r'\:' if help_ != '_' else ':') 16
1369 return f'{item.type}\n{value}\n{help_}' 16
1372# Our ZshComplete class depends crucially on the exact shape of the Zsh
1373# completion script. So only fix the completion formatter if the
1374# completion script is still the same.
1375#
1376# (This Zsh script is part of click, and available under the
1377# 3-clause-BSD license.)
1378_ORIG_SOURCE_TEMPLATE = """\
1379#compdef %(prog_name)s
1381%(complete_func)s() {
1382 local -a completions
1383 local -a completions_with_descriptions
1384 local -a response
1385 (( ! $+commands[%(prog_name)s] )) && return 1
1387 response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \
1388%(complete_var)s=zsh_complete %(prog_name)s)}")
1390 for type key descr in ${response}; do
1391 if [[ "$type" == "plain" ]]; then
1392 if [[ "$descr" == "_" ]]; then
1393 completions+=("$key")
1394 else
1395 completions_with_descriptions+=("$key":"$descr")
1396 fi
1397 elif [[ "$type" == "dir" ]]; then
1398 _path_files -/
1399 elif [[ "$type" == "file" ]]; then
1400 _path_files -f
1401 fi
1402 done
1404 if [ -n "$completions_with_descriptions" ]; then
1405 _describe -V unsorted completions_with_descriptions -U
1406 fi
1408 if [ -n "$completions" ]; then
1409 compadd -U -V unsorted -a completions
1410 fi
1411}
1413if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
1414 # autoload from fpath, call function directly
1415 %(complete_func)s "$@"
1416else
1417 # eval/source/. command, register function for later
1418 compdef %(complete_func)s %(prog_name)s
1419fi
1420"""
1421if (
1422 click.shell_completion.ZshComplete.source_template == _ORIG_SOURCE_TEMPLATE
1423): # pragma: no cover
1424 # Defensive programming (API defined by external source), so no
1425 # coverage.
1426 click.shell_completion.add_completion_class(ZshComplete)