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