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

339 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-23 12:17 +0200

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

2# 

3# SPDX-License-Identifier: Zlib 

4 

5 

6"""Command-line machinery for derivepassphrase. 

7 

8Warning: 

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 

15from __future__ import annotations 

16 

17import collections 

18import importlib.metadata 

19import inspect 

20import logging 

21import socket 

22import warnings 

23from typing import TYPE_CHECKING, Callable, Literal, TextIO, TypeVar 

24 

25import click 

26import click.shell_completion 

27from typing_extensions import Any, ParamSpec, override 

28 

29from derivepassphrase import _internals, _types 

30from derivepassphrase._internals import cli_messages as _msg 

31 

32if TYPE_CHECKING: 

33 import types 

34 from collections.abc import ( 

35 MutableSequence, 

36 ) 

37 

38 from typing_extensions import Self 

39 

40PROG_NAME = _internals.PROG_NAME 

41VERSION = _internals.VERSION 

42VERSION_OUTPUT_WRAPPING_WIDTH = 72 

43 

44# Error messages 

45NOT_AN_INTEGER = 'not an integer' 

46NOT_A_NONNEGATIVE_INTEGER = 'not a non-negative integer' 

47NOT_A_POSITIVE_INTEGER = 'not a positive integer' 

48 

49 

50# Logging 

51# ======= 

52 

53 

54class ClickEchoStderrHandler(logging.Handler): 

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: 

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

75class CLIofPackageFormatter(logging.Formatter): 

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__( 

98 self, 

99 *, 

100 prog_name: str = PROG_NAME, 

101 package_name: str | None = None, 

102 ) -> None: 

103 self.prog_name = prog_name 

104 self.package_name = ( 

105 package_name 

106 if package_name is not None 

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

108 ) 

109 

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

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() 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4

146 prefix = f'{self.prog_name}: ' 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4

147 if record.levelname == 'DEBUG': # pragma: no cover 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4

148 # Defensive programming, and currently not in production 

149 # use, so no coverage. 

150 level_indicator = 'Debug: ' 

151 elif record.levelname == 'INFO': 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4

152 level_indicator = '' 1T

153 elif record.levelname == 'WARNING': 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4

154 level_indicator = ( 2q W s X P F z j v T Y eb; l m Z n 0 1 2 3 4

155 f'{click.style("Deprecation warning", bold=True)}: ' 

156 if record.name.endswith('.deprecation') 

157 else f'{click.style("Warning", bold=True)}: ' 

158 ) 

159 elif record.levelname in {'ERROR', 'CRITICAL'}: 1GHIAJKLtMurNBCODQRESFUV

160 level_indicator = '' 1GHIAJKLtMurNBCODQRESFUV

161 else: # pragma: no cover 

162 # Defensive programming, so no coverage. 

163 msg = f'Unsupported logging level: {record.levelname}' 

164 raise AssertionError(msg) 

165 parts = [ 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4

166 ''.join( 

167 prefix + level_indicator + line 

168 for line in preliminary_result.splitlines(True) # noqa: FBT003 

169 ) 

170 ] 

171 if record.exc_info: 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4

172 parts.append(self.formatException(record.exc_info) + '\n') 1ArBCDE

173 return ''.join(parts) 2q W s X G H I A J K L t M u r N B C O D P Q R E S F z j v T Y eb; l m Z n 0 1 2 U 3 V 4

174 

175 

176class StandardCLILogging: 

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

178 

179 prog_name = PROG_NAME 

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

181 cli_formatter = CLIofPackageFormatter( 

182 prog_name=prog_name, package_name=package_name 

183 ) 

184 cli_handler = ClickEchoStderrHandler() 

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

186 cli_handler.setFormatter(cli_formatter) 

187 cli_handler.setLevel(logging.WARNING) 

188 warnings_handler = ClickEchoStderrHandler() 

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

190 warnings_handler.setFormatter(cli_formatter) 

191 warnings_handler.setLevel(logging.WARNING) 

192 

193 @classmethod 

194 def ensure_standard_logging(cls) -> StandardLoggingContextManager: 

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

196 return StandardLoggingContextManager( 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ebab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

197 handler=cls.cli_handler, 

198 root_logger=cls.package_name, 

199 ) 

200 

201 @classmethod 

202 def ensure_standard_warnings_logging( 

203 cls, 

204 ) -> StandardWarningsLoggingContextManager: 

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

206 return StandardWarningsLoggingContextManager( 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

207 handler=cls.warnings_handler, 

208 ) 

209 

210 

211class StandardLoggingContextManager: 

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__( 

225 self, 

226 handler: logging.Handler, 

227 root_logger: str | None = None, 

228 ) -> None: 

229 self.handler = handler 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

230 self.root_logger_name = root_logger 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

231 self.base_logger = logging.getLogger(self.root_logger_name) 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

232 self.action_required: MutableSequence[bool] = collections.deque() 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

233 

234 def __enter__(self) -> Self: 

235 self.action_required.append( 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

236 self.handler not in self.base_logger.handlers 

237 ) 

238 if self.action_required[-1]: 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

239 self.base_logger.addHandler(self.handler) 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

240 return self 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

241 

242 def __exit__( 

243 self, 

244 exc_type: type[BaseException] | None, 

245 exc_value: BaseException | None, 

246 exc_tb: types.TracebackType | None, 

247 ) -> Literal[False]: 

248 if self.action_required[-1]: 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

249 self.base_logger.removeHandler(self.handler) 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

250 self.action_required.pop() 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

251 return False 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ eb; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

252 

253 

254class StandardWarningsLoggingContextManager(StandardLoggingContextManager): 

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__( 

268 self, 

269 handler: logging.Handler, 

270 ) -> None: 

271 super().__init__(handler=handler, root_logger='py.warnings') 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

272 self.stack: MutableSequence[ 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

273 tuple[ 

274 Callable[ 

275 [ 

276 type[BaseException] | None, 

277 BaseException | None, 

278 types.TracebackType | None, 

279 ], 

280 None, 

281 ], 

282 Callable[ 

283 [ 

284 str | Warning, 

285 type[Warning], 

286 str, 

287 int, 

288 TextIO | None, 

289 str | None, 

290 ], 

291 None, 

292 ], 

293 ] 

294 ] = collections.deque() 

295 

296 def __enter__(self) -> Self: 

297 def showwarning( # noqa: PLR0913,PLR0917 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

298 message: str | Warning, 

299 category: type[Warning], 

300 filename: str, 

301 lineno: int, 

302 file: TextIO | None = None, 

303 line: str | None = None, 

304 ) -> None: 

305 if file is not None: # pragma: no cover 1;

306 # Defensive programming (API defined by external source, 

307 # and this code path is not triggered by our code), so 

308 # no coverage. 

309 self.stack[0][1]( 

310 message, category, filename, lineno, file, line 

311 ) 

312 else: 

313 logging.getLogger('py.warnings').warning( 1;

314 str( 

315 warnings.formatwarning( 

316 message, category, filename, lineno, line 

317 ) 

318 ) 

319 ) 

320 

321 ctx = warnings.catch_warnings() 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

322 exit_func = ctx.__exit__ 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

323 ctx.__enter__() 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

324 self.stack.append((exit_func, warnings.showwarning)) 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

325 warnings.showwarning = showwarning 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

326 return super().__enter__() 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

327 

328 def __exit__( 

329 self, 

330 exc_type: type[BaseException] | None, 

331 exc_value: BaseException | None, 

332 exc_tb: types.TracebackType | None, 

333 ) -> Literal[False]: 

334 ret = super().__exit__(exc_type, exc_value, exc_tb) 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

335 val = self.stack.pop()[0](exc_type, exc_value, exc_tb) 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

336 assert not val 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

337 return ret 25 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F ^ _ ` { z j v T Y | } ~ ; ab. 9 6 l bbcbm Z n 0 db/ 1 : 2 U 3 V 4

338 

339 

340P = ParamSpec('P') 

341R = TypeVar('R') 

342 

343 

344def adjust_logging_level( 

345 ctx: click.Context, 

346 /, 

347 param: click.Parameter | None = None, 

348 value: int | None = None, 

349) -> None: 

350 """Change the logs that are emitted to standard error. 

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: 2b 5 c f e a q i h k ! # ( ) * + W x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbob

360 return 2b 5 c f e a q i h k ! # ( ) * + W x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbob

361 StandardCLILogging.cli_handler.setLevel(value) 1P

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

363 

364 

365# Option parsing and grouping 

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

367 

368 

369class OptionGroupOption(click.Option): 

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 = '' 

386 """""" 

387 epilog: object = '' 

388 """""" 

389 

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

391 if self.__class__ == __class__: # type: ignore[name-defined] 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob

392 raise NotImplementedError 

393 # Though click 8.1 mostly defers help text processing until the 

394 # `BaseCommand.format_*` methods are called, the Option 

395 # constructor still preprocesses the help text, and asserts that 

396 # the help text is a string. Work around this by removing the 

397 # help text from the constructor arguments and re-adding it, 

398 # unprocessed, after constructor finishes. 

399 unset = object() 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob

400 help = kwargs.pop('help', unset) # noqa: A001 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob

401 super().__init__(*args, **kwargs) 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob

402 if help is not unset: # pragma: no branch 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob

403 self.help = help 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob

404 

405 

406class StandardOption(OptionGroupOption): 

407 pass 

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. 

450class CommandWithHelpGroups(click.Command): 

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 

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

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

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

469 return str(text) 1dcfeag

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]: 

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 [] 2d c f e a g w x y t 7 % ' z v fb

482 for param in self.get_params(ctx): 2d c f e a g w x y t 7 % ' z v fb

483 rv.extend(str(x) for x in param.get_usage_pieces(ctx)) 2d c f e a g w x y t 7 % ' z v fb

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

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( 

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) 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 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( 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob

508 ctx: click.Context, 

509 param: click.Parameter, # noqa: ARG001 

510 value: str, 

511 ) -> None: 

512 if value and not ctx.resilient_parsing: 2b 5 d c f e a q i h k g ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob

513 click.echo(ctx.get_help(), color=ctx.color) 1dcfeag

514 ctx.exit() 1dcfeag

515 

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

517 # object as the help string. 

518 return StandardOption( 2b 5 d c f e a q o i h k g p ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob

519 help_options, 

520 is_flag=True, 

521 is_eager=True, 

522 expose_value=False, 

523 callback=show_help, 

524 help=_msg.TranslatedString(_msg.Label.HELP_OPTION_HELP_TEXT), 

525 ) 

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( 

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 1dca]

547 # Defensive programming (API defined by external source, and 

548 # this code path is not triggered by our code), so no 

549 # coverage. 

550 text = inspect.cleandoc(self._text(self.short_help)) 

551 elif self.help: 1dca]

552 text = click.utils.make_default_short_help( 1dca]

553 self._text(self.help), limit 

554 ) 

555 else: # pragma: no cover 

556 # Defensive programming (API defined by external source, and 

557 # this code path is not triggered by our code), so no 

558 # coverage. 

559 text = '' 

560 if self.deprecated: # pragma: no cover 1dca]

561 # Defensive programming (API defined by external source, and 

562 # this code path is not triggered by our code), so no 

563 # coverage. 

564 # 

565 # Modification against click 8.1: The translated string is 

566 # looked up in the derivepassphrase message domain, not the 

567 # gettext default domain. 

568 text = str( 

569 _msg.TranslatedString(_msg.Label.DEPRECATED_COMMAND_LABEL) 

570 ).format(text=text) 

571 return text.strip() 1dca]

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( 

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 1dcfeag

590 # Modification against click 8.1: Call `_text()` on `self.help` 

591 # to allow help texts to be general objects, not just strings. 

592 # Used to implement translatable strings, as objects that 

593 # stringify to the translation. 

594 text = ( 1dcfeag

595 inspect.cleandoc(self._text(self.help).partition('\f')[0]) 

596 if self.help is not None 

597 else '' 

598 ) 

599 if self.deprecated: # pragma: no cover 1dcfeag

600 # Defensive programming (API defined by external source, and 

601 # this code path is not triggered by our code), so no 

602 # coverage. 

603 # 

604 # Modification against click 8.1: The translated string is 

605 # looked up in the derivepassphrase message domain, not the 

606 # gettext default domain. 

607 text = str( 

608 _msg.TranslatedString(_msg.Label.DEPRECATED_COMMAND_LABEL) 

609 ).format(text=text) 

610 if text: # pragma: no branch 1dcfeag

611 formatter.write_paragraph() 1dcfeag

612 with formatter.indentation(): 1dcfeag

613 formatter.write_text(text) 1dcfeag

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( 

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 = '' 1dcfeag

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

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

651 params = self.params[:] 1dcfeag

652 if ( # pragma: no branch 1dcfeag

653 (help_opt := self.get_help_option(ctx)) is not None 

654 and help_opt not in params 

655 ): 

656 params.append(help_opt) 1dcfeag

657 for param in params: 1dcfeag

658 rec = param.get_help_record(ctx) 1dcfeag

659 if rec is not None: 1dcfeag

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

661 if isinstance(param, OptionGroupOption): 1dcfeag

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

663 epilogs.setdefault(group_name, self._text(param.epilog)) 1dcfeag

664 else: # pragma: no cover 

665 # Defensive programming (API defined by external 

666 # source, and this code path is not triggered by our 

667 # code), so no coverage. 

668 group_name = default_group_name 

669 help_records.setdefault(group_name, []).append(rec) 1dcfeag

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

671 default_group = help_records.pop(default_group_name) 1dcfeag

672 default_group_label = ( 1dcfeag

673 _msg.Label.OTHER_OPTIONS_LABEL 

674 if len(default_group) > 1 

675 else _msg.Label.OPTIONS_LABEL 

676 ) 

677 default_group_name = self._text( 1dcfeag

678 _msg.TranslatedString(default_group_label) 

679 ) 

680 help_records[default_group_name] = default_group 1dcfeag

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

682 with formatter.section(group_name): 1dcfeag

683 formatter.write_dl(records) 1dcfeag

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

685 if epilog: 1dcfeag

686 formatter.write_paragraph() 1eag

687 with formatter.indentation(): 1eag

688 formatter.write_text(epilog) 1eag

689 self.format_commands(ctx, formatter) 1dcfeag

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( 

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): 1dcfeag

713 return 1feag

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

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

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

717 if cmd is None or cmd.hidden: # pragma: no cover 1dca

718 # Defensive programming (API defined by external source, 

719 # and this code path is not triggered by our code), so 

720 # no coverage. 

721 continue 

722 commands.append((subcommand, cmd)) 1dca

723 if commands: # pragma: no branch 1dca

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

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

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

727 for subcommand, cmd in commands: 1dca

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

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

730 if rows: # pragma: no branch 1dca

731 commands_label = self._text( 1dca

732 _msg.TranslatedString(_msg.Label.COMMANDS_LABEL) 

733 ) 

734 with formatter.section(commands_label): 1dca

735 formatter.write_dl(rows) 1dca

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( 

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 1dcfeag

754 if self.epilog: # pragma: no branch 1dcfeag

755 # Modification against click 8.1: Call `str()` on 

756 # `self.epilog` to allow help texts to be general objects, 

757 # not just strings. Used to implement translatable strings, 

758 # as objects that stringify to the translation. 

759 epilog = inspect.cleandoc(self._text(self.epilog)) 1deag

760 formatter.write_paragraph() 1deag

761 with formatter.indentation(): 1deag

762 formatter.write_text(epilog) 1deag

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 

808class DefaultToVaultGroup(CommandWithHelpGroups, click.Group): 

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( 

817 self, ctx: click.Context, args: list[str] 

818 ) -> tuple[str | None, click.Command | None, list[str]]: 

819 """Resolve a command, defaulting to "vault" instead of erroring out.""" # noqa: DOC201 

820 cmd_name = click.utils.make_str(args[0]) 1cfeaqihk8[zj?]@96l

821 

822 # Get the command 

823 cmd = self.get_command(ctx, cmd_name) 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 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: 1cfeaqihk8[zj?]@96l

843 #### 

844 # BEGIN modifications for derivepassphrase 

845 # 

846 # Instead of using 

847 # 

848 # if click.parsers.split_opt(cmd_name)[0] 

849 # 

850 # which splits the option prefix (typically `-` or `--`) from 

851 # the option name, but triggers deprecation warnings in click 

852 # 8.2.0 and later, we check directly for a `-` prefix. 

853 # 

854 # END modifications for derivepassphrase 

855 #### 

856 if cmd_name.startswith('-'): 1[j

857 self.parse_args(ctx, ctx.args) 1j

858 #### 

859 # BEGIN modifications for derivepassphrase 

860 # 

861 # Instead of calling ctx.fail here, default to "vault", and 

862 # issue a deprecation warning. 

863 deprecation = logging.getLogger(f'{PROG_NAME}.deprecation') 1[j

864 deprecation.warning( 1[j

865 _msg.TranslatedString( 

866 _msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED 

867 ) 

868 ) 

869 cmd_name = 'vault' 1[j

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

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

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

873 # 

874 # END modifications for derivepassphrase 

875 #### 

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

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 

882class TopLevelCLIEntryPoint(DefaultToVaultGroup): 

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 

896 self, 

897 *args: Any, # noqa: ANN401 

898 **kwargs: Any, # noqa: ANN401 

899 ) -> Any: # noqa: ANN401 

900 """""" # noqa: D419 

901 # Coverage testing is done with the `click.testing` module, 

902 # which does not use the `__call__` shortcut. So it is normal 

903 # that this function is never called, and thus should be 

904 # excluded from coverage. 

905 with ( 

906 StandardCLILogging.ensure_standard_logging(), 

907 StandardCLILogging.ensure_standard_warnings_logging(), 

908 ): 

909 return self.main(*args, **kwargs) 

910 

911 

912# Actual option groups and callbacks used by derivepassphrase 

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

914 

915 

916def color_forcing_callback( 

917 ctx: click.Context, 

918 param: click.Parameter, 

919 value: Any, # noqa: ANN401 

920) -> None: 

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

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 2b 5 d c f e a q i h k g ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob

937 ctx.color = False 2b 5 d c f e a q i h k g ! # ( ) * + W w x s y X G H I , 8 A J K L $ t M u r 7 N B C % ' - O D P Q R E S F [ z j v T Y . = ? ] @ 9 6 l m Z n 0 / 1 : 2 U 3 V 4 gbhbibjbkblbmbnbfbob

938 

939 

940def validate_occurrence_constraint( 

941 ctx: click.Context, 

942 param: click.Parameter, 

943 value: Any, # noqa: ANN401 

944) -> int | None: 

945 """Check that the occurrence constraint is valid (int, 0 or larger). 

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. 1b5q!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4

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

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

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

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

964 int_value = value 1=

965 else: 

966 try: 1b5!#wxyjmn

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

968 except ValueError as exc: 1w

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

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

971 raise click.BadParameter(NOT_A_NONNEGATIVE_INTEGER) 1w

972 return int_value 1b5!#xyj=mn

973 

974 

975def validate_length( 

976 ctx: click.Context, 

977 param: click.Parameter, 

978 value: Any, # noqa: ANN401 

979) -> int | None: 

980 """Check that the length is valid (int, 1 or larger). 

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. 1b5q!#()*+WwxsyXGHI,8AJKL$tMur7NBC%'-ODPQRESFjvTY.=?@96lmZn0/1:2U3V4

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

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

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

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

999 int_value = value 1=

1000 else: 

1001 try: 1b5wxsy$ur7lmn

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

1003 except ValueError as exc: 1w

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

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

1006 raise click.BadParameter(NOT_A_POSITIVE_INTEGER) 1w

1007 return int_value 1b5xsy$ur7=lmn

1008 

1009 

1010def common_version_output( 

1011 ctx: click.Context, 

1012 param: click.Parameter, 

1013 value: bool, # noqa: FBT001 

1014) -> None: 

1015 del param, value 1aoihkp

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

1017 try: 1aoihkp

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

1019 except ModuleNotFoundError: 1aoihkp

1020 pass 1aoihkp

1021 else: 

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

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

1024 

1025 click.echo( 1aoihkp

1026 ' '.join([ 

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

1028 VERSION, 

1029 ]), 

1030 color=ctx.color, 

1031 ) 

1032 for dependency in major_dependencies: 1aoihkp

1033 click.echo( 1aoihkp

1034 str( 

1035 _msg.TranslatedString( 

1036 _msg.Label.VERSION_INFO_MAJOR_LIBRARY_TEXT, 

1037 dependency_name_and_version=dependency, 

1038 ) 

1039 ), 

1040 color=ctx.color, 

1041 ) 

1042 

1043 

1044def print_version_info_types( 

1045 version_info_types: dict[_msg.Label, list[str]], 

1046 /, 

1047 *, 

1048 ctx: click.Context, 

1049) -> None: 

1050 for message_label, item_list in version_info_types.items(): 1aoihkp

1051 if item_list: 1aoihkp

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

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

1054 n = len(item_list) 1aoihkp

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

1056 space = ' ' 1aoihkp

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

1058 if ( 1aoihkp

1059 current_length + len(space) + len(item) + len(punctuation) 

1060 <= VERSION_OUTPUT_WRAPPING_WIDTH 

1061 ): 

1062 current_length += len(space) + len(item) + len(punctuation) 1aoihkp

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

1064 else: 

1065 space = ' ' 1aih

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

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

1068 formatted_item_list_pieces.append(piece) 1aoihkp

1069 click.echo( 1aoihkp

1070 ''.join([ 

1071 click.style( 

1072 str(_msg.TranslatedString(message_label)), 

1073 bold=True, 

1074 ), 

1075 ''.join(formatted_item_list_pieces), 

1076 ]), 

1077 color=ctx.color, 

1078 ) 

1079 

1080 

1081def derivepassphrase_version_option_callback( 

1082 ctx: click.Context, 

1083 param: click.Parameter, 

1084 value: bool, # noqa: FBT001 

1085) -> None: 

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

1087 common_version_output(ctx, param, value) 1ao

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

1089 supported_subcommands = set(_types.Subcommand) 1ao

1090 click.echo() 1ao

1091 version_info_types: dict[_msg.Label, list[str]] = { 1ao

1092 _msg.Label.SUPPORTED_DERIVATION_SCHEMES: [ 

1093 k for k, v in derivation_schemes.items() if v 

1094 ], 

1095 _msg.Label.UNAVAILABLE_DERIVATION_SCHEMES: [ 

1096 k for k, v in derivation_schemes.items() if not v 

1097 ], 

1098 _msg.Label.SUPPORTED_SUBCOMMANDS: sorted(supported_subcommands), 

1099 } 

1100 print_version_info_types(version_info_types, ctx=ctx) 1ao

1101 ctx.exit() 1ao

1102 

1103 

1104def export_version_option_callback( 

1105 ctx: click.Context, 

1106 param: click.Parameter, 

1107 value: bool, # noqa: FBT001 

1108) -> None: 

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

1110 common_version_output(ctx, param, value) 1ai

1111 supported_subcommands = set(_types.ExportSubcommand) 1ai

1112 foreign_configuration_formats = { 1ai

1113 _types.ForeignConfigurationFormat.VAULT_STOREROOM: False, 

1114 _types.ForeignConfigurationFormat.VAULT_V02: False, 

1115 _types.ForeignConfigurationFormat.VAULT_V03: False, 

1116 } 

1117 click.echo() 1ai

1118 version_info_types: dict[_msg.Label, list[str]] = { 1ai

1119 _msg.Label.UNAVAILABLE_FOREIGN_CONFIGURATION_FORMATS: [ 

1120 k for k, v in foreign_configuration_formats.items() if not v 

1121 ], 

1122 _msg.Label.SUPPORTED_SUBCOMMANDS: sorted(supported_subcommands), 

1123 } 

1124 print_version_info_types(version_info_types, ctx=ctx) 1ai

1125 ctx.exit() 1ai

1126 

1127 

1128def export_vault_version_option_callback( 

1129 ctx: click.Context, 

1130 param: click.Parameter, 

1131 value: bool, # noqa: FBT001 

1132) -> None: 

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

1134 common_version_output(ctx, param, value) 1ah

1135 foreign_configuration_formats = { 1ah

1136 _types.ForeignConfigurationFormat.VAULT_STOREROOM: False, 

1137 _types.ForeignConfigurationFormat.VAULT_V02: False, 

1138 _types.ForeignConfigurationFormat.VAULT_V03: False, 

1139 } 

1140 known_extras = { 1ah

1141 _types.PEP508Extra.EXPORT: False, 

1142 } 

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

1144 

1145 foreign_configuration_formats[ 1ah

1146 _types.ForeignConfigurationFormat.VAULT_STOREROOM 

1147 ] = not storeroom.STUBBED 

1148 foreign_configuration_formats[ 1ah

1149 _types.ForeignConfigurationFormat.VAULT_V02 

1150 ] = not vault_native.STUBBED 

1151 foreign_configuration_formats[ 1ah

1152 _types.ForeignConfigurationFormat.VAULT_V03 

1153 ] = not vault_native.STUBBED 

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

1155 not storeroom.STUBBED and not vault_native.STUBBED 

1156 ) 

1157 click.echo() 1ah

1158 version_info_types: dict[_msg.Label, list[str]] = { 1ah

1159 _msg.Label.SUPPORTED_FOREIGN_CONFIGURATION_FORMATS: [ 

1160 k for k, v in foreign_configuration_formats.items() if v 

1161 ], 

1162 _msg.Label.UNAVAILABLE_FOREIGN_CONFIGURATION_FORMATS: [ 

1163 k for k, v in foreign_configuration_formats.items() if not v 

1164 ], 

1165 _msg.Label.ENABLED_PEP508_EXTRAS: [ 

1166 k for k, v in known_extras.items() if v 

1167 ], 

1168 } 

1169 print_version_info_types(version_info_types, ctx=ctx) 1ah

1170 ctx.exit() 1ah

1171 

1172 

1173def vault_version_option_callback( 

1174 ctx: click.Context, 

1175 param: click.Parameter, 

1176 value: bool, # noqa: FBT001 

1177) -> None: 

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

1179 common_version_output(ctx, param, value) 1akp

1180 features = { 1akp

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

1182 } 

1183 click.echo() 1akp

1184 version_info_types: dict[_msg.Label, list[str]] = { 1akp

1185 _msg.Label.SUPPORTED_FEATURES: [ 

1186 k for k, v in features.items() if v 

1187 ], 

1188 _msg.Label.UNAVAILABLE_FEATURES: [ 

1189 k for k, v in features.items() if not v 

1190 ], 

1191 } 

1192 print_version_info_types(version_info_types, ctx=ctx) 1akp

1193 ctx.exit() 1akp

1194 

1195 

1196def version_option( 

1197 version_option_callback: Callable[ 

1198 [click.Context, click.Parameter, Any], Any 

1199 ], 

1200) -> Callable[[Callable[P, R]], Callable[P, R]]: 

1201 return click.option( 

1202 '--version', 

1203 is_flag=True, 

1204 is_eager=True, 

1205 expose_value=False, 

1206 callback=version_option_callback, 

1207 cls=StandardOption, 

1208 help=_msg.TranslatedString(_msg.Label.VERSION_OPTION_HELP_TEXT), 

1209 ) 

1210 

1211 

1212color_forcing_pseudo_option = click.option( 

1213 '--_pseudo-option-color-forcing', 

1214 '_color_forcing', 

1215 is_flag=True, 

1216 is_eager=True, 

1217 expose_value=False, 

1218 hidden=True, 

1219 callback=color_forcing_callback, 

1220 help='(pseudo-option)', 

1221) 

1222 

1223 

1224class PassphraseGenerationOption(OptionGroupOption): 

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

1226 

1227 option_group_name = _msg.TranslatedString( 

1228 _msg.Label.PASSPHRASE_GENERATION_LABEL 

1229 ) 

1230 epilog = _msg.TranslatedString( 

1231 _msg.Label.PASSPHRASE_GENERATION_EPILOG, 

1232 metavar=_msg.TranslatedString( 

1233 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1234 ), 

1235 ) 

1236 

1237 

1238class ConfigurationOption(OptionGroupOption): 

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

1240 

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

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

1243 

1244 

1245class StorageManagementOption(OptionGroupOption): 

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

1247 

1248 option_group_name = _msg.TranslatedString( 

1249 _msg.Label.STORAGE_MANAGEMENT_LABEL 

1250 ) 

1251 epilog = _msg.TranslatedString( 

1252 _msg.Label.STORAGE_MANAGEMENT_EPILOG, 

1253 metavar=_msg.TranslatedString( 

1254 _msg.Label.STORAGE_MANAGEMENT_METAVAR_PATH 

1255 ), 

1256 ) 

1257 

1258 

1259class CompatibilityOption(OptionGroupOption): 

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

1261 

1262 option_group_name = _msg.TranslatedString( 

1263 _msg.Label.COMPATIBILITY_OPTION_LABEL 

1264 ) 

1265 

1266 

1267class LoggingOption(OptionGroupOption): 

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

1269 

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

1271 epilog = '' 

1272 

1273 

1274debug_option = click.option( 

1275 '--debug', 

1276 'logging_level', 

1277 is_flag=True, 

1278 flag_value=logging.DEBUG, 

1279 expose_value=False, 

1280 callback=adjust_logging_level, 

1281 help=_msg.TranslatedString(_msg.Label.DEBUG_OPTION_HELP_TEXT), 

1282 cls=LoggingOption, 

1283) 

1284verbose_option = click.option( 

1285 '-v', 

1286 '--verbose', 

1287 'logging_level', 

1288 is_flag=True, 

1289 flag_value=logging.INFO, 

1290 expose_value=False, 

1291 callback=adjust_logging_level, 

1292 help=_msg.TranslatedString(_msg.Label.VERBOSE_OPTION_HELP_TEXT), 

1293 cls=LoggingOption, 

1294) 

1295quiet_option = click.option( 

1296 '-q', 

1297 '--quiet', 

1298 'logging_level', 

1299 is_flag=True, 

1300 flag_value=logging.ERROR, 

1301 expose_value=False, 

1302 callback=adjust_logging_level, 

1303 help=_msg.TranslatedString(_msg.Label.QUIET_OPTION_HELP_TEXT), 

1304 cls=LoggingOption, 

1305) 

1306 

1307 

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

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

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 

1333class ZshComplete(click.shell_completion.ZshComplete): 

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 

1354 def format_completion( 

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 '_' 16

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

1369 return f'{item.type}\n{value}\n{help_}' 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 = """\ 

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 

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

1414 # autoload from fpath, call function directly 

1415 %(complete_func)s "$@" 

1416else 

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

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

1419fi 

1420""" 

1421if ( 

1422 click.shell_completion.ZshComplete.source_template == _ORIG_SOURCE_TEMPLATE 

1423): # pragma: no cover 

1424 # Defensive programming (API defined by external source), so no 

1425 # coverage. 

1426 click.shell_completion.add_completion_class(ZshComplete)