Marco Ricci commited on 2024-06-22 21:19:30
Zeige 1 geänderte Dateien mit 286 Einfügungen und 92 Löschungen.
Currently, we use a one-to-one correspondence between command-line argument name and function argument, via catch-all keyword arguments. This is very unwieldy to use with type checking, it exposes us to name clashes with Python keywords, and it doesn't even save that much typing effort because the arguments still ought to be documented in the documentation anyway. Therefore, introduce an explicit target parameter name for the `click` interface and corresponding explicit function arguments. (The implicit list of (function) arguments that `click` passes to the function is already readily available in the context object.) As a related problem, the code for checking whether incompatible options were specified also relies (excessively cleverly) on command-line argument names and function argument names being equal, and sometimes conflates short and long option names, leading to it missing some examples of incompatible options. Therefore, instead, explicitly build a map of (short and long) option names to option objects and test whether incompatible option objects were used.
... | ... |
@@ -25,21 +25,136 @@ __all__ = ('derivepassphrase',) |
25 | 25 |
prog_name = 'derivepassphrase' |
26 | 26 |
|
27 | 27 |
|
28 |
-# Implement help text groups. Inspired by |
|
29 |
-# https://github.com/pallets/click/issues/373#issuecomment-515293746 and |
|
30 |
-# modified to support group epilogs as well. |
|
28 |
+def _config_filename() -> str | bytes | pathlib.Path: |
|
29 |
+ """Return the filename of the configuration file. |
|
30 |
+ |
|
31 |
+ The file is currently named `settings.json`, located within the |
|
32 |
+ configuration directory as determined by the `DERIVEPASSPHRASE_PATH` |
|
33 |
+ environment variable, or by [`click.get_app_dir`][] in POSIX |
|
34 |
+ mode. |
|
35 |
+ |
|
36 |
+ """ |
|
37 |
+ path: str | bytes | pathlib.Path |
|
38 |
+ path = (os.getenv(prog_name.upper() + '_PATH') |
|
39 |
+ or click.get_app_dir(prog_name, force_posix=True)) |
|
40 |
+ return os.path.join(path, 'settings.json') |
|
41 |
+ |
|
42 |
+ |
|
43 |
+def _load_config() -> dpp_types.VaultConfig: |
|
44 |
+ """Load a vault(1)-compatible config from the application directory. |
|
45 |
+ |
|
46 |
+ The filename is obtained via |
|
47 |
+ [`derivepassphrase.cli._config_filename`][]. This must be an |
|
48 |
+ unencrypted JSON file. |
|
49 |
+ |
|
50 |
+ Returns: |
|
51 |
+ The vault settings. See |
|
52 |
+ [`derivepassphrase.types.VaultConfig`][] for details. |
|
53 |
+ |
|
54 |
+ Raises: |
|
55 |
+ OSError: |
|
56 |
+ There was an OS error accessing the file. |
|
57 |
+ ValueError: |
|
58 |
+ The data loaded from the file is not a vault(1)-compatible |
|
59 |
+ config. |
|
60 |
+ |
|
61 |
+ """ |
|
62 |
+ filename = _config_filename() |
|
63 |
+ with open(filename, 'rb') as fileobj: |
|
64 |
+ data = json.load(fileobj) |
|
65 |
+ if not dpp_types.is_vault_config(data): |
|
66 |
+ raise ValueError('Invalid vault config') |
|
67 |
+ return data |
|
68 |
+ |
|
69 |
+ |
|
70 |
+def _save_config(config: dpp_types.VaultConfig, /) -> None: |
|
71 |
+ """Save a vault(1)-compatbile config to the application directory. |
|
72 |
+ |
|
73 |
+ The filename is obtained via |
|
74 |
+ [`derivepassphrase.cli._config_filename`][]. The config will be |
|
75 |
+ stored as an unencrypted JSON file. |
|
76 |
+ |
|
77 |
+ Args: |
|
78 |
+ config: |
|
79 |
+ vault configuration to save. |
|
80 |
+ |
|
81 |
+ Raises: |
|
82 |
+ OSError: |
|
83 |
+ There was an OS error accessing or writing the file. |
|
84 |
+ ValueError: |
|
85 |
+ The data cannot be stored as a vault(1)-compatible config. |
|
86 |
+ |
|
87 |
+ """ |
|
88 |
+ if not dpp_types.is_vault_config(config): |
|
89 |
+ raise ValueError('Invalid vault config') |
|
90 |
+ filename = _config_filename() |
|
91 |
+ with open(filename, 'wt', encoding='UTF-8') as fileobj: |
|
92 |
+ json.dump(config, fileobj) |
|
93 |
+ |
|
94 |
+ |
|
31 | 95 |
class OptionGroupOption(click.Option): |
32 |
- option_group_name = '' |
|
33 |
- epilog = '' |
|
96 |
+ """A [`click.Option`][] with an associated group name and group epilog. |
|
97 |
+ |
|
98 |
+ Used by [`derivepassphrase.cli.CommandWithHelpGroups`][] to print |
|
99 |
+ help sections. Each subclass contains its own group name and |
|
100 |
+ epilog. |
|
101 |
+ |
|
102 |
+ Attributes: |
|
103 |
+ option_group_name: |
|
104 |
+ The name of the option group. Used as a heading on the help |
|
105 |
+ text for options in this section. |
|
106 |
+ epilog: |
|
107 |
+ An epilog to print after listing the options in this |
|
108 |
+ section. |
|
109 |
+ |
|
110 |
+ """ |
|
111 |
+ option_group_name: str = '' |
|
112 |
+ epilog: str = '' |
|
113 |
+ |
|
114 |
+ def __init__(self, *args, **kwargs): # type: ignore |
|
115 |
+ if self.__class__ == __class__: |
|
116 |
+ raise NotImplementedError() |
|
117 |
+ return super().__init__(*args, **kwargs) |
|
118 |
+ |
|
34 | 119 |
|
35 | 120 |
class CommandWithHelpGroups(click.Command): |
121 |
+ """A [`click.Command`][] with support for help/option groups. |
|
122 |
+ |
|
123 |
+ Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and |
|
124 |
+ further modified to support group epilogs. |
|
125 |
+ |
|
126 |
+ [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746 |
|
127 |
+ |
|
128 |
+ """ |
|
129 |
+ |
|
36 | 130 |
def format_options( |
37 | 131 |
self, ctx: click.Context, formatter: click.HelpFormatter, |
38 | 132 |
) -> None: |
133 |
+ r"""Format options on the help listing, grouped into sections. |
|
134 |
+ |
|
135 |
+ As part of the `--help` listing, list all options, but grouped |
|
136 |
+ into sections according to the concrete [`click.Option`][] |
|
137 |
+ subclass being used. If the option is an instance of some |
|
138 |
+ subclass `X` of [`derivepassphrase.cli.OptionGroupOption`][], |
|
139 |
+ then the section heading and the epilog is taken from |
|
140 |
+ `X.option_group_name` and `X.epilog`; otherwise, the section |
|
141 |
+ heading is "Options" (or "Other options" if there are other |
|
142 |
+ option groups) and the epilog is empty. |
|
143 |
+ |
|
144 |
+ Args: |
|
145 |
+ ctx: |
|
146 |
+ The click context. |
|
147 |
+ formatter: |
|
148 |
+ The formatter for the `--help` listing. |
|
149 |
+ |
|
150 |
+ """ |
|
39 | 151 |
help_records: dict[str, list[tuple[str, str]]] = {} |
40 | 152 |
epilogs: dict[str, str] = {} |
41 | 153 |
params = self.params[:] |
42 |
- if (help_opt := self.get_help_option(ctx)) and help_opt not in params: |
|
154 |
+ if ( # pragma: no branch |
|
155 |
+ (help_opt := self.get_help_option(ctx)) is not None |
|
156 |
+ and help_opt not in params |
|
157 |
+ ): |
|
43 | 158 |
params.append(help_opt) |
44 | 159 |
for param in params: |
45 | 160 |
rec = param.get_help_record(ctx) |
... | ... |
@@ -63,22 +178,37 @@ class CommandWithHelpGroups(click.Command): |
63 | 178 |
with formatter.indentation(): |
64 | 179 |
formatter.write_text(epilog) |
65 | 180 |
|
181 |
+ |
|
66 | 182 |
# Concrete option groups used by this command-line interface. |
67 | 183 |
class PasswordGenerationOption(OptionGroupOption): |
184 |
+ """Password generation options for the CLI.""" |
|
68 | 185 |
option_group_name = 'Password generation' |
69 |
- epilog = 'Use NUMBER=0, e.g. "--symbol 0", to exclude a character type from the output' |
|
186 |
+ epilog = ''' |
|
187 |
+ Use NUMBER=0, e.g. "--symbol 0", to exclude a character type |
|
188 |
+ from the output. |
|
189 |
+ ''' |
|
190 |
+ |
|
70 | 191 |
|
71 | 192 |
class ConfigurationOption(OptionGroupOption): |
193 |
+ """Configuration options for the CLI.""" |
|
72 | 194 |
option_group_name = 'Configuration' |
73 |
- epilog = 'Use $VISUAL or $EDITOR to configure the spawned editor.' |
|
195 |
+ epilog = ''' |
|
196 |
+ Use $VISUAL or $EDITOR to configure the spawned editor. |
|
197 |
+ ''' |
|
198 |
+ |
|
74 | 199 |
|
75 | 200 |
class StorageManagementOption(OptionGroupOption): |
201 |
+ """Storage management options for the CLI.""" |
|
76 | 202 |
option_group_name = 'Storage management' |
77 |
- epilog = 'Using "-" as PATH for standard input/standard output is supported.' |
|
203 |
+ epilog = ''' |
|
204 |
+ Using "-" as PATH for standard input/standard output is |
|
205 |
+ supported. |
|
206 |
+ ''' |
|
78 | 207 |
|
79 |
-def validate_occurrence_constraint( |
|
208 |
+def _validate_occurrence_constraint( |
|
80 | 209 |
ctx: click.Context, param: click.Parameter, value: Any, |
81 | 210 |
) -> int | None: |
211 |
+ """Check that the occurrence constraint is valid (int, 0 or larger).""" |
|
82 | 212 |
if value is None: |
83 | 213 |
return value |
84 | 214 |
if isinstance(value, int): |
... | ... |
@@ -92,9 +222,11 @@ def validate_occurrence_constraint( |
92 | 222 |
raise click.BadParameter('not a non-negative integer') |
93 | 223 |
return int_value |
94 | 224 |
|
95 |
-def validate_length( |
|
225 |
+ |
|
226 |
+def _validate_length( |
|
96 | 227 |
ctx: click.Context, param: click.Parameter, value: Any, |
97 | 228 |
) -> int | None: |
229 |
+ """Check that the length is valid (int, 1 or larger).""" |
|
98 | 230 |
if value is None: |
99 | 231 |
return value |
100 | 232 |
if isinstance(value, int): |
... | ... |
@@ -119,69 +251,90 @@ def validate_length( |
119 | 251 |
backups of the settings and the SSH key, if any. |
120 | 252 |
''', |
121 | 253 |
) |
122 |
-@click.option('-p', '--phrase', is_flag=True, |
|
254 |
+@click.option('-p', '--phrase', 'use_phrase', is_flag=True, |
|
123 | 255 |
help='prompts you for your passphrase', |
124 | 256 |
cls=PasswordGenerationOption) |
125 |
-@click.option('-k', '--key', is_flag=True, |
|
257 |
+@click.option('-k', '--key', 'use_key', is_flag=True, |
|
126 | 258 |
help='uses your SSH private key to generate passwords', |
127 | 259 |
cls=PasswordGenerationOption) |
128 |
-@click.option('-l', '--length', metavar='NUMBER', callback=validate_length, |
|
260 |
+@click.option('-l', '--length', metavar='NUMBER', |
|
261 |
+ callback=_validate_length, |
|
129 | 262 |
help='emits password of length NUMBER', |
130 | 263 |
cls=PasswordGenerationOption) |
131 | 264 |
@click.option('-r', '--repeat', metavar='NUMBER', |
132 |
- callback=validate_occurrence_constraint, |
|
265 |
+ callback=_validate_occurrence_constraint, |
|
133 | 266 |
help='allows maximum of NUMBER repeated adjacent chars', |
134 | 267 |
cls=PasswordGenerationOption) |
135 | 268 |
@click.option('--lower', metavar='NUMBER', |
136 |
- callback=validate_occurrence_constraint, |
|
269 |
+ callback=_validate_occurrence_constraint, |
|
137 | 270 |
help='includes at least NUMBER lowercase letters', |
138 | 271 |
cls=PasswordGenerationOption) |
139 | 272 |
@click.option('--upper', metavar='NUMBER', |
140 |
- callback=validate_occurrence_constraint, |
|
273 |
+ callback=_validate_occurrence_constraint, |
|
141 | 274 |
help='includes at least NUMBER uppercase letters', |
142 | 275 |
cls=PasswordGenerationOption) |
143 | 276 |
@click.option('--number', metavar='NUMBER', |
144 |
- callback=validate_occurrence_constraint, |
|
277 |
+ callback=_validate_occurrence_constraint, |
|
145 | 278 |
help='includes at least NUMBER digits', |
146 | 279 |
cls=PasswordGenerationOption) |
147 | 280 |
@click.option('--space', metavar='NUMBER', |
148 |
- callback=validate_occurrence_constraint, |
|
281 |
+ callback=_validate_occurrence_constraint, |
|
149 | 282 |
help='includes at least NUMBER spaces', |
150 | 283 |
cls=PasswordGenerationOption) |
151 | 284 |
@click.option('--dash', metavar='NUMBER', |
152 |
- callback=validate_occurrence_constraint, |
|
285 |
+ callback=_validate_occurrence_constraint, |
|
153 | 286 |
help='includes at least NUMBER "-" or "_"', |
154 | 287 |
cls=PasswordGenerationOption) |
155 | 288 |
@click.option('--symbol', metavar='NUMBER', |
156 |
- callback=validate_occurrence_constraint, |
|
289 |
+ callback=_validate_occurrence_constraint, |
|
157 | 290 |
help='includes at least NUMBER symbol chars', |
158 | 291 |
cls=PasswordGenerationOption) |
159 |
-@click.option('-n', '--notes', is_flag=True, |
|
292 |
+@click.option('-n', '--notes', 'edit_notes', is_flag=True, |
|
160 | 293 |
help='spawn an editor to edit notes for SERVICE', |
161 | 294 |
cls=ConfigurationOption) |
162 |
-@click.option('-c', '--config', is_flag=True, |
|
295 |
+@click.option('-c', '--config', 'store_config_only', is_flag=True, |
|
163 | 296 |
help='saves the given settings for SERVICE or global', |
164 | 297 |
cls=ConfigurationOption) |
165 |
-@click.option('-x', '--delete', is_flag=True, |
|
298 |
+@click.option('-x', '--delete', 'delete_service_settings', is_flag=True, |
|
166 | 299 |
help='deletes settings for SERVICE', |
167 | 300 |
cls=ConfigurationOption) |
168 | 301 |
@click.option('--delete-globals', is_flag=True, |
169 | 302 |
help='deletes the global shared settings', |
170 | 303 |
cls=ConfigurationOption) |
171 |
-@click.option('-X', '--clear', is_flag=True, |
|
304 |
+@click.option('-X', '--clear', 'clear_all_settings', is_flag=True, |
|
172 | 305 |
help='deletes all settings', |
173 | 306 |
cls=ConfigurationOption) |
174 |
-@click.option('-e', '--export', metavar='PATH', type=click.File('wt'), |
|
307 |
+@click.option('-e', '--export', 'export_settings', metavar='PATH', |
|
308 |
+ type=click.Path(file_okay=True, allow_dash=True, exists=False), |
|
175 | 309 |
help='export all saved settings into file PATH', |
176 | 310 |
cls=StorageManagementOption) |
177 |
-@click.option('-i', '--import', metavar='PATH', type=click.File('rt'), |
|
311 |
+@click.option('-i', '--import', 'import_settings', metavar='PATH', |
|
312 |
+ type=click.Path(file_okay=True, allow_dash=True, exists=False), |
|
178 | 313 |
help='import saved settings from file PATH', |
179 | 314 |
cls=StorageManagementOption) |
180 | 315 |
@click.version_option(version=dpp.__version__, prog_name=prog_name) |
181 | 316 |
@click.argument('service', required=False) |
182 | 317 |
@click.pass_context |
183 | 318 |
def derivepassphrase( |
184 |
- ctx: click.Context, service: str | None = None, **kwargs: Any, |
|
319 |
+ ctx: click.Context, /, *, |
|
320 |
+ service: str | None = None, |
|
321 |
+ use_phrase: bool = False, |
|
322 |
+ use_key: bool = False, |
|
323 |
+ length: int | None = None, |
|
324 |
+ repeat: int | None = None, |
|
325 |
+ lower: int | None = None, |
|
326 |
+ upper: int | None = None, |
|
327 |
+ number: int | None = None, |
|
328 |
+ space: int | None = None, |
|
329 |
+ dash: int | None = None, |
|
330 |
+ symbol: int | None = None, |
|
331 |
+ edit_notes: bool = False, |
|
332 |
+ store_config_only: bool = False, |
|
333 |
+ delete_service_settings: bool = False, |
|
334 |
+ delete_globals: bool = False, |
|
335 |
+ clear_all_settings: bool = False, |
|
336 |
+ export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None, |
|
337 |
+ import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None, |
|
185 | 338 |
) -> None: |
186 | 339 |
"""Derive a strong passphrase, deterministically, from a master secret. |
187 | 340 |
|
... | ... |
@@ -196,7 +349,9 @@ def derivepassphrase( |
196 | 349 |
|
197 | 350 |
This is a [`click`][CLICK]-powered command-line interface function, |
198 | 351 |
and not intended for programmatic use. Call with arguments |
199 |
- `['--help']` to see full documentation of the interface. |
|
352 |
+ `['--help']` to see full documentation of the interface. (See also |
|
353 |
+ [`click.testing.CliRunner`][] for controlled, programmatic |
|
354 |
+ invocation.) |
|
200 | 355 |
|
201 | 356 |
[CLICK]: https://click.palletsprojects.com/ |
202 | 357 |
|
... | ... |
@@ -205,125 +360,164 @@ def derivepassphrase( |
205 | 360 |
The `click` context. |
206 | 361 |
|
207 | 362 |
Other Parameters: |
208 |
- service (str | None): |
|
363 |
+ service: |
|
209 | 364 |
A service name. Required, unless operating on global |
210 | 365 |
settings or importing/exporting settings. |
211 |
- phrase (bool): |
|
366 |
+ use_phrase: |
|
212 | 367 |
Command-line argument `-p`/`--phrase`. If given, query the |
213 | 368 |
user for a passphrase instead of an SSH key. |
214 |
- key (bool): |
|
369 |
+ use_key: |
|
215 | 370 |
Command-line argument `-k`/`--key`. If given, query the |
216 | 371 |
user for an SSH key instead of a passphrase. |
217 |
- length (int | None): |
|
372 |
+ length: |
|
218 | 373 |
Command-line argument `-l`/`--length`. Override the default |
219 | 374 |
length of the generated passphrase. |
220 |
- repeat (int | None): |
|
375 |
+ repeat: |
|
221 | 376 |
Command-line argument `-r`/`--repeat`. Override the default |
222 | 377 |
repetition limit if positive, or disable the repetition |
223 | 378 |
limit if 0. |
224 |
- lower (int | None): |
|
379 |
+ lower: |
|
225 | 380 |
Command-line argument `--lower`. Require a given amount of |
226 | 381 |
ASCII lowercase characters if positive, else forbid ASCII |
227 | 382 |
lowercase characters if 0. |
228 |
- upper (int | None): |
|
383 |
+ upper: |
|
229 | 384 |
Command-line argument `--upper`. Same as `lower`, but for |
230 | 385 |
ASCII uppercase characters. |
231 |
- number (int | None): |
|
386 |
+ number: |
|
232 | 387 |
Command-line argument `--number`. Same as `lower`, but for |
233 | 388 |
ASCII digits. |
234 |
- space (int | None): |
|
389 |
+ space: |
|
235 | 390 |
Command-line argument `--number`. Same as `lower`, but for |
236 | 391 |
the space character. |
237 |
- dash (int | None): |
|
392 |
+ dash: |
|
238 | 393 |
Command-line argument `--number`. Same as `lower`, but for |
239 | 394 |
the hyphen-minus and underscore characters. |
240 |
- symbol (int | None): |
|
395 |
+ symbol: |
|
241 | 396 |
Command-line argument `--number`. Same as `lower`, but for |
242 | 397 |
all other ASCII printable characters (except backquote). |
243 |
- notes (bool): |
|
398 |
+ edit_notes: |
|
244 | 399 |
Command-line argument `-n`/`--notes`. If given, spawn an |
245 | 400 |
editor to edit notes for `service`. |
246 |
- config (bool): |
|
401 |
+ store_config_only: |
|
247 | 402 |
Command-line argument `-c`/`--config`. If given, saves the |
248 | 403 |
other given settings (`--key`, ..., `--symbol`) to the |
249 | 404 |
configuration file, either specifically for `service` or as |
250 | 405 |
global settings. |
251 |
- delete (bool): |
|
406 |
+ delete_service_settings: |
|
252 | 407 |
Command-line argument `-x`/`--delete`. If given, removes |
253 | 408 |
the settings for `service` from the configuration file. |
254 |
- delete_globals (bool): |
|
409 |
+ delete_globals: |
|
255 | 410 |
Command-line argument `--delete-globals`. If given, removes |
256 | 411 |
the global settings from the configuration file. |
257 |
- clear (bool): |
|
412 |
+ clear_all_settings: |
|
258 | 413 |
Command-line argument `-X`/`--clear`. If given, removes all |
259 | 414 |
settings from the configuration file. |
260 |
- export (TextIO | click.utils.LazyFile | None): |
|
261 |
- Command-line argument `-e`/`--export`. If given, exports |
|
262 |
- the settings to the given file object (or |
|
263 |
- a `click.utils.LazyFile` instance that eventually supplies |
|
264 |
- such a file object), which must be open for writing and |
|
265 |
- accept `str` inputs. |
|
266 |
- import (TextIO | click.utils.LazyFile | None): |
|
267 |
- Command-line argument `-i`/`--import`. If given, imports |
|
268 |
- the settings from the given file object (or |
|
269 |
- a `click.utils.LazyFile` instance that eventually supplies |
|
270 |
- such a file object), which must be open for reading and |
|
271 |
- yield `str` values. |
|
415 |
+ export_settings: |
|
416 |
+ Command-line argument `-e`/`--export`. If a file object, |
|
417 |
+ then it must be open for writing and accept `str` inputs. |
|
418 |
+ Otherwise, a filename to open for writing. Using `-` for |
|
419 |
+ standard output is supported. |
|
420 |
+ import_settings: |
|
421 |
+ Command-line argument `-i`/`--import`. If a file object, it |
|
422 |
+ must be open for reading and yield `str` values. Otherwise, |
|
423 |
+ a filename to open for reading. Using `-` for standard |
|
424 |
+ input is supported. |
|
272 | 425 |
|
273 | 426 |
""" |
274 |
- options: dict[type[click.Option], list[str]] = {} |
|
427 |
+ |
|
428 |
+ options_in_group: dict[type[click.Option], list[click.Option]] = {} |
|
429 |
+ params_by_str: dict[str, click.Parameter] = {} |
|
275 | 430 |
for param in ctx.command.params: |
276 | 431 |
if isinstance(param, click.Option): |
277 |
- param_name = param.human_readable_name |
|
278 | 432 |
group: type[click.Option] |
279 |
- if isinstance(param, PasswordGenerationOption): |
|
433 |
+ match param: |
|
434 |
+ case PasswordGenerationOption(): |
|
280 | 435 |
group = PasswordGenerationOption |
281 |
- elif isinstance(param, ConfigurationOption): |
|
436 |
+ case ConfigurationOption(): |
|
282 | 437 |
group = ConfigurationOption |
283 |
- elif isinstance(param, StorageManagementOption): |
|
438 |
+ case StorageManagementOption(): |
|
284 | 439 |
group = StorageManagementOption |
285 |
- elif isinstance(param, OptionGroupOption): |
|
286 |
- raise AssertionError(f'Unknown option group for {param!r}') |
|
287 |
- else: |
|
440 |
+ case OptionGroupOption(): |
|
441 |
+ raise AssertionError( |
|
442 |
+ f'Unknown option group for {param!r}') |
|
443 |
+ case _: |
|
288 | 444 |
group = click.Option |
289 |
- options.setdefault(group, []).append(param_name) |
|
445 |
+ options_in_group.setdefault(group, []).append(param) |
|
446 |
+ params_by_str[param.human_readable_name] = param |
|
447 |
+ for name in param.opts + param.secondary_opts: |
|
448 |
+ params_by_str[name] = param |
|
449 |
+ |
|
450 |
+ def is_param_set(param: click.Parameter): |
|
451 |
+ return bool(ctx.params.get(param.human_readable_name)) |
|
452 |
+ |
|
290 | 453 |
def check_incompatible_options( |
291 |
- param_name: str, *incompatible: str |
|
454 |
+ param: click.Parameter | str, *incompatible: click.Parameter | str, |
|
292 | 455 |
) -> None: |
293 |
- parsed_params = ctx.params |
|
294 |
- if parsed_params.get(param_name) is None: |
|
456 |
+ if isinstance(param, str): |
|
457 |
+ param = params_by_str[param] |
|
458 |
+ assert isinstance(param, click.Parameter) |
|
459 |
+ if not is_param_set(param): |
|
295 | 460 |
return |
296 | 461 |
for other in incompatible: |
297 |
- if other != param_name and parsed_params.get(other) is not None: |
|
298 |
- param_name = param_name.replace('_', '-') |
|
299 |
- other = other.replace('_', '-') |
|
300 |
- raise click.UsageError( |
|
301 |
- f'--{param_name} and --{other} are mutually exclusive') |
|
302 |
- check_incompatible_options('phrase', 'key') |
|
462 |
+ if isinstance(other, str): |
|
463 |
+ other = params_by_str[other] |
|
464 |
+ assert isinstance(other, click.Parameter) |
|
465 |
+ if other != param and is_param_set(other): |
|
466 |
+ opt_str = param.opts[0] |
|
467 |
+ other_str = other.opts[0] |
|
468 |
+ raise click.BadOptionUsage( |
|
469 |
+ opt_str, f'mutually exclusive with {other_str}', ctx=ctx) |
|
470 |
+ |
|
471 |
+ def get_config() -> dpp_types.VaultConfig: |
|
472 |
+ try: |
|
473 |
+ return _load_config() |
|
474 |
+ except FileNotFoundError: |
|
475 |
+ return {'services': {}} |
|
476 |
+ except Exception as e: |
|
477 |
+ ctx.fail(f'cannot load config: {e}') |
|
478 |
+ |
|
479 |
+ configuration: dpp_types.VaultConfig |
|
480 |
+ |
|
481 |
+ check_incompatible_options('--phrase', '--key') |
|
303 | 482 |
for group in (ConfigurationOption, StorageManagementOption): |
304 |
- for opt in options[group]: |
|
305 |
- if opt != 'config': |
|
306 |
- check_incompatible_options(opt, |
|
307 |
- *options[PasswordGenerationOption]) |
|
483 |
+ for opt in options_in_group[group]: |
|
484 |
+ if opt != params_by_str['--config']: |
|
485 |
+ check_incompatible_options( |
|
486 |
+ opt, *options_in_group[PasswordGenerationOption]) |
|
487 |
+ |
|
308 | 488 |
for group in (ConfigurationOption, StorageManagementOption): |
309 |
- for opt in options[group]: |
|
310 |
- check_incompatible_options(opt, |
|
311 |
- *options[ConfigurationOption], |
|
312 |
- *options[StorageManagementOption]) |
|
313 |
- for opt in ['notes', 'delete']: |
|
314 |
- if kwargs.get(opt) is not None and not service: |
|
315 |
- opt = opt.replace('_', '-') |
|
316 |
- raise click.UsageError(f'--{opt} requires a SERVICE') |
|
317 |
- for opt in ['delete_globals', 'clear'] + options[StorageManagementOption]: |
|
318 |
- if kwargs.get(opt) is not None and service: |
|
319 |
- opt = opt.replace('_', '-') |
|
489 |
+ for opt in options_in_group[group]: |
|
490 |
+ check_incompatible_options( |
|
491 |
+ opt, *options_in_group[ConfigurationOption], |
|
492 |
+ *options_in_group[StorageManagementOption]) |
|
493 |
+ sv_options = (options_in_group[PasswordGenerationOption] + |
|
494 |
+ [params_by_str['--notes'], params_by_str['--delete']]) |
|
495 |
+ sv_options.remove(params_by_str['--key']) |
|
496 |
+ sv_options.remove(params_by_str['--phrase']) |
|
497 |
+ for param in sv_options: |
|
498 |
+ if is_param_set(param) and not service: |
|
499 |
+ opt_str = param.opts[0] |
|
500 |
+ raise click.UsageError(f'{opt_str} requires a SERVICE') |
|
501 |
+ for param in [params_by_str['--key'], params_by_str['--phrase']]: |
|
502 |
+ if ( |
|
503 |
+ is_param_set(param) |
|
504 |
+ and not (service or is_param_set(params_by_str['--config'])) |
|
505 |
+ ): |
|
506 |
+ opt_str = param.opts[0] |
|
507 |
+ raise click.UsageError(f'{opt_str} requires a SERVICE or --config') |
|
508 |
+ no_sv_options = [params_by_str['--delete-globals'], |
|
509 |
+ params_by_str['--clear'], |
|
510 |
+ *options_in_group[StorageManagementOption]] |
|
511 |
+ for param in no_sv_options: |
|
512 |
+ if is_param_set(param) and service: |
|
513 |
+ opt_str = param.opts[0] |
|
320 | 514 |
raise click.UsageError( |
321 |
- f'--{opt} does not take a SERVICE argument') |
|
515 |
+ f'{opt_str} does not take a SERVICE argument') |
|
322 | 516 |
#if kwargs['length'] is None: |
323 | 517 |
# kwargs['length'] = dpp.Vault.__init__.__kwdefaults__['length'] |
324 | 518 |
#if kwargs['repeat'] is None: |
325 | 519 |
# kwargs['repeat'] = dpp.Vault.__init__.__kwdefaults__['repeat'] |
326 |
- click.echo(repr({'service': service, **kwargs})) |
|
520 |
+ click.echo(repr(ctx.params)) |
|
327 | 521 |
|
328 | 522 |
|
329 | 523 |
if __name__ == '__main__': |
330 | 524 |