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

341 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 21:34 +0200

1# SPDX-FileCopyrightText: 2026 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 warnings 

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

23 

24import click 

25import click.shell_completion 

26from typing_extensions import Any, ParamSpec, override 

27 

28from derivepassphrase import _internals, _types 

29from derivepassphrase._internals import cli_messages as _msg 

30 

31if TYPE_CHECKING: 

32 import types 

33 from collections.abc import ( 

34 MutableSequence, 

35 ) 

36 

37 from typing_extensions import Self 

38 

39PROG_NAME = _internals.PROG_NAME 

40VERSION = _internals.VERSION 

41VERSION_OUTPUT_WRAPPING_WIDTH = 72 

42 

43# Error messages 

44NOT_AN_INTEGER = "not an integer" 

45NOT_A_NONNEGATIVE_INTEGER = "not a non-negative integer" 

46NOT_A_POSITIVE_INTEGER = "not a positive integer" 

47 

48 

49# Logging 

50# ======= 

51 

52 

53class ClickEchoStderrHandler(logging.Handler): 

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

55 

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

57 

58 """ 

59 

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

61 """Emit a log record. 

62 

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

64 [`sys.stderr`][]. 

65 

66 """ 

67 click.echo( 2i D E x y F n j w h p G U fb= k V l W q H X z I J K L M N Y O P Q R o r S s A B Z 0 T 1 C 2

68 self.format(record), 

69 err=True, 

70 color=getattr(record, "color", None), 

71 ) 

72 

73 

74class CLIofPackageFormatter(logging.Formatter): 

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

76 

77 Assuming a package `PKG` and loggers within the same hierarchy 

78 `PKG`, format all log records from that hierarchy for proper user 

79 feedback on the console. Intended for use with [`click`][CLICK] and 

80 when `PKG` provides a command-line tool `PKG` and when logs from 

81 that package should show up as output of the command-line tool. 

82 

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

84 lines to make them readable as standard error output. 

85 

86 Because this log output is intended to be displayed on standard 

87 error as high-level diagnostic output, you are strongly discouraged 

88 from changing the output format to include more tokens besides the 

89 log message. Use a dedicated log file handler instead, without this 

90 formatter. 

91 

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

93 

94 """ 

95 

96 def __init__( 

97 self, 

98 *, 

99 prog_name: str = PROG_NAME, 

100 package_name: str | None = None, 

101 ) -> None: 

102 self.prog_name = prog_name 

103 self.package_name = ( 

104 package_name 

105 if package_name is not None 

106 else prog_name.lower().replace(" ", "_").replace("-", "_") 

107 ) 

108 

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

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

111 

112 Prepend the formatted string `"PROG_NAME: LABEL"` to each line 

113 of the message, where `PROG_NAME` is the program name, and 

114 `LABEL` depends on the record's level and on the logger name as 

115 follows: 

116 

117 * For records at level [`logging.DEBUG`][], `LABEL` is 

118 `"Debug: "`. 

119 * For records at level [`logging.INFO`][], `LABEL` is the 

120 empty string. 

121 * For records at level [`logging.WARNING`][], `LABEL` is 

122 `"Deprecation warning: "` if the logger is named 

123 `PKG.deprecation` (where `PKG` is the package name), else 

124 `"Warning: "`. 

125 * For records at level [`logging.ERROR`][] and 

126 [`logging.CRITICAL`][] `"Error: "`, `LABEL` is the empty 

127 string. 

128 

129 The level indication strings at level `WARNING` or above are 

130 highlighted. Use [`click.echo`][] to output them and remove 

131 color output if necessary. 

132 

133 Args: 

134 record: A log record. 

135 

136 Returns: 

137 A formatted log record. 

138 

139 Raises: 

140 AssertionError: 

141 The log level is not supported. 

142 

143 """ 

144 preliminary_result = record.getMessage() 2i D E x y F n j w h p G U fb= k V l W q H X z I J K L M N Y O P Q R o r S s A B Z 0 T 1 C 2

145 prefix = f"{self.prog_name}: " 2i D E x y F n j w h p G U fb= k V l W q H X z I J K L M N Y O P Q R o r S s A B Z 0 T 1 C 2

146 if record.levelname == "DEBUG": # pragma: no cover [unused] 2i D E x y F n j w h p G U fb= k V l W q H X z I J K L M N Y O P Q R o r S s A B Z 0 T 1 C 2

147 level_indicator = "Debug: " 

148 elif record.levelname == "INFO": 2i D E x y F n j w h p G U fb= k V l W q H X z I J K L M N Y O P Q R o r S s A B Z 0 T 1 C 2

149 level_indicator = "" 1G

150 elif record.levelname == "WARNING": 2i D E x y F n j w h p G U fb= k V l W q H X z I J K L M N Y O P Q R o r S s A B Z 0 T 1 C 2

151 level_indicator = ( 2i E x n j w h p G U fb= k V l W q X Y Z 0 1 C 2

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

153 if record.name.endswith(".deprecation") 

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

155 ) 

156 elif record.levelname in {"ERROR", "CRITICAL"}: 1DxyFHzIJKLMNOPQRorSsABTC

157 level_indicator = "" 1DxyFHzIJKLMNOPQRorSsABTC

158 else: # pragma: no cover [failsafe] 

159 msg = f"Unsupported logging level: {record.levelname}" 

160 raise AssertionError(msg) 

161 parts = [ 2i D E x y F n j w h p G U fb= k V l W q H X z I J K L M N Y O P Q R o r S s A B Z 0 T 1 C 2

162 "".join( 

163 prefix + level_indicator + line 

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

165 ) 

166 ] 

167 if record.exc_info: 2i D E x y F n j w h p G U fb= k V l W q H X z I J K L M N Y O P Q R o r S s A B Z 0 T 1 C 2

168 parts.append(self.formatException(record.exc_info) + "\n") 1yzoAB

169 return "".join(parts) 2i D E x y F n j w h p G U fb= k V l W q H X z I J K L M N Y O P Q R o r S s A B Z 0 T 1 C 2

170 

171 

172class StandardCLILogging: 

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

174 

175 prog_name = PROG_NAME 

176 package_name = PROG_NAME.lower().replace(" ", "_").replace("-", "_") 

177 cli_formatter = CLIofPackageFormatter( 

178 prog_name=prog_name, package_name=package_name 

179 ) 

180 cli_handler = ClickEchoStderrHandler() 

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

182 cli_handler.setFormatter(cli_formatter) 

183 cli_handler.setLevel(logging.WARNING) 

184 warnings_handler = ClickEchoStderrHandler() 

185 warnings_handler.addFilter(logging.Filter(name="py.warnings")) 

186 warnings_handler.setFormatter(cli_formatter) 

187 warnings_handler.setLevel(logging.WARNING) 

188 

189 @classmethod 

190 def ensure_standard_logging(cls) -> StandardLoggingContextManager: 

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

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

193 handler=cls.cli_handler, 

194 root_logger=cls.package_name, 

195 ) 

196 

197 @classmethod 

198 def ensure_standard_warnings_logging( 

199 cls, 

200 ) -> StandardWarningsLoggingContextManager: 

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

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

203 handler=cls.warnings_handler, 

204 ) 

205 

206 

207class StandardLoggingContextManager: 

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

209 

210 Ensures that the given handler (defaulting to the CLI logging 

211 handler) is added to the named logger (defaulting to the root 

212 logger), and if it had to be added, then that it will be removed 

213 upon exiting the context. 

214 

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

216 global state. 

217 

218 """ 

219 

220 def __init__( 

221 self, 

222 handler: logging.Handler, 

223 root_logger: str | None = None, 

224 ) -> None: 

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

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

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

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

229 

230 def __enter__(self) -> Self: 

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

232 self.handler not in self.base_logger.handlers 

233 ) 

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

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

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

237 

238 def __exit__( 

239 self, 

240 exc_type: type[BaseException] | None, 

241 exc_value: BaseException | None, 

242 exc_tb: types.TracebackType | None, 

243 ) -> Literal[False]: 

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

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

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

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

248 

249 

250class StandardWarningsLoggingContextManager(StandardLoggingContextManager): 

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

252 

253 Ensures that warnings are being diverted to the logging system, and 

254 that the given handler (defaulting to the CLI logging handler) is 

255 added to the warnings logger. If the handler had to be added, then 

256 it will be removed upon exiting the context. 

257 

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

259 global state. 

260 

261 """ 

262 

263 def __init__( 

264 self, 

265 handler: logging.Handler, 

266 ) -> None: 

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

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

269 tuple[ 

270 Callable[ 

271 [ 

272 type[BaseException] | None, 

273 BaseException | None, 

274 types.TracebackType | None, 

275 ], 

276 None, 

277 ], 

278 Callable[ 

279 [ 

280 str | Warning, 

281 type[Warning], 

282 str, 

283 int, 

284 TextIO | None, 

285 str | None, 

286 ], 

287 None, 

288 ], 

289 ] 

290 ] = collections.deque() 

291 

292 def __enter__(self) -> Self: 

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

294 message: str | Warning, 

295 category: type[Warning], 

296 filename: str, 

297 lineno: int, 

298 file: TextIO | None = None, 

299 line: str | None = None, 

300 ) -> None: 

301 if file is not None: # pragma: no cover [external-api] 1i=

302 self.stack[0][1]( 

303 message, category, filename, lineno, file, line 

304 ) 

305 else: 

306 logging.getLogger("py.warnings").warning( 1i=

307 str( 

308 warnings.formatwarning( 

309 message, category, filename, lineno, line 

310 ) 

311 ) 

312 ) 

313 

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

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

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

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

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

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

320 

321 def __exit__( 

322 self, 

323 exc_type: type[BaseException] | None, 

324 exc_value: BaseException | None, 

325 exc_tb: types.TracebackType | None, 

326 ) -> Literal[False]: 

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

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

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

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

331 

332 

333P = ParamSpec("P") 

334R = TypeVar("R") 

335 

336 

337def adjust_logging_level( 

338 ctx: click.Context, 

339 /, 

340 param: click.Parameter | None = None, 

341 value: int | None = None, 

342) -> None: 

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

344 

345 This modifies the [`StandardCLILogging`][] settings such that log 

346 records at the respective level are emitted, based on the `param` 

347 and the `value`. 

348 

349 """ 

350 # Note: If multiple options use this callback, then we will be 

351 # called multiple times. Ensure the runs are idempotent. 

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

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

354 StandardCLILogging.cli_handler.setLevel(value) 1E

355 logging.getLogger(StandardCLILogging.package_name).setLevel(value) 1E

356 

357 

358# Option parsing and grouping 

359# =========================== 

360 

361 

362class OptionGroupOption(click.Option): 

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

364 

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

366 subclass contains its own group name and epilog. 

367 

368 Attributes: 

369 option_group_name: 

370 The name of the option group. Used as a heading on the help 

371 text for options in this section. 

372 epilog: 

373 An epilog to print after listing the options in this 

374 section. 

375 

376 """ 

377 

378 option_group_name: object = "" 

379 """""" 

380 epilog: object = "" 

381 """""" 

382 

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

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

385 raise NotImplementedError 

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

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

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

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

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

391 # unprocessed, after constructor finishes. 

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

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

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

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

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

397 

398 

399class StandardOption(OptionGroupOption): 

400 pass 

401 

402 

403# Portions of this class are based directly on code from click 8.1. 

404# (This does not in general include docstrings, unless otherwise noted.) 

405# They are subject to the 3-clause BSD license in the following 

406# paragraphs. Modifications to their code are marked with respective 

407# comments; they too are released under the same license below. The 

408# original code did not contain any "noqa" or "pragma" comments. 

409# 

410# Copyright 2024 Pallets 

411# 

412# Redistribution and use in source and binary forms, with or 

413# without modification, are permitted provided that the 

414# following conditions are met: 

415# 

416# 1. Redistributions of source code must retain the above 

417# copyright notice, this list of conditions and the 

418# following disclaimer. 

419# 

420# 2. Redistributions in binary form must reproduce the above 

421# copyright notice, this list of conditions and the 

422# following disclaimer in the documentation and/or other 

423# materials provided with the distribution. 

424# 

425# 3. Neither the name of the copyright holder nor the names 

426# of its contributors may be used to endorse or promote 

427# products derived from this software without specific 

428# prior written permission. 

429# 

430# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 

431# CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, 

432# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 

433# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 

434# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 

435# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 

436# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 

437# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 

438# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 

439# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 

440# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 

441# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 

442# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 

443class CommandWithHelpGroups(click.Command): 

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

445 

446 Supports help/option groups, group epilogs, and help text objects 

447 (objects that stringify to help texts). The latter is primarily 

448 used to implement translations. 

449 

450 Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE] for 

451 help/option group support, and further modified to include group 

452 epilogs and help text objects. 

453 

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

455 

456 """ 

457 

458 @staticmethod 

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

460 if isinstance(text, (list, tuple)): 1ac^d

461 return "\n\n".join(str(x) for x in text) 1ac^d

462 return str(text) 1acd

463 

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

465 # declaration for license details. 

466 def collect_usage_pieces(self, ctx: click.Context) -> list[str]: 

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

468 

469 Args: 

470 ctx: 

471 The click context. 

472 

473 """ 

474 rv = [str(self.options_metavar)] if self.options_metavar else [] 2a c w p d t u ! # v 4 s gb

475 for param in self.get_params(ctx): 2a c w p d t u ! # v 4 s gb

476 rv.extend(str(x) for x in param.get_usage_pieces(ctx)) 2a c w p d t u ! # v 4 s gb

477 return rv 2a c w p d t u ! # v 4 s gb

478 

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

480 # declaration for license details. 

481 def get_help_option( 

482 self, 

483 ctx: click.Context, 

484 ) -> click.Option | None: 

485 """Return a standard help option object. 

486 

487 Args: 

488 ctx: 

489 The click context. 

490 

491 """ 

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

493 

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

495 not help_options or not self.add_help_option 

496 ): # pragma: no cover [external-api] 

497 return None 

498 

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

500 ctx: click.Context, 

501 param: click.Parameter, # noqa: ARG001 

502 value: str, 

503 ) -> None: 

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

505 click.echo(ctx.get_help(), color=ctx.color) 1acd

506 ctx.exit() 1acd

507 

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

509 # object as the help string. 

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

511 help_options, 

512 is_flag=True, 

513 is_eager=True, 

514 expose_value=False, 

515 callback=show_help, 

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

517 ) 

518 

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

520 # declaration for license details. 

521 def get_short_help_str( 

522 self, 

523 limit: int = 45, 

524 ) -> str: 

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

526 

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

528 

529 Args: 

530 limit: 

531 The maximum width of the short help string. 

532 

533 """ 

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

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

536 # Used to implement translatable strings, as objects that 

537 # stringify to the translation. 

538 if self.short_help: # pragma: no cover [external-api] 1ac^

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

540 elif self.help: 1ac^

541 text = click.utils.make_default_short_help( 1ac^

542 self._text(self.help), limit 

543 ) 

544 else: # pragma: no cover [external-api] 

545 text = "" 

546 if self.deprecated: # pragma: no cover [external-api] 1ac^

547 # Modification against click 8.1: The translated string is 

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

549 # gettext default domain. 

550 text = str( 

551 _msg.TranslatedString(_msg.Label.DEPRECATED_COMMAND_LABEL) 

552 ).format(text=text) 

553 return text.strip() 1ac^

554 

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

556 # declaration for license details. 

557 def format_help_text( 

558 self, 

559 ctx: click.Context, 

560 formatter: click.HelpFormatter, 

561 ) -> None: 

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

563 

564 Args: 

565 ctx: 

566 The click context. 

567 formatter: 

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

569 

570 """ 

571 del ctx 1acd

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

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

574 # Used to implement translatable strings, as objects that 

575 # stringify to the translation. 

576 text = ( 1acd

577 inspect.cleandoc(self._text(self.help).partition("\f")[0]) 

578 if self.help is not None 

579 else "" 

580 ) 

581 if self.deprecated: # pragma: no cover [external-api] 1acd

582 # Modification against click 8.1: The translated string is 

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

584 # gettext default domain. 

585 text = str( 

586 _msg.TranslatedString(_msg.Label.DEPRECATED_COMMAND_LABEL) 

587 ).format(text=text) 

588 if text: # pragma: no branch 1acd

589 formatter.write_paragraph() 1acd

590 with formatter.indentation(): 1acd

591 formatter.write_text(text) 1acd

592 

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

594 # declaration for license details. Consider the whole section 

595 # marked as modified; the code modifications are too numerous to 

596 # mark individually. 

597 def format_options( 

598 self, 

599 ctx: click.Context, 

600 formatter: click.HelpFormatter, 

601 ) -> None: 

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

603 

604 This is a callback for [`click.Command.get_help`][] that 

605 implements the `--help` listing, by calling appropriate methods 

606 of the `formatter`. We list all options (like the base 

607 implementation), but grouped into sections according to the 

608 concrete [`click.Option`][] subclass being used. If the option 

609 is an instance of some subclass of [`OptionGroupOption`][], then 

610 the section heading and the epilog are taken from the 

611 [`option_group_name`] [OptionGroupOption.option_group_name] and 

612 [`epilog`] [OptionGroupOption.epilog] attributes; otherwise, the 

613 section heading is "Options" (or "Other options" if there are 

614 other option groups) and the epilog is empty. 

615 

616 We unconditionally call [`format_commands`][], and rely on it to 

617 act as a no-op if we aren't actually a [`click.MultiCommand`][]. 

618 

619 Args: 

620 ctx: 

621 The click context. 

622 formatter: 

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

624 

625 """ 

626 default_group_name = "" 1acd

627 help_records: dict[str, list[tuple[str, str]]] = {} 1acd

628 epilogs: dict[str, str] = {} 1acd

629 params = self.params[:] 1acd

630 if ( # pragma: no branch 1acd

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

632 and help_opt not in params 

633 ): 

634 params.append(help_opt) 1acd

635 for param in params: 1acd

636 rec = param.get_help_record(ctx) 1acd

637 if rec is not None: 1acd

638 rec = (rec[0], self._text(rec[1])) 1acd

639 if isinstance(param, OptionGroupOption): 1acd

640 group_name = self._text(param.option_group_name) 1acd

641 epilogs.setdefault(group_name, self._text(param.epilog)) 1acd

642 else: # pragma: no cover [external-api] 

643 group_name = default_group_name 

644 help_records.setdefault(group_name, []).append(rec) 1acd

645 if default_group_name in help_records: # pragma: no branch 1acd

646 default_group = help_records.pop(default_group_name) 1acd

647 default_group_label = ( 1acd

648 _msg.Label.OTHER_OPTIONS_LABEL 

649 if len(default_group) > 1 

650 else _msg.Label.OPTIONS_LABEL 

651 ) 

652 default_group_name = self._text( 1acd

653 _msg.TranslatedString(default_group_label) 

654 ) 

655 help_records[default_group_name] = default_group 1acd

656 for group_name, records in help_records.items(): 1acd

657 with formatter.section(group_name): 1acd

658 formatter.write_dl(records) 1acd

659 epilog = inspect.cleandoc(epilogs.get(group_name, "")) 1acd

660 if epilog: 1acd

661 formatter.write_paragraph() 1acd

662 with formatter.indentation(): 1acd

663 formatter.write_text(epilog) 1acd

664 self.format_commands(ctx, formatter) 1acd

665 

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

667 # declaration for license details. Consider the whole section 

668 # marked as modified; the code modifications are too numerous to 

669 # mark individually. 

670 def format_commands( 

671 self, 

672 ctx: click.Context, 

673 formatter: click.HelpFormatter, 

674 ) -> None: 

675 """Format the subcommands, if any. 

676 

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

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

679 

680 Args: 

681 ctx: 

682 The click context. 

683 formatter: 

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

685 

686 """ 

687 if not isinstance(self, click.Group): 1acd

688 return 1acd

689 commands: list[tuple[str, click.Command]] = [] 1ac

690 for subcommand in self.list_commands(ctx): 1ac

691 cmd = self.get_command(ctx, subcommand) 1ac

692 if cmd is None or cmd.hidden: # pragma: no cover [external-api] 1ac

693 continue 

694 commands.append((subcommand, cmd)) 1ac

695 if commands: # pragma: no branch 1ac

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

697 limit = formatter.width - 6 - len(longest_command) 1ac

698 rows: list[tuple[str, str]] = [] 1ac

699 for subcommand, cmd in commands: 1ac

700 help_str = self._text(cmd.get_short_help_str(limit) or "") 1ac

701 rows.append((subcommand, help_str)) 1ac

702 if rows: # pragma: no branch 1ac

703 commands_label = self._text( 1ac

704 _msg.TranslatedString(_msg.Label.COMMANDS_LABEL) 

705 ) 

706 with formatter.section(commands_label): 1ac

707 formatter.write_dl(rows) 1ac

708 

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

710 # declaration for license details. 

711 def format_epilog( 

712 self, 

713 ctx: click.Context, 

714 formatter: click.HelpFormatter, 

715 ) -> None: 

716 """Format the epilog, if any. 

717 

718 Args: 

719 ctx: 

720 The click context. 

721 formatter: 

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

723 

724 """ 

725 del ctx 1acd

726 if self.epilog: # pragma: no branch 1acd

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

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

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

730 # as objects that stringify to the translation. 

731 epilog = inspect.cleandoc(self._text(self.epilog)) 1acd

732 formatter.write_paragraph() 1acd

733 with formatter.indentation(): 1acd

734 formatter.write_text(epilog) 1acd

735 

736 

737# Portions of this class are based directly on code from click 8.1. 

738# (This does not in general include docstrings, unless otherwise noted.) 

739# They are subject to the 3-clause BSD license in the following 

740# paragraphs. Modifications to their code are marked with respective 

741# comments; they too are released under the same license below. The 

742# original code did not contain any "noqa" or "pragma" comments. 

743# 

744# Copyright 2024 Pallets 

745# 

746# Redistribution and use in source and binary forms, with or 

747# without modification, are permitted provided that the 

748# following conditions are met: 

749# 

750# 1. Redistributions of source code must retain the above 

751# copyright notice, this list of conditions and the 

752# following disclaimer. 

753# 

754# 2. Redistributions in binary form must reproduce the above 

755# copyright notice, this list of conditions and the 

756# following disclaimer in the documentation and/or other 

757# materials provided with the distribution. 

758# 

759# 3. Neither the name of the copyright holder nor the names 

760# of its contributors may be used to endorse or promote 

761# products derived from this software without specific 

762# prior written permission. 

763# 

764# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 

765# CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, 

766# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 

767# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 

768# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 

769# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 

770# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 

771# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 

772# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 

773# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 

774# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 

775# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 

776# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 

777# 

778# TODO(the-13th-letter): Remove this class and license block in v1.0. 

779# https://the13thletter.info/derivepassphrase/latest/upgrade-notes/#v1.0-implied-subcommands 

780class DefaultToVaultGroup(CommandWithHelpGroups, click.Group): 

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

782 

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

784 an implementation detail and a kludge. 

785 

786 """ 

787 

788 def resolve_command( 

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

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

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

792 cmd_name = click.utils.make_str(args[0]) 1nacfge?@5j3^w]h6

793 

794 # Get the command 

795 cmd = self.get_command(ctx, cmd_name) 1nacfge?@5j3^w]h6

796 

797 # If we can't find the command but there is a normalization 

798 # function available, we try with that one. 

799 if ( # pragma: no cover [external-api] 1nacfge?@5j3^w]h6

800 cmd is None and ctx.token_normalize_func is not None 

801 ): 

802 cmd_name = ctx.token_normalize_func(cmd_name) 

803 cmd = self.get_command(ctx, cmd_name) 

804 

805 # If we don't find the command we want to show an error message 

806 # to the user that it was not provided. However, there is 

807 # something else we should do: if the first argument looks like 

808 # an option we want to kick off parsing again for arguments to 

809 # resolve things like --help which now should go to the main 

810 # place. 

811 if cmd is None and not ctx.resilient_parsing: 1nacfge?@5j3^w]h6

812 #### 

813 # BEGIN modifications for derivepassphrase 

814 # 

815 # Instead of using 

816 # 

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

818 # 

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

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

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

822 # 

823 # END modifications for derivepassphrase 

824 #### 

825 if cmd_name.startswith("-"): 1]h

826 self.parse_args(ctx, ctx.args) 1h

827 #### 

828 # BEGIN modifications for derivepassphrase 

829 # 

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

831 # issue a deprecation warning. 

832 deprecation = logging.getLogger(f"{PROG_NAME}.deprecation") 1]h

833 deprecation.warning( 1]h

834 _msg.TranslatedString( 

835 _msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED 

836 ) 

837 ) 

838 cmd_name = "vault" 1]h

839 cmd = self.get_command(ctx, cmd_name) 1]h

840 assert cmd is not None, 'Mandatory subcommand "vault" missing!' 1]h

841 args = [cmd_name, *args] 1]h

842 # 

843 # END modifications for derivepassphrase 

844 #### 

845 return cmd_name if cmd else None, cmd, args[1:] 1nacfge?@5j3^w]h6

846 

847 

848# TODO(the-13th-letter): Base this class on CommandWithHelpGroups and 

849# click.Group in v1.0. 

850# https://the13thletter.info/derivepassphrase/latest/upgrade-notes/#v1.0-implied-subcommands 

851class TopLevelCLIEntryPoint(DefaultToVaultGroup): 

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

853 

854 When called as a function, this sets up the environment properly 

855 before invoking the actual callbacks. Currently, this means setting 

856 up the logging subsystem and the delegation of Python warnings to 

857 the logging subsystem. 

858 

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

860 directly. 

861 

862 """ 

863 

864 def __call__( # pragma: no cover [external-api] 

865 self, 

866 *args: Any, # noqa: ANN401 

867 **kwargs: Any, # noqa: ANN401 

868 ) -> Any: # noqa: ANN401 

869 """""" # noqa: D419 

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

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

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

873 # excluded from coverage. 

874 with ( 

875 StandardCLILogging.ensure_standard_logging(), 

876 StandardCLILogging.ensure_standard_warnings_logging(), 

877 ): 

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

879 

880 

881# Actual option groups and callbacks used by derivepassphrase 

882# =========================================================== 

883 

884 

885def color_forcing_callback( 

886 ctx: click.Context, 

887 param: click.Parameter, 

888 value: Any, # noqa: ANN401 

889) -> None: 

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

891 

892 Ideally, we would default to color and text styling if outputting to 

893 a TTY, or monochrome/unstyled otherwise. We would also support the 

894 `NO_COLOR` and `FORCE_COLOR` environment variables to override this 

895 auto-detection, and perhaps the `TTY_COMPATIBLE` variable too. 

896 

897 Alas, this is not sensible to support at the moment, because the 

898 conventions are still in flux. And settling on a specific 

899 interpretation of the conventions would likely prove very difficult 

900 to change later on in a backward-compatible way. We thus opt for 

901 a conservative approach and use device-indepedendent text output 

902 without any color or text styling whatsoever. 

903 

904 """ 

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

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

907 

908 

909def validate_occurrence_constraint( 

910 ctx: click.Context, 

911 param: click.Parameter, 

912 value: Any, # noqa: ANN401 

913) -> int | None: 

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

915 

916 Args: 

917 ctx: The `click` context. 

918 param: The current command-line parameter. 

919 value: The parameter value to be checked. 

920 

921 Returns: 

922 The parsed parameter value. 

923 

924 Raises: 

925 click.BadParameter: The parameter value is invalid. 

926 

927 """ 

928 del ctx # Unused. 1biDExyFn?@5j3hpGU$[kVlW78qtu!#v%'(H)*X+,zIJK6-LMNYOPQRorS4sAB.Z09T1C/:;2

929 del param # Unused. 1biDExyFn?@5j3hpGU$[kVlW78qtu!#v%'(H)*X+,zIJK6-LMNYOPQRorS4sAB.Z09T1C/:;2

930 if value is None: 1biDExyFn?@5j3hpGU$[kVlW78qtu!#v%'(H)*X+,zIJK6-LMNYOPQRorS4sAB.Z09T1C/:;2

931 return value 1biDExyFn?@5j3hpGU$[kVlW78qt!#v%'(H)*X+,zIJK6-LMNYOPQRorS4sAB.Z09T1C/:;2

932 if isinstance(value, int): 1bih[kl78tuv

933 int_value = value 1[

934 else: 

935 try: 1bihkl78tuv

936 int_value = int(value, 10) 1bihkl78tuv

937 except ValueError as exc: 1u

938 raise click.BadParameter(NOT_AN_INTEGER) from exc 1u

939 if int_value < 0: 1bih[kl78tuv

940 raise click.BadParameter(NOT_A_NONNEGATIVE_INTEGER) 1u

941 return int_value 1bih[kl78tv

942 

943 

944def validate_length( 

945 ctx: click.Context, 

946 param: click.Parameter, 

947 value: Any, # noqa: ANN401 

948) -> int | None: 

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

950 

951 Args: 

952 ctx: The `click` context. 

953 param: The current command-line parameter. 

954 value: The parameter value to be checked. 

955 

956 Returns: 

957 The parsed parameter value. 

958 

959 Raises: 

960 click.BadParameter: The parameter value is invalid. 

961 

962 """ 

963 del ctx # Unused. 1biDExyFn?@5j3hpGU$[kVlW78qtu!#v%'(H)*X+,zIJK6-LMNYOPQRorS4sAB.Z09T1C/:;2

964 del param # Unused. 1biDExyFn?@5j3hpGU$[kVlW78qtu!#v%'(H)*X+,zIJK6-LMNYOPQRorS4sAB.Z09T1C/:;2

965 if value is None: 1biDExyFn?@5j3hpGU$[kVlW78qtu!#v%'(H)*X+,zIJK6-LMNYOPQRorS4sAB.Z09T1C/:;2

966 return value 1biDExyFn?@5j3hpGU$[kVlW78qt!#v%'(H)*X+,zIJK6-LMNYOPQRSsAB.Z09T1C/:;2

967 if isinstance(value, int): 1bij[klqtuvor49

968 int_value = value 1[

969 else: 

970 try: 1bijklqtuvor49

971 int_value = int(value, 10) 1bijklqtuvor49

972 except ValueError as exc: 1u

973 raise click.BadParameter(NOT_AN_INTEGER) from exc 1u

974 if int_value < 1: 1bij[klqtuvor49

975 raise click.BadParameter(NOT_A_POSITIVE_INTEGER) 1u

976 return int_value 1bij[klqtvor49

977 

978 

979def common_version_output( 

980 ctx: click.Context, 

981 param: click.Parameter, 

982 value: bool, # noqa: FBT001 

983) -> None: 

984 del param, value 1amfge

985 major_dependencies: list[str] = [] 1amfge

986 try: 1amfge

987 cryptography_version = importlib.metadata.version("cryptography") 1amfge

988 except ModuleNotFoundError: 1amfge

989 pass 1amfge

990 else: 

991 major_dependencies.append(f"cryptography {cryptography_version}") 1amfge

992 major_dependencies.append(f"click {importlib.metadata.version('click')}") 1amfge

993 

994 click.echo( 1amfge

995 " ".join([ 

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

997 VERSION, 

998 ]), 

999 color=ctx.color, 

1000 ) 

1001 for dependency in major_dependencies: 1amfge

1002 click.echo( 1amfge

1003 str( 

1004 _msg.TranslatedString( 

1005 _msg.Label.VERSION_INFO_MAJOR_LIBRARY_TEXT, 

1006 dependency_name_and_version=dependency, 

1007 ) 

1008 ), 

1009 color=ctx.color, 

1010 ) 

1011 

1012 

1013def print_version_info_types( 

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

1015 /, 

1016 *, 

1017 ctx: click.Context, 

1018) -> None: 

1019 for message_label, item_list in version_info_types.items(): 1amfge

1020 if item_list: 1amfge

1021 current_length = len(str(_msg.TranslatedString(message_label))) 1amfge

1022 formatted_item_list_pieces: list[str] = [] 1amfge

1023 n = len(item_list) 1amfge

1024 for i, item in enumerate(item_list, start=1): 1amfge

1025 space = " " 1amfge

1026 punctuation = "." if i == n else "," 1amfge

1027 if ( 1amfge

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

1029 <= VERSION_OUTPUT_WRAPPING_WIDTH 

1030 ): 

1031 current_length += len(space) + len(item) + len(punctuation) 1amfge

1032 piece = f"{space}{item}{punctuation}" 1amfge

1033 else: 

1034 space = " " 1afge

1035 current_length = len(space) + len(item) + len(punctuation) 1afge

1036 piece = f"\n{space}{item}{punctuation}" 1afge

1037 formatted_item_list_pieces.append(piece) 1amfge

1038 click.echo( 1amfge

1039 "".join([ 

1040 click.style( 

1041 str(_msg.TranslatedString(message_label)), 

1042 bold=True, 

1043 ), 

1044 "".join(formatted_item_list_pieces), 

1045 ]), 

1046 color=ctx.color, 

1047 ) 

1048 

1049 

1050def derivepassphrase_version_option_callback( 

1051 ctx: click.Context, 

1052 param: click.Parameter, 

1053 value: bool, # noqa: FBT001 

1054) -> None: 

1055 if value and not ctx.resilient_parsing: 1nacmfge?@5j3^w]hp6

1056 common_version_output(ctx, param, value) 1am

1057 derivation_schemes = set(_types.DerivationScheme) 1am

1058 supported_subcommands = set(_types.Subcommand) 1am

1059 click.echo() 1am

1060 version_info_types: dict[_msg.Label, list[str]] = { 1am

1061 _msg.Label.SUPPORTED_DERIVATION_SCHEMES: [ 

1062 k for k in derivation_schemes if k.test() 

1063 ], 

1064 _msg.Label.UNAVAILABLE_DERIVATION_SCHEMES: [ 

1065 k for k in derivation_schemes if not k.test() 

1066 ], 

1067 _msg.Label.SUPPORTED_SUBCOMMANDS: [ 

1068 k for k in supported_subcommands if k.test() 

1069 ], 

1070 } 

1071 print_version_info_types(version_info_types, ctx=ctx) 1am

1072 ctx.exit() 1am

1073 

1074 

1075def export_version_option_callback( 

1076 ctx: click.Context, 

1077 param: click.Parameter, 

1078 value: bool, # noqa: FBT001 

1079) -> None: 

1080 if value and not ctx.resilient_parsing: 1acfg?@^w]

1081 common_version_output(ctx, param, value) 1ag

1082 supported_subcommands = set(_types.ExportSubcommand) 1ag

1083 foreign_configuration_formats = [ 1ag

1084 _types.ForeignConfigurationFormat.VAULT_STOREROOM, 

1085 _types.ForeignConfigurationFormat.VAULT_V02, 

1086 _types.ForeignConfigurationFormat.VAULT_V03, 

1087 ] 

1088 click.echo() 1ag

1089 version_info_types: dict[_msg.Label, list[str]] = { 1ag

1090 # Marked as known-but-unavailable, because they are used by 

1091 # subcommands of `derivepassphrase export`, not by the 

1092 # command itself. 

1093 _msg.Label.UNAVAILABLE_FOREIGN_CONFIGURATION_FORMATS: list( 

1094 foreign_configuration_formats 

1095 ), 

1096 _msg.Label.SUPPORTED_SUBCOMMANDS: [ 

1097 k for k in supported_subcommands if k.test() 

1098 ], 

1099 } 

1100 print_version_info_types(version_info_types, ctx=ctx) 1ag

1101 ctx.exit() 1ag

1102 

1103 

1104def export_vault_version_option_callback( 

1105 ctx: click.Context, 

1106 param: click.Parameter, 

1107 value: bool, # noqa: FBT001 

1108) -> None: 

1109 if value and not ctx.resilient_parsing: 2a c f ? @ w ] hbibjbkblbmbnbobgbpb

1110 common_version_output(ctx, param, value) 1af

1111 foreign_configuration_formats = [ 1af

1112 _types.ForeignConfigurationFormat.VAULT_STOREROOM, 

1113 _types.ForeignConfigurationFormat.VAULT_V02, 

1114 _types.ForeignConfigurationFormat.VAULT_V03, 

1115 ] 

1116 known_extras = [ 1af

1117 _types.PEP508Extra.EXPORT, 

1118 ] 

1119 click.echo() 1af

1120 version_info_types: dict[_msg.Label, list[str]] = { 1af

1121 _msg.Label.SUPPORTED_FOREIGN_CONFIGURATION_FORMATS: [ 

1122 k for k in foreign_configuration_formats if k.test() 

1123 ], 

1124 _msg.Label.UNAVAILABLE_FOREIGN_CONFIGURATION_FORMATS: [ 

1125 k for k in foreign_configuration_formats if not k.test() 

1126 ], 

1127 _msg.Label.ENABLED_PEP508_EXTRAS: [ 

1128 k for k in known_extras if k.test() 

1129 ], 

1130 } 

1131 print_version_info_types(version_info_types, ctx=ctx) 1af

1132 ctx.exit() 1af

1133 

1134 

1135def vault_version_option_callback( 

1136 ctx: click.Context, 

1137 param: click.Parameter, 

1138 value: bool, # noqa: FBT001 

1139) -> None: 

1140 if value and not ctx.resilient_parsing: 1biDExyFnace?@5j3hpGU$[kVlW78dqtu!#v%'(H)*X+,zIJK6-LMNYOPQRorS4sAB.Z09T1C/:;2

1141 common_version_output(ctx, param, value) 1ae

1142 features = [ 1ae

1143 _types.Feature.SSH_KEY, 

1144 ] 

1145 click.echo() 1ae

1146 

1147 from derivepassphrase.ssh_agent import socketprovider # noqa: PLC0415 1ae

1148 

1149 socket_providers: dict[str, bool] = {} 1ae

1150 for key, names in socketprovider.SocketProvider.grouped().items(): 1ae

1151 if isinstance(key, _types.BuiltinSSHAgentSocketProvider): 1ae

1152 if not key.is_known_fake_agent(): 1ae

1153 other_names = set(names) - {key} 1ae

1154 formatted_key = ( 1ae

1155 "{key!s} ({aliases_key!s} {aliases})".format( 

1156 key=key, 

1157 aliases_key=_msg.TranslatedString( 

1158 _msg.Label.FEATURE_ITEM_ALIASES 

1159 ), 

1160 aliases=", ".join(sorted(other_names, key=str)), 

1161 ) 

1162 if other_names 

1163 else str(key) 

1164 ) 

1165 socket_providers[formatted_key] = key.test() 1ae

1166 elif key: # pragma: no cover [external] 

1167 socket_providers[key] = True 

1168 version_info_types: dict[_msg.Label, list[str]] = { 1ae

1169 _msg.Label.SUPPORTED_FEATURES: [k for k in features if k.test()], 

1170 _msg.Label.UNAVAILABLE_FEATURES: [ 

1171 k for k in features if not k.test() 

1172 ], 

1173 _msg.Label.SUPPORTED_SSH_AGENT_SOCKET_PROVIDERS: sorted( 

1174 k for k, v in socket_providers.items() if v 

1175 ), 

1176 _msg.Label.UNAVAILABLE_SSH_AGENT_SOCKET_PROVIDERS: sorted( 

1177 k for k, v in socket_providers.items() if not v 

1178 ), 

1179 } 

1180 print_version_info_types(version_info_types, ctx=ctx) 1ae

1181 ctx.exit() 1ae

1182 

1183 

1184def version_option( 

1185 version_option_callback: Callable[ 

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

1187 ], 

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

1189 return click.option( 

1190 "--version", 

1191 is_flag=True, 

1192 is_eager=True, 

1193 expose_value=False, 

1194 callback=version_option_callback, 

1195 cls=StandardOption, 

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

1197 ) 

1198 

1199 

1200color_forcing_pseudo_option = click.option( 

1201 "--_pseudo-option-color-forcing", 

1202 "_color_forcing", 

1203 is_flag=True, 

1204 is_eager=True, 

1205 expose_value=False, 

1206 hidden=True, 

1207 callback=color_forcing_callback, 

1208 help="(pseudo-option)", 

1209) 

1210 

1211 

1212class PassphraseGenerationOption(OptionGroupOption): 

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

1214 

1215 option_group_name = _msg.TranslatedString( 

1216 _msg.Label.PASSPHRASE_GENERATION_LABEL 

1217 ) 

1218 epilog = _msg.TranslatedString( 

1219 _msg.Label.PASSPHRASE_GENERATION_EPILOG, 

1220 metavar=_msg.TranslatedString( 

1221 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1222 ), 

1223 ) 

1224 

1225 

1226class ConfigurationOption(OptionGroupOption): 

1227 """Configuration options for the CLI.""" 

1228 

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

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

1231 

1232 

1233class StorageManagementOption(OptionGroupOption): 

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

1235 

1236 option_group_name = _msg.TranslatedString( 

1237 _msg.Label.STORAGE_MANAGEMENT_LABEL 

1238 ) 

1239 epilog = _msg.TranslatedString( 

1240 _msg.Label.STORAGE_MANAGEMENT_EPILOG, 

1241 metavar=_msg.TranslatedString( 

1242 _msg.Label.STORAGE_MANAGEMENT_METAVAR_PATH 

1243 ), 

1244 ) 

1245 

1246 

1247class CompatibilityOption(OptionGroupOption): 

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

1249 

1250 option_group_name = _msg.TranslatedString( 

1251 _msg.Label.COMPATIBILITY_OPTION_LABEL 

1252 ) 

1253 

1254 

1255class LoggingOption(OptionGroupOption): 

1256 """Logging options for the CLI.""" 

1257 

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

1259 epilog = "" 

1260 

1261 

1262debug_option = click.option( 

1263 "--debug", 

1264 "logging_level", 

1265 is_flag=True, 

1266 flag_value=logging.DEBUG, 

1267 expose_value=False, 

1268 callback=adjust_logging_level, 

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

1270 cls=LoggingOption, 

1271) 

1272verbose_option = click.option( 

1273 "-v", 

1274 "--verbose", 

1275 "logging_level", 

1276 is_flag=True, 

1277 flag_value=logging.INFO, 

1278 expose_value=False, 

1279 callback=adjust_logging_level, 

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

1281 cls=LoggingOption, 

1282) 

1283quiet_option = click.option( 

1284 "-q", 

1285 "--quiet", 

1286 "logging_level", 

1287 is_flag=True, 

1288 flag_value=logging.ERROR, 

1289 expose_value=False, 

1290 callback=adjust_logging_level, 

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

1292 cls=LoggingOption, 

1293) 

1294 

1295 

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

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

1298 

1299 Adds the three click options `-v`/`--verbose`, `-q`/`--quiet` and 

1300 `--debug`, which calls back into the [`adjust_logging_level`][] 

1301 function (with different argument values). 

1302 

1303 Args: 

1304 f: A callable to decorate. 

1305 

1306 Returns: 

1307 The decorated callable. 

1308 

1309 """ 

1310 return debug_option(verbose_option(quiet_option(f))) 

1311 

1312 

1313# Shell completion 

1314# ================ 

1315 

1316 

1317# TODO(the-13th-letter): Remove this once upstream click's Zsh completion 

1318# script properly supports colons. 

1319# 

1320# https://github.com/pallets/click/pull/2846 

1321class ZshComplete(click.shell_completion.ZshComplete): 

1322 """Zsh completion class that supports colons. 

1323 

1324 `click`'s Zsh completion class (at least v8.1.7 and v8.1.8) uses 

1325 some completion helper functions (provided by Zsh) that parse each 

1326 completion item into value-description pairs, separated by a colon. 

1327 Other completion helper functions don't. Correspondingly, any 

1328 internal colons in the completion item's value sometimes need to be 

1329 escaped, and sometimes don't. 

1330 

1331 The "right" way to fix this is to modify the Zsh completion script 

1332 to only use one type of serialization: either escaped, or unescaped. 

1333 However, the Zsh completion script itself may already be installed 

1334 in the user's Zsh settings, and we have no way of knowing that. 

1335 Therefore, it is better to change the `format_completion` method to 

1336 adaptively and "smartly" emit colon-escaped output or not, based on 

1337 whether the completion script will be using it. 

1338 

1339 As of `click` v8.2.2, the Zsh completion class also adaptively emits 

1340 colon-escaped output or not, based on the very same criterion. 

1341 

1342 """ 

1343 

1344 @override 

1345 def format_completion( 

1346 self, 

1347 item: click.shell_completion.CompletionItem, 

1348 ) -> str: 

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

1350 

1351 This serialization ensures colons in the item value are properly 

1352 escaped if and only if the completion script will attempt to 

1353 pass a colon-separated key/description pair to the underlying 

1354 Zsh machinery. This is the case if and only if the help text is 

1355 non-degenerate. 

1356 

1357 """ 

1358 help_ = item.help or "_" 13

1359 value = item.value.replace(":", r"\:" if help_ != "_" else ":") 13

1360 return f"{item.type}\n{value}\n{help_}" 13

1361 

1362 

1363# Our ZshComplete class depends crucially on the exact shape of the Zsh 

1364# completion script. So only fix the completion formatter if the 

1365# completion script is still the same. 

1366# 

1367# (This Zsh script is part of click, and available under the 

1368# 3-clause-BSD license.) 

1369_ORIG_SOURCE_TEMPLATE = """\ 

1370#compdef %(prog_name)s 

1371 

1372%(complete_func)s() { 

1373 local -a completions 

1374 local -a completions_with_descriptions 

1375 local -a response 

1376 (( ! $+commands[%(prog_name)s] )) && return 1 

1377 

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

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

1380 

1381 for type key descr in ${response}; do 

1382 if [[ "$type" == "plain" ]]; then 

1383 if [[ "$descr" == "_" ]]; then 

1384 completions+=("$key") 

1385 else 

1386 completions_with_descriptions+=("$key":"$descr") 

1387 fi 

1388 elif [[ "$type" == "dir" ]]; then 

1389 _path_files -/ 

1390 elif [[ "$type" == "file" ]]; then 

1391 _path_files -f 

1392 fi 

1393 done 

1394 

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

1396 _describe -V unsorted completions_with_descriptions -U 

1397 fi 

1398 

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

1400 compadd -U -V unsorted -a completions 

1401 fi 

1402} 

1403 

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

1405 # autoload from fpath, call function directly 

1406 %(complete_func)s "$@" 

1407else 

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

1409 compdef %(complete_func)s %(prog_name)s 

1410fi 

1411""" 

1412if ( 

1413 click.shell_completion.ZshComplete.source_template == _ORIG_SOURCE_TEMPLATE 

1414): # pragma: no cover [external] 

1415 click.shell_completion.add_completion_class(ZshComplete)