Coverage for src\derivepassphrase\_internals\cli_machinery.py: 100.000%

1# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>

2#

3# SPDX-License-Identifier: Zlib

4

5

6"""Command-line machinery for derivepassphrase.

7

8 Warning:

9 Non-public module (implementation detail), provided for didactical and

10 educational purposes only. Subject to change without notice, including

11 removal.

12

13"""

14

15 from __future__ import annotations (empty)

16

17 import collections (empty)

18 import importlib.metadata (empty)

19 import inspect (empty)

20 import logging (empty)

21 import socket (empty)

22 import warnings (empty)

23 from typing import TYPE_CHECKING, Callable, Literal, TextIO, TypeVar (empty)

24

25 import click (empty)

26 import click.shell_completion (empty)

27 from typing_extensions import Any, ParamSpec, override (empty)

28

29 from derivepassphrase import _internals, _types (empty)

30 from derivepassphrase._internals import cli_messages as _msg (empty)

31

32 if TYPE_CHECKING: (empty)

33 import types

34 from collections.abc import (

35 MutableSequence,

36 )

37

38 from typing_extensions import Self

39

40 PROG_NAME = _internals.PROG_NAME (empty)

41 VERSION = _internals.VERSION (empty)

42 VERSION_OUTPUT_WRAPPING_WIDTH = 72 (empty)

43

44# Error messages

45 NOT_AN_INTEGER = 'not an integer' (empty)

46 NOT_A_NONNEGATIVE_INTEGER = 'not a non-negative integer' (empty)

47 NOT_A_POSITIVE_INTEGER = 'not a positive integer' (empty)

48

49

50# Logging

51# =======

52

53

54 class ClickEchoStderrHandler(logging.Handler): (empty)

55 """A [`logging.Handler`][] for `click` applications.

56

57 Outputs log messages to [`sys.stderr`][] via [`click.echo`][].

58

59 """

60

61 def emit(self, record: logging.LogRecord) -> None: (empty)

62 """Emit a log record.

63

64 Format the log record, then emit it via [`click.echo`][] to

65 [`sys.stderr`][].

66

67 """

68 click.echo( 44 ctx 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 )

73

74

75 class CLIofPackageFormatter(logging.Formatter): (empty)

76 """A [`logging.LogRecord`][] formatter for the CLI of a Python package.

77

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.

83

84 Essentially, this prepends certain short strings to the log message

85 lines to make them readable as standard error output.

86

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.

92

93 [CLICK]: https://pypi.org/projects/click/

94

95 """

96

97 def __init__( (empty)

98 self,

99 *,

100 prog_name: str = PROG_NAME,

101 package_name: str | None = None,

102 ) -> None:

103 self.prog_name = prog_name (empty)

104 self.package_name = ( (empty)

105 package_name

106 if package_name is not None

107 else prog_name.lower().replace(' ', '_').replace('-', '_')

108 )

109

110 def format(self, record: logging.LogRecord) -> str: (empty)

111 """Format a log record suitably for standard error console output.

112

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:

117

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.

129

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.

133

134 Args:

135 record: A log record.

136

137 Returns:

138 A formatted log record.

139

140 Raises:

141 AssertionError:

142 The log level is not supported.

143

144 """

145 preliminary_result = record.getMessage() 44 ctx 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}: ' 44 ctx 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 44 ctx 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': 44 ctx 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 = '' 1 ctx 1T

153 elif record.levelname == 'WARNING': 44 ctx 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 = ( 22 ctx 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'}: 23 ctx 1GHIAJKLtMurNBCODQRESFUV

160 level_indicator = '' 23 ctx 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 = [ 44 ctx 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: 44 ctx 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') 6 ctx 1ArBCDE

173 return ''.join(parts) 44 ctx 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

174

175

176 class StandardCLILogging: (empty)

177 """Set up CLI logging handlers upon instantiation."""

178

179 prog_name = PROG_NAME (empty)

180 package_name = PROG_NAME.lower().replace(' ', '_').replace('-', '_') (empty)

181 cli_formatter = CLIofPackageFormatter( (empty)

182 prog_name=prog_name, package_name=package_name

183 )

184 cli_handler = ClickEchoStderrHandler() (empty)

185 cli_handler.addFilter(logging.Filter(name=package_name)) (empty)

186 cli_handler.setFormatter(cli_formatter) (empty)

187 cli_handler.setLevel(logging.WARNING) (empty)

188 warnings_handler = ClickEchoStderrHandler() (empty)

189 warnings_handler.addFilter(logging.Filter(name='py.warnings')) (empty)

190 warnings_handler.setFormatter(cli_formatter) (empty)

191 warnings_handler.setLevel(logging.WARNING) (empty)

192

193 @classmethod (empty)

194 def ensure_standard_logging(cls) -> StandardLoggingContextManager: (empty)

195 """Return a context manager to ensure standard logging is set up."""

196 return StandardLoggingContextManager( 87 ctx 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 )

200

201 @classmethod (empty)

202 def ensure_standard_warnings_logging( (empty)

203 cls,

204 ) -> StandardWarningsLoggingContextManager:

205 """Return a context manager to ensure warnings logging is set up."""

206 return StandardWarningsLoggingContextManager( 87 ctx 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 )

209

210

211 class StandardLoggingContextManager: (empty)

212 """A reentrant context manager setting up standard CLI logging.

213

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.

218

219 Reentrant, but not thread safe, because it temporarily modifies

220 global state.

221

222 """

223

224 def __init__( (empty)

225 self,

226 handler: logging.Handler,

227 root_logger: str | None = None,

228 ) -> None:

229 self.handler = handler 88 ctx 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 88 ctx 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) 88 ctx 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() 88 ctx 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

233

234 def __enter__(self) -> Self: (empty)

235 self.action_required.append( 88 ctx 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]: 88 ctx 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) 88 ctx 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 88 ctx 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

241

242 def __exit__( (empty)

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]: 88 ctx 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) 88 ctx 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() 88 ctx 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 88 ctx 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

252

253

254 class StandardWarningsLoggingContextManager(StandardLoggingContextManager): (empty)

255 """A reentrant context manager setting up standard warnings logging.

256

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.

261

262 Reentrant, but not thread safe, because it temporarily modifies

263 global state.

264

265 """

266

267 def __init__( (empty)

268 self,

269 handler: logging.Handler,

270 ) -> None:

271 super().__init__(handler=handler, root_logger='py.warnings') 87 ctx 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[ 87 ctx 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()

295

296 def __enter__(self) -> Self: (empty)

297 def showwarning( # noqa: PLR0913,PLR0917 87 ctx 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 ctx 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 ctx 1;

314 str(

315 warnings.formatwarning(

316 message, category, filename, lineno, line

317 )

318 )

319 )

320

321 ctx = warnings.catch_warnings() 87 ctx 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__ 87 ctx 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__() 87 ctx 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)) 87 ctx 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 87 ctx 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__() 87 ctx 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

327

328 def __exit__( (empty)

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) 87 ctx 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) 87 ctx 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 87 ctx 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 87 ctx 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

338

339

340 P = ParamSpec('P') (empty)

341 R = TypeVar('R') (empty)

342

343

344 def adjust_logging_level( (empty)

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.

351

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`.

355

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: 85 ctx 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 84 ctx 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) 1 ctx 1P

362 logging.getLogger(StandardCLILogging.package_name).setLevel(value) 1 ctx 1P

363

364

365# Option parsing and grouping

366# ===========================

367

368

369 class OptionGroupOption(click.Option): (empty)

370 """A [`click.Option`][] with an associated group name and group epilog.

371

372 Used by [`CommandWithHelpGroups`][] to print help sections. Each

373 subclass contains its own group name and epilog.

374

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.

382

383 """

384

385 option_group_name: object = '' (empty)

386 """""" (empty)

387 epilog: object = '' (empty)

388 """""" (empty)

389

390 def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 (empty)

391 if self.__class__ == __class__: # type: ignore[name-defined] 91 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

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() 91 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

400 help = kwargs.pop('help', unset) # noqa: A001 91 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

401 super().__init__(*args, **kwargs) 91 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

402 if help is not unset: # pragma: no branch 91 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

403 self.help = help 91 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

404

405

406 class StandardOption(OptionGroupOption): (empty)

407 pass (empty)

408

409

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.

450 class CommandWithHelpGroups(click.Command): (empty)

451 """A [`click.Command`][] with support for some help text customizations.

452

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.

456

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.

460

461 [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746

462

463 """

464

465 @staticmethod (empty)

466 def _text(text: object, /) -> str: (empty)

467 if isinstance(text, (list, tuple)): 7 ctx 1dcfeag]

468 return '\n\n'.join(str(x) for x in text) 7 ctx 1dcfeag]

469 return str(text) 6 ctx 1dcfeag

470

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]: (empty)

474 """Return the pieces for the usage string.

475

476 Args:

477 ctx:

478 The click context.

479

480 """

481 rv = [str(self.options_metavar)] if self.options_metavar else [] 16 ctx 2d c f e a g w x y t 7 % ' z v fb

482 for param in self.get_params(ctx): 16 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)) 16 ctx 2d c f e a g w x y t 7 % ' z v fb

484 return rv 16 ctx 2d c f e a g w x y t 7 % ' z v fb

485

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( (empty)

489 self,

490 ctx: click.Context,

491 ) -> click.Option | None:

492 """Return a standard help option object.

493

494 Args:

495 ctx:

496 The click context.

497

498 """

499 help_options = self.get_help_option_names(ctx) 91 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

500

501 if not help_options or not self.add_help_option: # pragma: no cover 91 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

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

506

507 def show_help( 91 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

508 ctx: click.Context,

509 param: click.Parameter, # noqa: ARG001

510 value: str,

511 ) -> None:

512 if value and not ctx.resilient_parsing: 89 ctx 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) 6 ctx 1dcfeag

514 ctx.exit() 6 ctx 1dcfeag

515

516 # Modified from click 8.1: We use StandardOption and a non-str

517 # object as the help string.

518 return StandardOption( 91 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

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 )

526

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( (empty)

530 self,

531 limit: int = 45,

532 ) -> str:

533 """Return the short help string for a command.

534

535 If only a long help string is given, shorten it.

536

537 Args:

538 limit:

539 The maximum width of the short help string.

540

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 4 ctx 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: 4 ctx 1dca]

552 text = click.utils.make_default_short_help( 4 ctx 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 4 ctx 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() 4 ctx 1dca]

572

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( (empty)

576 self,

577 ctx: click.Context,

578 formatter: click.HelpFormatter,

579 ) -> None:

580 """Format the help text prologue, if any.

581

582 Args:

583 ctx:

584 The click context.

585 formatter:

586 The formatter for the `--help` listing.

587

588 """

589 del ctx 6 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 = ( 6 ctx 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 6 ctx 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 6 ctx 1dcfeag

611 formatter.write_paragraph() 6 ctx 1dcfeag

612 with formatter.indentation(): 6 ctx 1dcfeag

613 formatter.write_text(text) 6 ctx 1dcfeag

614

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( (empty)

620 self,

621 ctx: click.Context,

622 formatter: click.HelpFormatter,

623 ) -> None:

624 r"""Format options on the help listing, grouped into sections.

625

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.

637

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`][].

640

641 Args:

642 ctx:

643 The click context.

644 formatter:

645 The formatter for the `--help` listing.

646

647 """

648 default_group_name = '' 6 ctx 1dcfeag

649 help_records: dict[str, list[tuple[str, str]]] = {} 6 ctx 1dcfeag

650 epilogs: dict[str, str] = {} 6 ctx 1dcfeag

651 params = self.params[:] 6 ctx 1dcfeag

652 if ( # pragma: no branch 6 ctx 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) 6 ctx 1dcfeag

657 for param in params: 6 ctx 1dcfeag

658 rec = param.get_help_record(ctx) 6 ctx 1dcfeag

659 if rec is not None: 6 ctx 1dcfeag

660 rec = (rec[0], self._text(rec[1])) 6 ctx 1dcfeag

661 if isinstance(param, OptionGroupOption): 6 ctx 1dcfeag

662 group_name = self._text(param.option_group_name) 6 ctx 1dcfeag

663 epilogs.setdefault(group_name, self._text(param.epilog)) 6 ctx 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) 6 ctx 1dcfeag

670 if default_group_name in help_records: # pragma: no branch 6 ctx 1dcfeag

671 default_group = help_records.pop(default_group_name) 6 ctx 1dcfeag

672 default_group_label = ( 6 ctx 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( 6 ctx 1dcfeag

678 _msg.TranslatedString(default_group_label)

679 )

680 help_records[default_group_name] = default_group 6 ctx 1dcfeag

681 for group_name, records in help_records.items(): 6 ctx 1dcfeag

682 with formatter.section(group_name): 6 ctx 1dcfeag

683 formatter.write_dl(records) 6 ctx 1dcfeag

684 epilog = inspect.cleandoc(epilogs.get(group_name, '')) 6 ctx 1dcfeag

685 if epilog: 6 ctx 1dcfeag

686 formatter.write_paragraph() 3 ctx 1eag

687 with formatter.indentation(): 3 ctx 1eag

688 formatter.write_text(epilog) 3 ctx 1eag

689 self.format_commands(ctx, formatter) 6 ctx 1dcfeag

690

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( (empty)

696 self,

697 ctx: click.Context,

698 formatter: click.HelpFormatter,

699 ) -> None:

700 """Format the subcommands, if any.

701

702 If called on a command object that isn't derived from

703 [`click.Group`][], then do nothing.

704

705 Args:

706 ctx:

707 The click context.

708 formatter:

709 The formatter for the `--help` listing.

710

711 """

712 if not isinstance(self, click.Group): 6 ctx 1dcfeag

713 return 4 ctx 1feag

714 commands: list[tuple[str, click.Command]] = [] 3 ctx 1dca

715 for subcommand in self.list_commands(ctx): 3 ctx 1dca

716 cmd = self.get_command(ctx, subcommand) 3 ctx 1dca

717 if cmd is None or cmd.hidden: # pragma: no cover 3 ctx 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)) 3 ctx 1dca

723 if commands: # pragma: no branch 3 ctx 1dca

724 longest_command = max((cmd[0] for cmd in commands), key=len) 3 ctx 1dca

725 limit = formatter.width - 6 - len(longest_command) 3 ctx 1dca

726 rows: list[tuple[str, str]] = [] 3 ctx 1dca

727 for subcommand, cmd in commands: 3 ctx 1dca

728 help_str = self._text(cmd.get_short_help_str(limit) or '') 3 ctx 1dca

729 rows.append((subcommand, help_str)) 3 ctx 1dca

730 if rows: # pragma: no branch 3 ctx 1dca

731 commands_label = self._text( 3 ctx 1dca

732 _msg.TranslatedString(_msg.Label.COMMANDS_LABEL)

733 )

734 with formatter.section(commands_label): 3 ctx 1dca

735 formatter.write_dl(rows) 3 ctx 1dca

736

737 # This method is based on click 8.1; see the comment above the class

738 # declaration for license details.

739 def format_epilog( (empty)

740 self,

741 ctx: click.Context,

742 formatter: click.HelpFormatter,

743 ) -> None:

744 """Format the epilog, if any.

745

746 Args:

747 ctx:

748 The click context.

749 formatter:

750 The formatter for the `--help` listing.

751

752 """

753 del ctx 6 ctx 1dcfeag

754 if self.epilog: # pragma: no branch 6 ctx 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)) 4 ctx 1deag

760 formatter.write_paragraph() 4 ctx 1deag

761 with formatter.indentation(): 4 ctx 1deag

762 formatter.write_text(epilog) 4 ctx 1deag

763

764

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

808 class DefaultToVaultGroup(CommandWithHelpGroups, click.Group): (empty)

809 """A helper class to implement the default-to-"vault"-subcommand behavior.

810

811 Modifies internal [`click.MultiCommand`][] methods, and thus is both

812 an implementation detail and a kludge.

813

814 """

815

816 def resolve_command( (empty)

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]) 18 ctx 1cfeaqihk8[zj?]@96l

821

822 # Get the command

823 cmd = self.get_command(ctx, cmd_name) 18 ctx 1cfeaqihk8[zj?]@96l

824

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 18 ctx 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)

835

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: 18 ctx 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('-'): 2 ctx 1[j

857 self.parse_args(ctx, ctx.args) 1 ctx 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') 2 ctx 1[j

864 deprecation.warning( 2 ctx 1[j

865 _msg.TranslatedString(

866 _msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED

867 )

868 )

869 cmd_name = 'vault' 2 ctx 1[j

870 cmd = self.get_command(ctx, cmd_name) 2 ctx 1[j

871 assert cmd is not None, 'Mandatory subcommand "vault" missing!' 2 ctx 1[j

872 args = [cmd_name, *args] 2 ctx 1[j

873 #

874 # END modifications for derivepassphrase

875 ####

876 return cmd_name if cmd else None, cmd, args[1:] 18 ctx 1cfeaqihk8[zj?]@96l

877

878

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

882 class TopLevelCLIEntryPoint(DefaultToVaultGroup): (empty)

883 """A minor variation of DefaultToVaultGroup for the top-level command.

884

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.

889

890 The environment setup can be bypassed by calling the `.main` method

891 directly.

892

893 """

894

895 def __call__( # pragma: no cover (empty)

896 self,

897 *args: Any, # noqa: ANN401 (empty)

898 **kwargs: Any, # noqa: ANN401 (empty)

899 ) -> Any: # noqa: ANN401 (empty)

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)

910

911

912# Actual option groups and callbacks used by derivepassphrase

913# ===========================================================

914

915

916 def color_forcing_callback( (empty)

917 ctx: click.Context,

918 param: click.Parameter,

919 value: Any, # noqa: ANN401

920) -> None:

921 """Disable automatic color (and text highlighting).

922

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.

927

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.

934

935 """

936 del param, value 89 ctx 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 89 ctx 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

938

939

940 def validate_occurrence_constraint( (empty)

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).

946

947 Args:

948 ctx: The `click` context.

949 param: The current command-line parameter.

950 value: The parameter value to be checked.

951

952 Returns:

953 The parsed parameter value.

954

955 Raises:

956 click.BadParameter: The parameter value is invalid.

957

958 """

959 del ctx # Unused. 67 ctx 1b5q!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4

960 del param # Unused. 67 ctx 1b5q!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4

961 if value is None: 67 ctx 1b5q!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4

962 return value 66 ctx 1b5q!#()*+WxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4

963 if isinstance(value, int): 11 ctx 1b5!#wxyj=mn

964 int_value = value 1 ctx 1=

965 else:

966 try: 10 ctx 1b5!#wxyjmn

967 int_value = int(value, 10) 10 ctx 1b5!#wxyjmn

968 except ValueError as exc: 1 ctx 1w

969 raise click.BadParameter(NOT_AN_INTEGER) from exc 1 ctx 1w

970 if int_value < 0: 11 ctx 1b5!#wxyj=mn

971 raise click.BadParameter(NOT_A_NONNEGATIVE_INTEGER) 1 ctx 1w

972 return int_value 10 ctx 1b5!#xyj=mn

973

974

975 def validate_length( (empty)

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).

981

982 Args:

983 ctx: The `click` context.

984 param: The current command-line parameter.

985 value: The parameter value to be checked.

986

987 Returns:

988 The parsed parameter value.

989

990 Raises:

991 click.BadParameter: The parameter value is invalid.

992

993 """

994 del ctx # Unused. 67 ctx 1b5q!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4

995 del param # Unused. 67 ctx 1b5q!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4

996 if value is None: 67 ctx 1b5q!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4

997 return value 63 ctx 1b5q!#()*+WxsyXGHI,8AJKL$tMNBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4

998 if isinstance(value, int): 14 ctx 1b5wxsy$ur7=lmn

999 int_value = value 1 ctx 1=

1000 else:

1001 try: 13 ctx 1b5wxsy$ur7lmn

1002 int_value = int(value, 10) 13 ctx 1b5wxsy$ur7lmn

1003 except ValueError as exc: 1 ctx 1w

1004 raise click.BadParameter(NOT_AN_INTEGER) from exc 1 ctx 1w

1005 if int_value < 1: 14 ctx 1b5wxsy$ur7=lmn

1006 raise click.BadParameter(NOT_A_POSITIVE_INTEGER) 1 ctx 1w

1007 return int_value 13 ctx 1b5xsy$ur7=lmn

1008

1009

1010 def common_version_output( (empty)

1011 ctx: click.Context,

1012 param: click.Parameter,

1013 value: bool, # noqa: FBT001

1014) -> None:

1015 del param, value 6 ctx 1aoihkp

1016 major_dependencies: list[str] = [] 6 ctx 1aoihkp

1017 try: 6 ctx 1aoihkp

1018 cryptography_version = importlib.metadata.version('cryptography') 6 ctx 1aoihkp

1019 except ModuleNotFoundError: 6 ctx 1aoihkp

1020 pass 6 ctx 1aoihkp

1021 else:

1022 major_dependencies.append(f'cryptography {cryptography_version}') 6 ctx 1aoihkp

1023 major_dependencies.append(f'click {importlib.metadata.version("click")}') 6 ctx 1aoihkp

1024

1025 click.echo( 6 ctx 1aoihkp

1026 ' '.join([

1027 click.style(PROG_NAME, bold=True),

1028 VERSION,

1029 ]),

1030 color=ctx.color,

1031 )

1032 for dependency in major_dependencies: 6 ctx 1aoihkp

1033 click.echo( 6 ctx 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 )

1042

1043

1044 def print_version_info_types( (empty)

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(): 6 ctx 1aoihkp

1051 if item_list: 6 ctx 1aoihkp

1052 current_length = len(str(_msg.TranslatedString(message_label))) 6 ctx 1aoihkp

1053 formatted_item_list_pieces: list[str] = [] 6 ctx 1aoihkp

1054 n = len(item_list) 6 ctx 1aoihkp

1055 for i, item in enumerate(item_list, start=1): 6 ctx 1aoihkp

1056 space = ' ' 6 ctx 1aoihkp

1057 punctuation = '.' if i == n else ',' 6 ctx 1aoihkp

1058 if ( 6 ctx 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) 6 ctx 1aoihkp

1063 piece = f'{space}{item}{punctuation}' 6 ctx 1aoihkp

1064 else:

1065 space = ' ' 3 ctx 1aih

1066 current_length = len(space) + len(item) + len(punctuation) 3 ctx 1aih

1067 piece = f'\n{space}{item}{punctuation}' 3 ctx 1aih

1068 formatted_item_list_pieces.append(piece) 6 ctx 1aoihkp

1069 click.echo( 6 ctx 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 )

1079

1080

1081 def derivepassphrase_version_option_callback( (empty)

1082 ctx: click.Context,

1083 param: click.Parameter,

1084 value: bool, # noqa: FBT001

1085) -> None:

1086 if value and not ctx.resilient_parsing: 21 ctx 1dcfeaqoihk8[zjv?]@96l

1087 common_version_output(ctx, param, value) 2 ctx 1ao

1088 derivation_schemes = dict.fromkeys(_types.DerivationScheme, True) 2 ctx 1ao

1089 supported_subcommands = set(_types.Subcommand) 2 ctx 1ao

1090 click.echo() 2 ctx 1ao

1091 version_info_types: dict[_msg.Label, list[str]] = { 2 ctx 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) 2 ctx 1ao

1101 ctx.exit() 2 ctx 1ao

1102

1103

1104 def export_version_option_callback( (empty)

1105 ctx: click.Context,

1106 param: click.Parameter,

1107 value: bool, # noqa: FBT001

1108) -> None:

1109 if value and not ctx.resilient_parsing: 10 ctx 1cfaih[z?]@

1110 common_version_output(ctx, param, value) 2 ctx 1ai

1111 supported_subcommands = set(_types.ExportSubcommand) 2 ctx 1ai

1112 foreign_configuration_formats = { 2 ctx 1ai

1113 _types.ForeignConfigurationFormat.VAULT_STOREROOM: False,

1114 _types.ForeignConfigurationFormat.VAULT_V02: False,

1115 _types.ForeignConfigurationFormat.VAULT_V03: False,

1116 }

1117 click.echo() 2 ctx 1ai

1118 version_info_types: dict[_msg.Label, list[str]] = { 2 ctx 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) 2 ctx 1ai

1125 ctx.exit() 2 ctx 1ai

1126

1127

1128 def export_vault_version_option_callback( (empty)

1129 ctx: click.Context,

1130 param: click.Parameter,

1131 value: bool, # noqa: FBT001

1132) -> None:

1133 if value and not ctx.resilient_parsing: 17 ctx 2f a h [ z ? @ gbhbibjbkblbmbnbfbob

1134 common_version_output(ctx, param, value) 2 ctx 1ah

1135 foreign_configuration_formats = { 2 ctx 1ah

1136 _types.ForeignConfigurationFormat.VAULT_STOREROOM: False,

1137 _types.ForeignConfigurationFormat.VAULT_V02: False,

1138 _types.ForeignConfigurationFormat.VAULT_V03: False,

1139 }

1140 known_extras = { 2 ctx 1ah

1141 _types.PEP508Extra.EXPORT: False,

1142 }

1143 from derivepassphrase.exporter import storeroom, vault_native # noqa: I001,PLC0415 2 ctx 1ah

1144

1145 foreign_configuration_formats[ 2 ctx 1ah

1146 _types.ForeignConfigurationFormat.VAULT_STOREROOM

1147 ] = not storeroom.STUBBED

1148 foreign_configuration_formats[ 2 ctx 1ah

1149 _types.ForeignConfigurationFormat.VAULT_V02

1150 ] = not vault_native.STUBBED

1151 foreign_configuration_formats[ 2 ctx 1ah

1152 _types.ForeignConfigurationFormat.VAULT_V03

1153 ] = not vault_native.STUBBED

1154 known_extras[_types.PEP508Extra.EXPORT] = ( 2 ctx 1ah

1155 not storeroom.STUBBED and not vault_native.STUBBED

1156 )

1157 click.echo() 2 ctx 1ah

1158 version_info_types: dict[_msg.Label, list[str]] = { 2 ctx 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) 2 ctx 1ah

1170 ctx.exit() 2 ctx 1ah

1171

1172

1173 def vault_version_option_callback( (empty)

1174 ctx: click.Context,

1175 param: click.Parameter,

1176 value: bool, # noqa: FBT001

1177) -> None:

1178 if value and not ctx.resilient_parsing: 72 ctx 1b5eaqkgp!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4

1179 common_version_output(ctx, param, value) 3 ctx 1akp

1180 features = { 3 ctx 1akp

1181 _types.Feature.SSH_KEY: hasattr(socket, 'AF_UNIX'),

1182 }

1183 click.echo() 3 ctx 1akp

1184 version_info_types: dict[_msg.Label, list[str]] = { 3 ctx 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) 3 ctx 1akp

1193 ctx.exit() 3 ctx 1akp

1194

1195

1196 def version_option( (empty)

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( (empty)

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 )

1210

1211

1212 color_forcing_pseudo_option = click.option( (empty)

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)

1222

1223

1224 class PassphraseGenerationOption(OptionGroupOption): (empty)

1225 """Passphrase generation options for the CLI."""

1226

1227 option_group_name = _msg.TranslatedString( (empty)

1228 _msg.Label.PASSPHRASE_GENERATION_LABEL

1229 )

1230 epilog = _msg.TranslatedString( (empty)

1231 _msg.Label.PASSPHRASE_GENERATION_EPILOG,

1232 metavar=_msg.TranslatedString(

1233 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1234 ),

1235 )

1236

1237

1238 class ConfigurationOption(OptionGroupOption): (empty)

1239 """Configuration options for the CLI."""

1240

1241 option_group_name = _msg.TranslatedString(_msg.Label.CONFIGURATION_LABEL) (empty)

1242 epilog = _msg.TranslatedString(_msg.Label.CONFIGURATION_EPILOG) (empty)

1243

1244

1245 class StorageManagementOption(OptionGroupOption): (empty)

1246 """Storage management options for the CLI."""

1247

1248 option_group_name = _msg.TranslatedString( (empty)

1249 _msg.Label.STORAGE_MANAGEMENT_LABEL

1250 )

1251 epilog = _msg.TranslatedString( (empty)

1252 _msg.Label.STORAGE_MANAGEMENT_EPILOG,

1253 metavar=_msg.TranslatedString(

1254 _msg.Label.STORAGE_MANAGEMENT_METAVAR_PATH

1255 ),

1256 )

1257

1258

1259 class CompatibilityOption(OptionGroupOption): (empty)

1260 """Compatibility and incompatibility options for the CLI."""

1261

1262 option_group_name = _msg.TranslatedString( (empty)

1263 _msg.Label.COMPATIBILITY_OPTION_LABEL

1264 )

1265

1266

1267 class LoggingOption(OptionGroupOption): (empty)

1268 """Logging options for the CLI."""

1269

1270 option_group_name = _msg.TranslatedString(_msg.Label.LOGGING_LABEL) (empty)

1271 epilog = '' (empty)

1272

1273

1274 debug_option = click.option( (empty)

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)

1284 verbose_option = click.option( (empty)

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)

1295 quiet_option = click.option( (empty)

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)

1306

1307

1308 def standard_logging_options(f: Callable[P, R]) -> Callable[P, R]: (empty)

1309 """Decorate the function with standard logging click options.

1310

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).

1314

1315 Args:

1316 f: A callable to decorate.

1317

1318 Returns:

1319 The decorated callable.

1320

1321 """

1322 return debug_option(verbose_option(quiet_option(f))) (empty)

1323

1324

1325# Shell completion

1326# ================

1327

1328

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

1333 class ZshComplete(click.shell_completion.ZshComplete): (empty)

1334 """Zsh completion class that supports colons.

1335

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.

1342

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.

1350

1351 """

1352

1353 @override (empty)

1354 def format_completion( (empty)

1355 self,

1356 item: click.shell_completion.CompletionItem,

1357 ) -> str:

1358 """Return a suitable serialization of the CompletionItem.

1359

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.

1365

1366 """

1367 help_ = item.help or '_' 1 ctx 16

1368 value = item.value.replace(':', r'\:' if help_ != '_' else ':') 1 ctx 16

1369 return f'{item.type}\n{value}\n{help_}' 1 ctx 16

1370

1371

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 = """\ (empty)

1379#compdef %(prog_name)s

1380

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

1386

1387 response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \

1388%(complete_var)s=zsh_complete %(prog_name)s)}")

1389

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

1403

1404 if [ -n "$completions_with_descriptions" ]; then

1405 _describe -V unsorted completions_with_descriptions -U

1406 fi

1407

1408 if [ -n "$completions" ]; then

1409 compadd -U -V unsorted -a completions

1410 fi

1411}

1412

1413 if [[ $zsh_eval_context[-1] == loadautofunc ]]; then

1414 # autoload from fpath, call function directly

1415 %(complete_func)s "$@"

1416 else

1417 # eval/source/. command, register function for later

1418 compdef %(complete_func)s %(prog_name)s

1419 fi

1420"""

1421 if ( (empty)

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) (empty)