Marco Ricci commited on 2025-03-16 13:23:05
              Zeige 3 geänderte Dateien mit 1449 Einfügungen und 1109 Löschungen.
            
Introduce a "vault context" object holding the associated data for the call to the `derivepassphrase vault` CLI, and implement the actual CLI as a collection of methods on this object. The "vault context" object mostly just wraps the click context object and encodes the dispatch table for the CLI operations. The main advantage of this modelling is the much smaller size per, and lower cyclomatic complexity of, each CLI operation. As an implementation detail, the old implementation of the `get_user_config` inner function's error handling branch for `OSError` had coverage testing purely by accident, because `get_user_config` was being called unconditionally, very early in the CLI control flow. The refactored CLI however reads the user configuration only as needed for the specific operation. So this error handling branch needs a new, separate test. As a further implementation detail, the `is_param_set` inner function no longer caches its result, because it is now an object method: `functools.cache` explicitly documents that adding a cache to object methods causes a circular reference between the cache and the object via the `self` parameter. Given the actual contents of this function, there appears to be little point in working around this memory issue just to outfit an already cheap operation (a single `Mapping.get` call) with a cache. As yet another further implementation detail, checking for misleading passphrases should work even if the passed configuration snippet is not technically a `dict`, but rather a `Mapping`. Finally, due to the vault context object now wrapping the click context, it is possible (and sensible) to wrap the logging calls into methods of the vault context object so that less out-of-bounds information needs to be passed to the logger explicitly at the call site.
| ... | ... | 
                      @@ -45,6 +45,7 @@ if TYPE_CHECKING:  | 
                  
| 45 | 45 | 
                        import socket  | 
                    
| 46 | 46 | 
                        from collections.abc import (  | 
                    
| 47 | 47 | 
                        Iterator,  | 
                    
| 48 | 
                        + Mapping,  | 
                    |
| 48 | 49 | 
                        Sequence,  | 
                    
| 49 | 50 | 
                        )  | 
                    
| 50 | 51 | 
                         | 
                    
| ... | ... | 
                      @@ -714,7 +715,7 @@ class ORIGIN(enum.Enum):  | 
                  
| 714 | 715 | 
                         | 
                    
| 715 | 716 | 
                        def check_for_misleading_passphrase(  | 
                    
| 716 | 717 | 
                        key: tuple[str, ...] | ORIGIN,  | 
                    
| 717 | 
                        - value: dict[str, Any],  | 
                    |
| 718 | 
                        + value: Mapping[str, Any],  | 
                    |
| 718 | 719 | 
                        *,  | 
                    
| 719 | 720 | 
                        main_config: dict[str, Any],  | 
                    
| 720 | 721 | 
                        ctx: click.Context | None = None,  | 
                    
| ... | ... | 
                      @@ -11,12 +11,12 @@ from __future__ import annotations  | 
                  
| 11 | 11 | 
                        import base64  | 
                    
| 12 | 12 | 
                        import collections  | 
                    
| 13 | 13 | 
                        import contextlib  | 
                    
| 14 | 
                        -import functools  | 
                    |
| 15 | 14 | 
                        import json  | 
                    
| 16 | 15 | 
                        import logging  | 
                    
| 17 | 16 | 
                        import os  | 
                    
| 18 | 17 | 
                        from typing import (  | 
                    
| 19 | 18 | 
                        TYPE_CHECKING,  | 
                    
| 19 | 
                        + Final,  | 
                    |
| 20 | 20 | 
                        Literal,  | 
                    
| 21 | 21 | 
                        NoReturn,  | 
                    
| 22 | 22 | 
                        TextIO,  | 
                    
| ... | ... | 
                      @@ -34,10 +34,8 @@ from derivepassphrase._internals import cli_helpers, cli_machinery  | 
                  
| 34 | 34 | 
                        from derivepassphrase._internals import cli_messages as _msg  | 
                    
| 35 | 35 | 
                         | 
                    
| 36 | 36 | 
                        if TYPE_CHECKING:  | 
                    
| 37 | 
                        - from collections.abc import (  | 
                    |
| 38 | 
                        - Callable,  | 
                    |
| 39 | 
                        - Sequence,  | 
                    |
| 40 | 
                        - )  | 
                    |
| 37 | 
                        + from collections.abc import Sequence  | 
                    |
| 38 | 
                        + from collections.abc import Set as AbstractSet  | 
                    |
| 41 | 39 | 
                         | 
                    
| 42 | 40 | 
                         __all__ = ('derivepassphrase',)
                       | 
                    
| 43 | 41 | 
                         | 
                    
| ... | ... | 
                      @@ -317,263 +315,788 @@ def derivepassphrase_export_vault(  | 
                  
| 317 | 315 | 
                        ctx.exit(1)  | 
                    
| 318 | 316 | 
                         | 
                    
| 319 | 317 | 
                         | 
                    
| 320 | 
                        -@derivepassphrase.command(  | 
                    |
| 321 | 
                        - 'vault',  | 
                    |
| 322 | 
                        -    context_settings={'help_option_names': ['-h', '--help']},
                       | 
                    |
| 323 | 
                        - cls=cli_machinery.CommandWithHelpGroups,  | 
                    |
| 324 | 
                        - help=(  | 
                    |
| 325 | 
                        - _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_01),  | 
                    |
| 326 | 
                        - _msg.TranslatedString(  | 
                    |
| 327 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_02,  | 
                    |
| 328 | 
                        - service_metavar=_msg.TranslatedString(  | 
                    |
| 329 | 
                        - _msg.Label.VAULT_METAVAR_SERVICE  | 
                    |
| 330 | 
                        - ),  | 
                    |
| 331 | 
                        - ),  | 
                    |
| 332 | 
                        - ),  | 
                    |
| 333 | 
                        - epilog=(  | 
                    |
| 334 | 
                        - _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_EPILOG_01),  | 
                    |
| 335 | 
                        - _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_EPILOG_02),  | 
                    |
| 336 | 
                        - ),  | 
                    |
| 318 | 
                        +class _VaultContext: # noqa: PLR0904  | 
                    |
| 319 | 
                        + """The context for the "vault" command-line interface.  | 
                    |
| 320 | 
                        +  | 
                    |
| 321 | 
                        + This context object -- wrapping a [`click.Context`][] object --  | 
                    |
| 322 | 
                        + encapsulates a single call to the `derivepassphrase vault`  | 
                    |
| 323 | 
                        + command-line. Although fully documented, this class is an  | 
                    |
| 324 | 
                        + implementation detail of the `derivepassphrase vault` command-line  | 
                    |
| 325 | 
                        + and should not be instantiated directly by users or API clients.  | 
                    |
| 326 | 
                        +  | 
                    |
| 327 | 
                        + Attributes:  | 
                    |
| 328 | 
                        + logger:  | 
                    |
| 329 | 
                        + The logger used for warnings and error messages.  | 
                    |
| 330 | 
                        + deprecation:  | 
                    |
| 331 | 
                        + The logger used for deprecation warnings.  | 
                    |
| 332 | 
                        + ctx:  | 
                    |
| 333 | 
                        + The underlying [`click.Context`][] from which the  | 
                    |
| 334 | 
                        + command-line settings and parameter values are queried.  | 
                    |
| 335 | 
                        + all_ops:  | 
                    |
| 336 | 
                        + A list of operations supported by the CLI, in the  | 
                    |
| 337 | 
                        + order that they are queried in the `click` context. The  | 
                    |
| 338 | 
                        + final entry is the default operation, and does not  | 
                    |
| 339 | 
                        + correspond to a `click` parameter; all others are `click`  | 
                    |
| 340 | 
                        + parameter names.  | 
                    |
| 341 | 
                        + readwrite_ops:  | 
                    |
| 342 | 
                        + A set of operations which modify the `derivepassphrase`  | 
                    |
| 343 | 
                        + state. All other operations are read-only.  | 
                    |
| 344 | 
                        + options_in_group:  | 
                    |
| 345 | 
                        + A mapping of option group names to lists of known options  | 
                    |
| 346 | 
                        + from this group. Used during the validation of the command  | 
                    |
| 347 | 
                        + line.  | 
                    |
| 348 | 
                        + params_by_str:  | 
                    |
| 349 | 
                        + A mapping of option names (long names, short names, etc.) to  | 
                    |
| 350 | 
                        + option objects. Used during the validation of the command  | 
                    |
| 351 | 
                        + line.  | 
                    |
| 352 | 
                        +  | 
                    |
| 353 | 
                        + """  | 
                    |
| 354 | 
                        +  | 
                    |
| 355 | 
                        + logger: Final = logging.getLogger(PROG_NAME)  | 
                    |
| 356 | 
                        + """"""  | 
                    |
| 357 | 
                        + deprecation: Final = logging.getLogger(PROG_NAME + '.deprecation')  | 
                    |
| 358 | 
                        + """"""  | 
                    |
| 359 | 
                        + ctx: Final[click.Context]  | 
                    |
| 360 | 
                        + """"""  | 
                    |
| 361 | 
                        + all_ops: tuple[str, ...] = (  | 
                    |
| 362 | 
                        + 'delete_service_settings',  | 
                    |
| 363 | 
                        + 'delete_globals',  | 
                    |
| 364 | 
                        + 'clear_all_settings',  | 
                    |
| 365 | 
                        + 'import_settings',  | 
                    |
| 366 | 
                        + 'export_settings',  | 
                    |
| 367 | 
                        + 'store_config_only',  | 
                    |
| 368 | 
                        + # The default op "derive_passphrase" must be last!  | 
                    |
| 369 | 
                        + 'derive_passphrase',  | 
                    |
| 337 | 370 | 
                        )  | 
                    
| 338 | 
                        -@click.option(  | 
                    |
| 339 | 
                        - '-p',  | 
                    |
| 340 | 
                        - '--phrase',  | 
                    |
| 341 | 
                        - 'use_phrase',  | 
                    |
| 342 | 
                        - is_flag=True,  | 
                    |
| 343 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 344 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_PHRASE_HELP_TEXT  | 
                    |
| 345 | 
                        - ),  | 
                    |
| 346 | 
                        - cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 371 | 
                        +    readwrite_ops: AbstractSet[str] = frozenset({
                       | 
                    |
| 372 | 
                        + 'delete_service_settings',  | 
                    |
| 373 | 
                        + 'delete_globals',  | 
                    |
| 374 | 
                        + 'clear_all_settings',  | 
                    |
| 375 | 
                        + 'import_settings',  | 
                    |
| 376 | 
                        + 'store_config_only',  | 
                    |
| 377 | 
                        + })  | 
                    |
| 378 | 
                        + options_in_group: dict[type[click.Option], list[click.Option]]  | 
                    |
| 379 | 
                        + params_by_str: dict[str, click.Parameter]  | 
                    |
| 380 | 
                        +  | 
                    |
| 381 | 
                        + def __init__(self, ctx: click.Context, /) -> None:  | 
                    |
| 382 | 
                        + """Initialize the vault context.  | 
                    |
| 383 | 
                        +  | 
                    |
| 384 | 
                        + Args:  | 
                    |
| 385 | 
                        + ctx:  | 
                    |
| 386 | 
                        + The underlying [`click.Context`][] from which the  | 
                    |
| 387 | 
                        + command-line settings and parameter values are queried.  | 
                    |
| 388 | 
                        +  | 
                    |
| 389 | 
                        + """  | 
                    |
| 390 | 
                        + self.ctx = ctx  | 
                    |
| 391 | 
                        +        self.params_by_str = {}
                       | 
                    |
| 392 | 
                        +        self.options_in_group = {}
                       | 
                    |
| 393 | 
                        + for param in ctx.command.params:  | 
                    |
| 394 | 
                        + if isinstance(param, click.Option):  | 
                    |
| 395 | 
                        + group: type[click.Option]  | 
                    |
| 396 | 
                        + known_option_groups = [  | 
                    |
| 397 | 
                        + cli_machinery.PassphraseGenerationOption,  | 
                    |
| 398 | 
                        + cli_machinery.ConfigurationOption,  | 
                    |
| 399 | 
                        + cli_machinery.StorageManagementOption,  | 
                    |
| 400 | 
                        + cli_machinery.LoggingOption,  | 
                    |
| 401 | 
                        + cli_machinery.CompatibilityOption,  | 
                    |
| 402 | 
                        + cli_machinery.StandardOption,  | 
                    |
| 403 | 
                        + ]  | 
                    |
| 404 | 
                        + if isinstance(param, cli_machinery.OptionGroupOption):  | 
                    |
| 405 | 
                        + for class_ in known_option_groups:  | 
                    |
| 406 | 
                        + if isinstance(param, class_):  | 
                    |
| 407 | 
                        + group = class_  | 
                    |
| 408 | 
                        + break  | 
                    |
| 409 | 
                        + else: # pragma: no cover  | 
                    |
| 410 | 
                        +                        assert False, f'Unknown option group for {param!r}'  # noqa: B011,PT015
                       | 
                    |
| 411 | 
                        + else:  | 
                    |
| 412 | 
                        + group = click.Option  | 
                    |
| 413 | 
                        + self.options_in_group.setdefault(group, []).append(param)  | 
                    |
| 414 | 
                        + self.params_by_str[param.human_readable_name] = param  | 
                    |
| 415 | 
                        + for name in param.opts + param.secondary_opts:  | 
                    |
| 416 | 
                        + self.params_by_str[name] = param  | 
                    |
| 417 | 
                        +  | 
                    |
| 418 | 
                        + def is_param_set(self, param: click.Parameter, /) -> bool:  | 
                    |
| 419 | 
                        + """Return true if the parameter is set."""  | 
                    |
| 420 | 
                        + return bool(self.ctx.params.get(param.human_readable_name))  | 
                    |
| 421 | 
                        +  | 
                    |
| 422 | 
                        + def option_name(self, param: click.Parameter | str, /) -> str:  | 
                    |
| 423 | 
                        + """Return the option name of a parameter.  | 
                    |
| 424 | 
                        +  | 
                    |
| 425 | 
                        + Annoyingly, `param.human_readable_name` contains the *function*  | 
                    |
| 426 | 
                        + parameter name, not the list of option names. *Those* are  | 
                    |
| 427 | 
                        + stashed in the `.opts` and `.secondary_opts` attributes, which  | 
                    |
| 428 | 
                        + are visible in the `.to_info_dict()` output, but not otherwise  | 
                    |
| 429 | 
                        + documented. We return the shortest one among the long-form  | 
                    |
| 430 | 
                        + option names.  | 
                    |
| 431 | 
                        +  | 
                    |
| 432 | 
                        + Args:  | 
                    |
| 433 | 
                        + param: The parameter whose option name is requested.  | 
                    |
| 434 | 
                        +  | 
                    |
| 435 | 
                        + Raises:  | 
                    |
| 436 | 
                        + ValueError: The parameter has no long-form option names.  | 
                    |
| 437 | 
                        +  | 
                    |
| 438 | 
                        + """  | 
                    |
| 439 | 
                        + param = self.params_by_str[param] if isinstance(param, str) else param  | 
                    |
| 440 | 
                        + names = [param.human_readable_name, *param.opts, *param.secondary_opts]  | 
                    |
| 441 | 
                        +        option_names = [n for n in names if n.startswith('--')]
                       | 
                    |
| 442 | 
                        + return min(option_names, key=len)  | 
                    |
| 443 | 
                        +  | 
                    |
| 444 | 
                        + def check_incompatible_options(  | 
                    |
| 445 | 
                        + self,  | 
                    |
| 446 | 
                        + param1: click.Parameter | str,  | 
                    |
| 447 | 
                        + param2: click.Parameter | str,  | 
                    |
| 448 | 
                        + ) -> None:  | 
                    |
| 449 | 
                        + """Raise an error if the two options are incompatible.  | 
                    |
| 450 | 
                        +  | 
                    |
| 451 | 
                        + Raises:  | 
                    |
| 452 | 
                        + click.BadOptionUsage: The given options are incompatible.  | 
                    |
| 453 | 
                        +  | 
                    |
| 454 | 
                        + """  | 
                    |
| 455 | 
                        + param1 = (  | 
                    |
| 456 | 
                        + self.params_by_str[param1] if isinstance(param1, str) else param1  | 
                    |
| 347 | 457 | 
                        )  | 
                    
| 348 | 
                        -@click.option(  | 
                    |
| 349 | 
                        - '-k',  | 
                    |
| 350 | 
                        - '--key',  | 
                    |
| 351 | 
                        - 'use_key',  | 
                    |
| 352 | 
                        - is_flag=True,  | 
                    |
| 353 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 354 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_KEY_HELP_TEXT  | 
                    |
| 355 | 
                        - ),  | 
                    |
| 356 | 
                        - cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 458 | 
                        + param2 = (  | 
                    |
| 459 | 
                        + self.params_by_str[param2] if isinstance(param2, str) else param2  | 
                    |
| 357 | 460 | 
                        )  | 
                    
| 358 | 
                        -@click.option(  | 
                    |
| 359 | 
                        - '-l',  | 
                    |
| 360 | 
                        - '--length',  | 
                    |
| 361 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 362 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 363 | 
                        - ),  | 
                    |
| 364 | 
                        - callback=cli_machinery.validate_length,  | 
                    |
| 365 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 366 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_LENGTH_HELP_TEXT,  | 
                    |
| 367 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 368 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 369 | 
                        - ),  | 
                    |
| 370 | 
                        - ),  | 
                    |
| 371 | 
                        - cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 461 | 
                        + if param1 == param2:  | 
                    |
| 462 | 
                        + return  | 
                    |
| 463 | 
                        + if not self.is_param_set(param1):  | 
                    |
| 464 | 
                        + return  | 
                    |
| 465 | 
                        + if self.is_param_set(param2):  | 
                    |
| 466 | 
                        + param1_str = self.option_name(param1)  | 
                    |
| 467 | 
                        + param2_str = self.option_name(param2)  | 
                    |
| 468 | 
                        + raise click.BadOptionUsage(  | 
                    |
| 469 | 
                        + param1_str,  | 
                    |
| 470 | 
                        + str(  | 
                    |
| 471 | 
                        + _msg.TranslatedString(  | 
                    |
| 472 | 
                        + _msg.ErrMsgTemplate.PARAMS_MUTUALLY_EXCLUSIVE,  | 
                    |
| 473 | 
                        + param1=param1_str,  | 
                    |
| 474 | 
                        + param2=param2_str,  | 
                    |
| 372 | 475 | 
                        )  | 
                    
| 373 | 
                        -@click.option(  | 
                    |
| 374 | 
                        - '-r',  | 
                    |
| 375 | 
                        - '--repeat',  | 
                    |
| 376 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 377 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 378 | 
                        - ),  | 
                    |
| 379 | 
                        - callback=cli_machinery.validate_occurrence_constraint,  | 
                    |
| 380 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 381 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_REPEAT_HELP_TEXT,  | 
                    |
| 382 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 383 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 384 | 
                        - ),  | 
                    |
| 385 | 476 | 
                        ),  | 
                    
| 386 | 
                        - cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 477 | 
                        + ctx=self.ctx,  | 
                    |
| 387 | 478 | 
                        )  | 
                    
| 388 | 
                        -@click.option(  | 
                    |
| 389 | 
                        - '--lower',  | 
                    |
| 390 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 391 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 392 | 
                        - ),  | 
                    |
| 393 | 
                        - callback=cli_machinery.validate_occurrence_constraint,  | 
                    |
| 394 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 395 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_LOWER_HELP_TEXT,  | 
                    |
| 396 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 397 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 398 | 
                        - ),  | 
                    |
| 399 | 
                        - ),  | 
                    |
| 400 | 
                        - cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 479 | 
                        + return  | 
                    |
| 480 | 
                        +  | 
                    |
| 481 | 
                        + def err(self, msg: Any, /, **kwargs: Any) -> NoReturn: # noqa: ANN401  | 
                    |
| 482 | 
                        + """Log an error, then abort the function call.  | 
                    |
| 483 | 
                        +  | 
                    |
| 484 | 
                        + We ensure that color handling is done properly before the error  | 
                    |
| 485 | 
                        + is logged.  | 
                    |
| 486 | 
                        +  | 
                    |
| 487 | 
                        + """  | 
                    |
| 488 | 
                        +        stacklevel = kwargs.pop('stacklevel', 1)
                       | 
                    |
| 489 | 
                        + stacklevel += 1  | 
                    |
| 490 | 
                        +        extra = kwargs.pop('extra', {})
                       | 
                    |
| 491 | 
                        +        extra.setdefault('color', self.ctx.color)
                       | 
                    |
| 492 | 
                        + self.logger.error(msg, stacklevel=stacklevel, extra=extra, **kwargs)  | 
                    |
| 493 | 
                        + self.ctx.exit(1)  | 
                    |
| 494 | 
                        +  | 
                    |
| 495 | 
                        + def warning(self, msg: Any, /, **kwargs: Any) -> None: # noqa: ANN401  | 
                    |
| 496 | 
                        + """Log a warning.  | 
                    |
| 497 | 
                        +  | 
                    |
| 498 | 
                        + We ensure that color handling is done properly before the  | 
                    |
| 499 | 
                        + warning is logged.  | 
                    |
| 500 | 
                        +  | 
                    |
| 501 | 
                        + """  | 
                    |
| 502 | 
                        +        stacklevel = kwargs.pop('stacklevel', 1)
                       | 
                    |
| 503 | 
                        + stacklevel += 1  | 
                    |
| 504 | 
                        +        extra = kwargs.pop('extra', {})
                       | 
                    |
| 505 | 
                        +        extra.setdefault('color', self.ctx.color)
                       | 
                    |
| 506 | 
                        + self.logger.warning(msg, stacklevel=stacklevel, extra=extra, **kwargs)  | 
                    |
| 507 | 
                        +  | 
                    |
| 508 | 
                        + def deprecation_warning(self, msg: Any, /, **kwargs: Any) -> None: # noqa: ANN401  | 
                    |
| 509 | 
                        + """Log a deprecation warning.  | 
                    |
| 510 | 
                        +  | 
                    |
| 511 | 
                        + We ensure that color handling is done properly before the  | 
                    |
| 512 | 
                        + warning is logged.  | 
                    |
| 513 | 
                        +  | 
                    |
| 514 | 
                        + """  | 
                    |
| 515 | 
                        +        stacklevel = kwargs.pop('stacklevel', 1)
                       | 
                    |
| 516 | 
                        + stacklevel += 1  | 
                    |
| 517 | 
                        +        extra = kwargs.pop('extra', {})
                       | 
                    |
| 518 | 
                        +        extra.setdefault('color', self.ctx.color)
                       | 
                    |
| 519 | 
                        + self.deprecation.warning(  | 
                    |
| 520 | 
                        + msg, stacklevel=stacklevel, extra=extra, **kwargs  | 
                    |
| 401 | 521 | 
                        )  | 
                    
| 402 | 
                        -@click.option(  | 
                    |
| 403 | 
                        - '--upper',  | 
                    |
| 404 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 405 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 406 | 
                        - ),  | 
                    |
| 407 | 
                        - callback=cli_machinery.validate_occurrence_constraint,  | 
                    |
| 408 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 409 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_UPPER_HELP_TEXT,  | 
                    |
| 410 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 411 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 412 | 
                        - ),  | 
                    |
| 413 | 
                        - ),  | 
                    |
| 414 | 
                        - cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 522 | 
                        +  | 
                    |
| 523 | 
                        + def deprecation_info(self, msg: Any, /, **kwargs: Any) -> None: # noqa: ANN401  | 
                    |
| 524 | 
                        + """Log a deprecation info message.  | 
                    |
| 525 | 
                        +  | 
                    |
| 526 | 
                        + We ensure that color handling is done properly before the  | 
                    |
| 527 | 
                        + warning is logged.  | 
                    |
| 528 | 
                        +  | 
                    |
| 529 | 
                        + """  | 
                    |
| 530 | 
                        +        stacklevel = kwargs.pop('stacklevel', 1)
                       | 
                    |
| 531 | 
                        + stacklevel += 1  | 
                    |
| 532 | 
                        +        extra = kwargs.pop('extra', {})
                       | 
                    |
| 533 | 
                        +        extra.setdefault('color', self.ctx.color)
                       | 
                    |
| 534 | 
                        + self.deprecation.info(  | 
                    |
| 535 | 
                        + msg, stacklevel=stacklevel, extra=extra, **kwargs  | 
                    |
| 415 | 536 | 
                        )  | 
                    
| 416 | 
                        -@click.option(  | 
                    |
| 417 | 
                        - '--number',  | 
                    |
| 418 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 419 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 420 | 
                        - ),  | 
                    |
| 421 | 
                        - callback=cli_machinery.validate_occurrence_constraint,  | 
                    |
| 422 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 423 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_NUMBER_HELP_TEXT,  | 
                    |
| 424 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 425 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 426 | 
                        - ),  | 
                    |
| 537 | 
                        +  | 
                    |
| 538 | 
                        + def get_config(self) -> _types.VaultConfig:  | 
                    |
| 539 | 
                        + """Return the vault configuration stored on disk.  | 
                    |
| 540 | 
                        +  | 
                    |
| 541 | 
                        + If no configuration is stored, return an empty configuration.  | 
                    |
| 542 | 
                        +  | 
                    |
| 543 | 
                        + If only a v0.1-style configuration is found, attempt to migrate  | 
                    |
| 544 | 
                        + it. This will be removed in v1.0.  | 
                    |
| 545 | 
                        +  | 
                    |
| 546 | 
                        + """  | 
                    |
| 547 | 
                        + try:  | 
                    |
| 548 | 
                        + return cli_helpers.load_config()  | 
                    |
| 549 | 
                        + except FileNotFoundError:  | 
                    |
| 550 | 
                        + # TODO(the-13th-letter): Return the empty default  | 
                    |
| 551 | 
                        + # configuration directly.  | 
                    |
| 552 | 
                        + # https://the13thletter.info/derivepassphrase/latest/upgrade-notes.html#v1.0-old-settings-file  | 
                    |
| 553 | 
                        + try:  | 
                    |
| 554 | 
                        + backup_config, exc = cli_helpers.migrate_and_load_old_config()  | 
                    |
| 555 | 
                        + except FileNotFoundError:  | 
                    |
| 556 | 
                        +                return {'services': {}}
                       | 
                    |
| 557 | 
                        + old_name = cli_helpers.config_filename(  | 
                    |
| 558 | 
                        + subsystem='old settings.json'  | 
                    |
| 559 | 
                        + ).name  | 
                    |
| 560 | 
                        + new_name = cli_helpers.config_filename(subsystem='vault').name  | 
                    |
| 561 | 
                        + self.deprecation_warning(  | 
                    |
| 562 | 
                        + _msg.TranslatedString(  | 
                    |
| 563 | 
                        + _msg.WarnMsgTemplate.V01_STYLE_CONFIG,  | 
                    |
| 564 | 
                        + old=old_name,  | 
                    |
| 565 | 
                        + new=new_name,  | 
                    |
| 427 | 566 | 
                        ),  | 
                    
| 428 | 
                        - cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 429 | 567 | 
                        )  | 
                    
| 430 | 
                        -@click.option(  | 
                    |
| 431 | 
                        - '--space',  | 
                    |
| 432 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 433 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 434 | 
                        - ),  | 
                    |
| 435 | 
                        - callback=cli_machinery.validate_occurrence_constraint,  | 
                    |
| 436 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 437 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_SPACE_HELP_TEXT,  | 
                    |
| 438 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 439 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 440 | 
                        - ),  | 
                    |
| 441 | 
                        - ),  | 
                    |
| 442 | 
                        - cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 568 | 
                        + if isinstance(exc, OSError):  | 
                    |
| 569 | 
                        + self.warning(  | 
                    |
| 570 | 
                        + _msg.TranslatedString(  | 
                    |
| 571 | 
                        + _msg.WarnMsgTemplate.FAILED_TO_MIGRATE_CONFIG,  | 
                    |
| 572 | 
                        + path=new_name,  | 
                    |
| 573 | 
                        + error=exc.strerror,  | 
                    |
| 574 | 
                        + filename=exc.filename,  | 
                    |
| 575 | 
                        + ).maybe_without_filename(),  | 
                    |
| 443 | 576 | 
                        )  | 
                    
| 444 | 
                        -@click.option(  | 
                    |
| 445 | 
                        - '--dash',  | 
                    |
| 446 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 447 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 448 | 
                        - ),  | 
                    |
| 449 | 
                        - callback=cli_machinery.validate_occurrence_constraint,  | 
                    |
| 450 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 451 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_DASH_HELP_TEXT,  | 
                    |
| 452 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 453 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 454 | 
                        - ),  | 
                    |
| 577 | 
                        + else:  | 
                    |
| 578 | 
                        + self.deprecation_info(  | 
                    |
| 579 | 
                        + _msg.TranslatedString(  | 
                    |
| 580 | 
                        + _msg.InfoMsgTemplate.SUCCESSFULLY_MIGRATED,  | 
                    |
| 581 | 
                        + path=new_name,  | 
                    |
| 455 | 582 | 
                        ),  | 
                    
| 456 | 
                        - cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 457 | 583 | 
                        )  | 
                    
| 458 | 
                        -@click.option(  | 
                    |
| 459 | 
                        - '--symbol',  | 
                    |
| 460 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 461 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 462 | 
                        - ),  | 
                    |
| 463 | 
                        - callback=cli_machinery.validate_occurrence_constraint,  | 
                    |
| 464 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 465 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_SYMBOL_HELP_TEXT,  | 
                    |
| 466 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 467 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 468 | 
                        - ),  | 
                    |
| 469 | 
                        - ),  | 
                    |
| 470 | 
                        - cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 584 | 
                        + return backup_config  | 
                    |
| 585 | 
                        + except OSError as exc:  | 
                    |
| 586 | 
                        + self.err(  | 
                    |
| 587 | 
                        + _msg.TranslatedString(  | 
                    |
| 588 | 
                        + _msg.ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS,  | 
                    |
| 589 | 
                        + error=exc.strerror,  | 
                    |
| 590 | 
                        + filename=exc.filename,  | 
                    |
| 591 | 
                        + ).maybe_without_filename(),  | 
                    |
| 471 | 592 | 
                        )  | 
                    
| 472 | 
                        -@click.option(  | 
                    |
| 473 | 
                        - '-n',  | 
                    |
| 474 | 
                        - '--notes',  | 
                    |
| 475 | 
                        - 'edit_notes',  | 
                    |
| 476 | 
                        - is_flag=True,  | 
                    |
| 477 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 478 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_HELP_TEXT,  | 
                    |
| 593 | 
                        + except Exception as exc: # noqa: BLE001  | 
                    |
| 594 | 
                        + self.err(  | 
                    |
| 595 | 
                        + _msg.TranslatedString(  | 
                    |
| 596 | 
                        + _msg.ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS,  | 
                    |
| 597 | 
                        + error=str(exc),  | 
                    |
| 598 | 
                        + filename=None,  | 
                    |
| 599 | 
                        + ).maybe_without_filename(),  | 
                    |
| 600 | 
                        + exc_info=exc,  | 
                    |
| 601 | 
                        + )  | 
                    |
| 602 | 
                        +  | 
                    |
| 603 | 
                        + def put_config(self, config: _types.VaultConfig, /) -> None:  | 
                    |
| 604 | 
                        + """Store the given vault configuration to disk."""  | 
                    |
| 605 | 
                        + try:  | 
                    |
| 606 | 
                        + cli_helpers.save_config(config)  | 
                    |
| 607 | 
                        + except OSError as exc:  | 
                    |
| 608 | 
                        + self.err(  | 
                    |
| 609 | 
                        + _msg.TranslatedString(  | 
                    |
| 610 | 
                        + _msg.ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS,  | 
                    |
| 611 | 
                        + error=exc.strerror,  | 
                    |
| 612 | 
                        + filename=exc.filename,  | 
                    |
| 613 | 
                        + ).maybe_without_filename(),  | 
                    |
| 614 | 
                        + )  | 
                    |
| 615 | 
                        + except Exception as exc: # noqa: BLE001  | 
                    |
| 616 | 
                        + self.err(  | 
                    |
| 617 | 
                        + _msg.TranslatedString(  | 
                    |
| 618 | 
                        + _msg.ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS,  | 
                    |
| 619 | 
                        + error=str(exc),  | 
                    |
| 620 | 
                        + filename=None,  | 
                    |
| 621 | 
                        + ).maybe_without_filename(),  | 
                    |
| 622 | 
                        + exc_info=exc,  | 
                    |
| 623 | 
                        + )  | 
                    |
| 624 | 
                        +  | 
                    |
| 625 | 
                        + def get_user_config(self) -> dict[str, Any]:  | 
                    |
| 626 | 
                        + """Return the global user configuration stored on disk.  | 
                    |
| 627 | 
                        +  | 
                    |
| 628 | 
                        + If no configuration is stored, return an empty configuration.  | 
                    |
| 629 | 
                        +  | 
                    |
| 630 | 
                        + """  | 
                    |
| 631 | 
                        + try:  | 
                    |
| 632 | 
                        + return cli_helpers.load_user_config()  | 
                    |
| 633 | 
                        + except FileNotFoundError:  | 
                    |
| 634 | 
                        +            return {}
                       | 
                    |
| 635 | 
                        + except OSError as exc:  | 
                    |
| 636 | 
                        + self.err(  | 
                    |
| 637 | 
                        + _msg.TranslatedString(  | 
                    |
| 638 | 
                        + _msg.ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG,  | 
                    |
| 639 | 
                        + error=exc.strerror,  | 
                    |
| 640 | 
                        + filename=exc.filename,  | 
                    |
| 641 | 
                        + ).maybe_without_filename(),  | 
                    |
| 642 | 
                        + )  | 
                    |
| 643 | 
                        + except Exception as exc: # noqa: BLE001  | 
                    |
| 644 | 
                        + self.err(  | 
                    |
| 645 | 
                        + _msg.TranslatedString(  | 
                    |
| 646 | 
                        + _msg.ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG,  | 
                    |
| 647 | 
                        + error=str(exc),  | 
                    |
| 648 | 
                        + filename=None,  | 
                    |
| 649 | 
                        + ).maybe_without_filename(),  | 
                    |
| 650 | 
                        + exc_info=exc,  | 
                    |
| 651 | 
                        + )  | 
                    |
| 652 | 
                        +  | 
                    |
| 653 | 
                        + def validate_command_line(self) -> None: # noqa: C901,PLR0912  | 
                    |
| 654 | 
                        + """Check for missing/extra arguments and conflicting options.  | 
                    |
| 655 | 
                        +  | 
                    |
| 656 | 
                        + Raises:  | 
                    |
| 657 | 
                        + click.UsageError:  | 
                    |
| 658 | 
                        + The command-line is invalid because of missing or extra  | 
                    |
| 659 | 
                        + arguments, or because of conflicting options. The error  | 
                    |
| 660 | 
                        + message contains further details.  | 
                    |
| 661 | 
                        +  | 
                    |
| 662 | 
                        + """  | 
                    |
| 663 | 
                        + param: click.Parameter  | 
                    |
| 664 | 
                        +        self.check_incompatible_options('--phrase', '--key')
                       | 
                    |
| 665 | 
                        + for group in (  | 
                    |
| 666 | 
                        + cli_machinery.ConfigurationOption,  | 
                    |
| 667 | 
                        + cli_machinery.StorageManagementOption,  | 
                    |
| 668 | 
                        + ):  | 
                    |
| 669 | 
                        + for opt in self.options_in_group[group]:  | 
                    |
| 670 | 
                        +                if opt not in {
                       | 
                    |
| 671 | 
                        + self.params_by_str['--config'],  | 
                    |
| 672 | 
                        + self.params_by_str['--notes'],  | 
                    |
| 673 | 
                        + }:  | 
                    |
| 674 | 
                        + for other_opt in self.options_in_group[  | 
                    |
| 675 | 
                        + cli_machinery.PassphraseGenerationOption  | 
                    |
| 676 | 
                        + ]:  | 
                    |
| 677 | 
                        + self.check_incompatible_options(opt, other_opt)  | 
                    |
| 678 | 
                        +  | 
                    |
| 679 | 
                        + for group in (  | 
                    |
| 680 | 
                        + cli_machinery.ConfigurationOption,  | 
                    |
| 681 | 
                        + cli_machinery.StorageManagementOption,  | 
                    |
| 682 | 
                        + ):  | 
                    |
| 683 | 
                        + for opt in self.options_in_group[group]:  | 
                    |
| 684 | 
                        + for other_opt in self.options_in_group[  | 
                    |
| 685 | 
                        + cli_machinery.ConfigurationOption  | 
                    |
| 686 | 
                        + ]:  | 
                    |
| 687 | 
                        +                    if {opt, other_opt} != {
                       | 
                    |
| 688 | 
                        + self.params_by_str['--config'],  | 
                    |
| 689 | 
                        + self.params_by_str['--notes'],  | 
                    |
| 690 | 
                        + }:  | 
                    |
| 691 | 
                        + self.check_incompatible_options(opt, other_opt)  | 
                    |
| 692 | 
                        + for other_opt in self.options_in_group[  | 
                    |
| 693 | 
                        + cli_machinery.StorageManagementOption  | 
                    |
| 694 | 
                        + ]:  | 
                    |
| 695 | 
                        + self.check_incompatible_options(opt, other_opt)  | 
                    |
| 696 | 
                        + service: str | None = self.ctx.params['service']  | 
                    |
| 479 | 697 | 
                        service_metavar = _msg.TranslatedString(  | 
                    
| 480 | 698 | 
                        _msg.Label.VAULT_METAVAR_SERVICE  | 
                    
| 481 | 
                        - ),  | 
                    |
| 482 | 
                        - ),  | 
                    |
| 483 | 
                        - cls=cli_machinery.ConfigurationOption,  | 
                    |
| 484 | 699 | 
                        )  | 
                    
| 485 | 
                        -@click.option(  | 
                    |
| 486 | 
                        - '-c',  | 
                    |
| 487 | 
                        - '--config',  | 
                    |
| 488 | 
                        - 'store_config_only',  | 
                    |
| 489 | 
                        - is_flag=True,  | 
                    |
| 490 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 491 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_CONFIG_HELP_TEXT,  | 
                    |
| 700 | 
                        + sv_or_global_options = self.options_in_group[  | 
                    |
| 701 | 
                        + cli_machinery.PassphraseGenerationOption  | 
                    |
| 702 | 
                        + ]  | 
                    |
| 703 | 
                        + for param in sv_or_global_options:  | 
                    |
| 704 | 
                        + if self.is_param_set(param) and not (  | 
                    |
| 705 | 
                        + service is not None  | 
                    |
| 706 | 
                        + or self.is_param_set(self.params_by_str['--config'])  | 
                    |
| 707 | 
                        + ):  | 
                    |
| 708 | 
                        + err_msg = _msg.TranslatedString(  | 
                    |
| 709 | 
                        + _msg.ErrMsgTemplate.PARAMS_NEEDS_SERVICE_OR_CONFIG,  | 
                    |
| 710 | 
                        + param=param.opts[0],  | 
                    |
| 711 | 
                        + service_metavar=service_metavar,  | 
                    |
| 712 | 
                        + )  | 
                    |
| 713 | 
                        + raise click.UsageError(str(err_msg))  | 
                    |
| 714 | 
                        + sv_options = [  | 
                    |
| 715 | 
                        + self.params_by_str['--notes'],  | 
                    |
| 716 | 
                        + self.params_by_str['--delete'],  | 
                    |
| 717 | 
                        + ]  | 
                    |
| 718 | 
                        + for param in sv_options:  | 
                    |
| 719 | 
                        + if self.is_param_set(param) and not service is not None:  | 
                    |
| 720 | 
                        + err_msg = _msg.TranslatedString(  | 
                    |
| 721 | 
                        + _msg.ErrMsgTemplate.PARAMS_NEEDS_SERVICE,  | 
                    |
| 722 | 
                        + param=param.opts[0],  | 
                    |
| 723 | 
                        + service_metavar=service_metavar,  | 
                    |
| 724 | 
                        + )  | 
                    |
| 725 | 
                        + raise click.UsageError(str(err_msg))  | 
                    |
| 726 | 
                        + no_sv_options = [  | 
                    |
| 727 | 
                        + self.params_by_str['--delete-globals'],  | 
                    |
| 728 | 
                        + self.params_by_str['--clear'],  | 
                    |
| 729 | 
                        + *self.options_in_group[cli_machinery.StorageManagementOption],  | 
                    |
| 730 | 
                        + ]  | 
                    |
| 731 | 
                        + for param in no_sv_options:  | 
                    |
| 732 | 
                        + if self.is_param_set(param) and service is not None:  | 
                    |
| 733 | 
                        + err_msg = _msg.TranslatedString(  | 
                    |
| 734 | 
                        + _msg.ErrMsgTemplate.PARAMS_NO_SERVICE,  | 
                    |
| 735 | 
                        + param=param.opts[0],  | 
                    |
| 736 | 
                        + service_metavar=service_metavar,  | 
                    |
| 737 | 
                        + )  | 
                    |
| 738 | 
                        + raise click.UsageError(str(err_msg))  | 
                    |
| 739 | 
                        +  | 
                    |
| 740 | 
                        + def get_mutex(self, op: str) -> contextlib.AbstractContextManager[None]:  | 
                    |
| 741 | 
                        + """Return a mutex for accessing the configuration on disk.  | 
                    |
| 742 | 
                        +  | 
                    |
| 743 | 
                        + The mutex is a context manager, and will lock out other threads  | 
                    |
| 744 | 
                        + and processes attempting to access the configuration in an  | 
                    |
| 745 | 
                        + incompatible manner.  | 
                    |
| 746 | 
                        +  | 
                    |
| 747 | 
                        + Returns:  | 
                    |
| 748 | 
                        + If the requested operation is a read-only operation, return  | 
                    |
| 749 | 
                        + a no-op mutex. (Concurrent reads are always allowed, even  | 
                    |
| 750 | 
                        + in the presence of writers.) Otherwise, for read-write  | 
                    |
| 751 | 
                        + operations, returns the result from  | 
                    |
| 752 | 
                        + [`cli_helpers.configuration_mutex`][].  | 
                    |
| 753 | 
                        +  | 
                    |
| 754 | 
                        + """  | 
                    |
| 755 | 
                        + return (  | 
                    |
| 756 | 
                        + cli_helpers.configuration_mutex()  | 
                    |
| 757 | 
                        + if op in self.readwrite_ops  | 
                    |
| 758 | 
                        + else contextlib.nullcontext()  | 
                    |
| 759 | 
                        + )  | 
                    |
| 760 | 
                        +  | 
                    |
| 761 | 
                        + def dispatch_op(self) -> None:  | 
                    |
| 762 | 
                        + """Dispatch to the handler matching the command-line call.  | 
                    |
| 763 | 
                        +  | 
                    |
| 764 | 
                        + Also issue any appropriate warnings about the command-line,  | 
                    |
| 765 | 
                        + e.g., incompatibilities with vault(1) or ineffective options.  | 
                    |
| 766 | 
                        +  | 
                    |
| 767 | 
                        + """  | 
                    |
| 492 | 768 | 
                        service_metavar = _msg.TranslatedString(  | 
                    
| 493 | 769 | 
                        _msg.Label.VAULT_METAVAR_SERVICE  | 
                    
| 770 | 
                        + )  | 
                    |
| 771 | 
                        + if self.ctx.params['service'] == '': # noqa: PLC1901  | 
                    |
| 772 | 
                        + self.warning(  | 
                    |
| 773 | 
                        + _msg.TranslatedString(  | 
                    |
| 774 | 
                        + _msg.WarnMsgTemplate.EMPTY_SERVICE_NOT_SUPPORTED,  | 
                    |
| 775 | 
                        + service_metavar=service_metavar,  | 
                    |
| 494 | 776 | 
                        ),  | 
                    
| 777 | 
                        + )  | 
                    |
| 778 | 
                        +        if self.ctx.params.get('edit_notes') and not self.ctx.params.get(
                       | 
                    |
| 779 | 
                        + 'store_config_only'  | 
                    |
| 780 | 
                        + ):  | 
                    |
| 781 | 
                        + self.warning(  | 
                    |
| 782 | 
                        + _msg.TranslatedString(  | 
                    |
| 783 | 
                        + _msg.WarnMsgTemplate.EDITING_NOTES_BUT_NOT_STORING_CONFIG,  | 
                    |
| 784 | 
                        + service_metavar=service_metavar,  | 
                    |
| 495 | 785 | 
                        ),  | 
                    
| 496 | 
                        - cls=cli_machinery.ConfigurationOption,  | 
                    |
| 497 | 786 | 
                        )  | 
                    
| 498 | 
                        -@click.option(  | 
                    |
| 499 | 
                        - '-x',  | 
                    |
| 500 | 
                        - '--delete',  | 
                    |
| 501 | 
                        - 'delete_service_settings',  | 
                    |
| 502 | 
                        - is_flag=True,  | 
                    |
| 503 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 504 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_HELP_TEXT,  | 
                    |
| 787 | 
                        + op: str  | 
                    |
| 788 | 
                        + for candidate_op in self.all_ops[:-1]:  | 
                    |
| 789 | 
                        + if self.ctx.params.get(candidate_op):  | 
                    |
| 790 | 
                        + op = candidate_op  | 
                    |
| 791 | 
                        + break  | 
                    |
| 792 | 
                        + else:  | 
                    |
| 793 | 
                        + op = self.all_ops[-1]  | 
                    |
| 794 | 
                        + with self.get_mutex(op):  | 
                    |
| 795 | 
                        + op_func = getattr(self, 'run_op_' + op)  | 
                    |
| 796 | 
                        + return op_func()  | 
                    |
| 797 | 
                        +  | 
                    |
| 798 | 
                        + def run_op_delete_service_settings(self) -> None:  | 
                    |
| 799 | 
                        + """Delete settings for a specific service."""  | 
                    |
| 800 | 
                        + service = self.ctx.params['service']  | 
                    |
| 801 | 
                        + assert service is not None  | 
                    |
| 802 | 
                        + configuration = self.get_config()  | 
                    |
| 803 | 
                        + if service in configuration['services']:  | 
                    |
| 804 | 
                        + del configuration['services'][service]  | 
                    |
| 805 | 
                        + self.put_config(configuration)  | 
                    |
| 806 | 
                        +  | 
                    |
| 807 | 
                        + def run_op_delete_globals(self) -> None:  | 
                    |
| 808 | 
                        + """Delete the global settings."""  | 
                    |
| 809 | 
                        + configuration = self.get_config()  | 
                    |
| 810 | 
                        + if 'global' in configuration:  | 
                    |
| 811 | 
                        + del configuration['global']  | 
                    |
| 812 | 
                        + self.put_config(configuration)  | 
                    |
| 813 | 
                        +  | 
                    |
| 814 | 
                        + def run_op_clear_all_settings(self) -> None:  | 
                    |
| 815 | 
                        + """Clear all settings."""  | 
                    |
| 816 | 
                        +        self.put_config({'services': {}})
                       | 
                    |
| 817 | 
                        +  | 
                    |
| 818 | 
                        + def run_op_import_settings(self) -> None: # noqa: C901,PLR0912  | 
                    |
| 819 | 
                        + """Import settings from a given file.  | 
                    |
| 820 | 
                        +  | 
                    |
| 821 | 
                        + Issue multiple warnings, if appropriate, e.g. for Unicode  | 
                    |
| 822 | 
                        + normalization issues with stored passphrases or conflicting  | 
                    |
| 823 | 
                        + stored passphrases and keys. Respect the `--overwrite-config`  | 
                    |
| 824 | 
                        + and `--merge-config` options when writing the imported  | 
                    |
| 825 | 
                        + configuration to disk.  | 
                    |
| 826 | 
                        +  | 
                    |
| 827 | 
                        + """  | 
                    |
| 505 | 828 | 
                        service_metavar = _msg.TranslatedString(  | 
                    
| 506 | 829 | 
                        _msg.Label.VAULT_METAVAR_SERVICE  | 
                    
| 830 | 
                        + )  | 
                    |
| 831 | 
                        + user_config = self.get_user_config()  | 
                    |
| 832 | 
                        + import_settings = self.ctx.params['import_settings']  | 
                    |
| 833 | 
                        + overwrite_config = self.ctx.params['overwrite_config']  | 
                    |
| 834 | 
                        + try:  | 
                    |
| 835 | 
                        + # TODO(the-13th-letter): keep track of auto-close; try  | 
                    |
| 836 | 
                        + # os.dup if feasible  | 
                    |
| 837 | 
                        + infile = cast(  | 
                    |
| 838 | 
                        + 'TextIO',  | 
                    |
| 839 | 
                        + (  | 
                    |
| 840 | 
                        + import_settings  | 
                    |
| 841 | 
                        + if hasattr(import_settings, 'close')  | 
                    |
| 842 | 
                        + else click.open_file(os.fspath(import_settings), 'rt')  | 
                    |
| 507 | 843 | 
                        ),  | 
                    
| 844 | 
                        + )  | 
                    |
| 845 | 
                        + # Don't specifically catch TypeError or ValueError here if  | 
                    |
| 846 | 
                        + # the passed-in fileobj is not a readable text stream. This  | 
                    |
| 847 | 
                        + # will never happen on the command-line (thanks to `click`),  | 
                    |
| 848 | 
                        + # and for programmatic use, our caller may want accurate  | 
                    |
| 849 | 
                        + # error information.  | 
                    |
| 850 | 
                        + with infile:  | 
                    |
| 851 | 
                        + maybe_config = json.load(infile)  | 
                    |
| 852 | 
                        + except json.JSONDecodeError as exc:  | 
                    |
| 853 | 
                        + self.err(  | 
                    |
| 854 | 
                        + _msg.TranslatedString(  | 
                    |
| 855 | 
                        + _msg.ErrMsgTemplate.CANNOT_DECODEIMPORT_VAULT_SETTINGS,  | 
                    |
| 856 | 
                        + error=exc,  | 
                    |
| 857 | 
                        + )  | 
                    |
| 858 | 
                        + )  | 
                    |
| 859 | 
                        + except OSError as exc:  | 
                    |
| 860 | 
                        + self.err(  | 
                    |
| 861 | 
                        + _msg.TranslatedString(  | 
                    |
| 862 | 
                        + _msg.ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS,  | 
                    |
| 863 | 
                        + error=exc.strerror,  | 
                    |
| 864 | 
                        + filename=exc.filename,  | 
                    |
| 865 | 
                        + ).maybe_without_filename()  | 
                    |
| 866 | 
                        + )  | 
                    |
| 867 | 
                        + cleaned = _types.clean_up_falsy_vault_config_values(maybe_config)  | 
                    |
| 868 | 
                        + if not _types.is_vault_config(maybe_config):  | 
                    |
| 869 | 
                        + self.err(  | 
                    |
| 870 | 
                        + _msg.TranslatedString(  | 
                    |
| 871 | 
                        + _msg.ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS,  | 
                    |
| 872 | 
                        + error=_msg.TranslatedString(  | 
                    |
| 873 | 
                        + _msg.ErrMsgTemplate.INVALID_VAULT_CONFIG,  | 
                    |
| 874 | 
                        + config=maybe_config,  | 
                    |
| 508 | 875 | 
                        ),  | 
                    
| 509 | 
                        - cls=cli_machinery.ConfigurationOption,  | 
                    |
| 876 | 
                        + filename=None,  | 
                    |
| 877 | 
                        + ).maybe_without_filename()  | 
                    |
| 510 | 878 | 
                        )  | 
                    
| 511 | 
                        -@click.option(  | 
                    |
| 512 | 
                        - '--delete-globals',  | 
                    |
| 513 | 
                        - is_flag=True,  | 
                    |
| 514 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 515 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_GLOBALS_HELP_TEXT,  | 
                    |
| 879 | 
                        + assert cleaned is not None  | 
                    |
| 880 | 
                        + for step in cleaned:  | 
                    |
| 881 | 
                        + # These are never fatal errors, because the semantics of  | 
                    |
| 882 | 
                        + # vault upon encountering these settings are ill-specified,  | 
                    |
| 883 | 
                        + # but not ill-defined.  | 
                    |
| 884 | 
                        + if step.action == 'replace':  | 
                    |
| 885 | 
                        + self.warning(  | 
                    |
| 886 | 
                        + _msg.TranslatedString(  | 
                    |
| 887 | 
                        + _msg.WarnMsgTemplate.STEP_REPLACE_INVALID_VALUE,  | 
                    |
| 888 | 
                        + old=json.dumps(step.old_value),  | 
                    |
| 889 | 
                        + path=_types.json_path(step.path),  | 
                    |
| 890 | 
                        + new=json.dumps(step.new_value),  | 
                    |
| 516 | 891 | 
                        ),  | 
                    
| 517 | 
                        - cls=cli_machinery.ConfigurationOption,  | 
                    |
| 518 | 892 | 
                        )  | 
                    
| 519 | 
                        -@click.option(  | 
                    |
| 520 | 
                        - '-X',  | 
                    |
| 521 | 
                        - '--clear',  | 
                    |
| 522 | 
                        - 'clear_all_settings',  | 
                    |
| 523 | 
                        - is_flag=True,  | 
                    |
| 524 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 525 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_ALL_HELP_TEXT,  | 
                    |
| 893 | 
                        + else:  | 
                    |
| 894 | 
                        + self.warning(  | 
                    |
| 895 | 
                        + _msg.TranslatedString(  | 
                    |
| 896 | 
                        + _msg.WarnMsgTemplate.STEP_REMOVE_INEFFECTIVE_VALUE,  | 
                    |
| 897 | 
                        + path=_types.json_path(step.path),  | 
                    |
| 898 | 
                        + old=json.dumps(step.old_value),  | 
                    |
| 526 | 899 | 
                        ),  | 
                    
| 527 | 
                        - cls=cli_machinery.ConfigurationOption,  | 
                    |
| 528 | 900 | 
                        )  | 
                    
| 529 | 
                        -@click.option(  | 
                    |
| 530 | 
                        - '-e',  | 
                    |
| 531 | 
                        - '--export',  | 
                    |
| 532 | 
                        - 'export_settings',  | 
                    |
| 533 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 534 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 535 | 
                        - ),  | 
                    |
| 536 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 537 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_EXPORT_HELP_TEXT,  | 
                    |
| 538 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 539 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 901 | 
                        + if '' in maybe_config['services']:  | 
                    |
| 902 | 
                        + self.warning(  | 
                    |
| 903 | 
                        + _msg.TranslatedString(  | 
                    |
| 904 | 
                        + _msg.WarnMsgTemplate.EMPTY_SERVICE_SETTINGS_INACCESSIBLE,  | 
                    |
| 905 | 
                        + service_metavar=service_metavar,  | 
                    |
| 906 | 
                        + PROG_NAME=PROG_NAME,  | 
                    |
| 540 | 907 | 
                        ),  | 
                    
| 908 | 
                        + )  | 
                    |
| 909 | 
                        + for service_name in sorted(maybe_config['services'].keys()):  | 
                    |
| 910 | 
                        + if not cli_helpers.is_completable_item(service_name):  | 
                    |
| 911 | 
                        + self.warning(  | 
                    |
| 912 | 
                        + _msg.TranslatedString(  | 
                    |
| 913 | 
                        + _msg.WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE,  | 
                    |
| 914 | 
                        + service=service_name,  | 
                    |
| 541 | 915 | 
                        ),  | 
                    
| 542 | 
                        - cls=cli_machinery.StorageManagementOption,  | 
                    |
| 543 | 
                        - shell_complete=cli_helpers.shell_complete_path,  | 
                    |
| 544 | 916 | 
                        )  | 
                    
| 545 | 
                        -@click.option(  | 
                    |
| 546 | 
                        - '-i',  | 
                    |
| 547 | 
                        - '--import',  | 
                    |
| 548 | 
                        - 'import_settings',  | 
                    |
| 549 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 550 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 917 | 
                        + try:  | 
                    |
| 918 | 
                        + cli_helpers.check_for_misleading_passphrase(  | 
                    |
| 919 | 
                        +                ('global',),
                       | 
                    |
| 920 | 
                        +                cast('dict[str, Any]', maybe_config.get('global', {})),
                       | 
                    |
| 921 | 
                        + main_config=user_config,  | 
                    |
| 922 | 
                        + ctx=self.ctx,  | 
                    |
| 923 | 
                        + )  | 
                    |
| 924 | 
                        + for key, value in maybe_config['services'].items():  | 
                    |
| 925 | 
                        + cli_helpers.check_for_misleading_passphrase(  | 
                    |
| 926 | 
                        +                    ('services', key),
                       | 
                    |
| 927 | 
                        +                    cast('dict[str, Any]', value),
                       | 
                    |
| 928 | 
                        + main_config=user_config,  | 
                    |
| 929 | 
                        + ctx=self.ctx,  | 
                    |
| 930 | 
                        + )  | 
                    |
| 931 | 
                        + except AssertionError as exc:  | 
                    |
| 932 | 
                        + self.err(  | 
                    |
| 933 | 
                        + _msg.TranslatedString(  | 
                    |
| 934 | 
                        + _msg.ErrMsgTemplate.INVALID_USER_CONFIG,  | 
                    |
| 935 | 
                        + error=exc,  | 
                    |
| 936 | 
                        + filename=None,  | 
                    |
| 937 | 
                        + ).maybe_without_filename(),  | 
                    |
| 938 | 
                        + )  | 
                    |
| 939 | 
                        +        global_obj = maybe_config.get('global', {})
                       | 
                    |
| 940 | 
                        +        has_key = _types.js_truthiness(global_obj.get('key'))
                       | 
                    |
| 941 | 
                        +        has_phrase = _types.js_truthiness(global_obj.get('phrase'))
                       | 
                    |
| 942 | 
                        + if has_key and has_phrase:  | 
                    |
| 943 | 
                        + self.warning(  | 
                    |
| 944 | 
                        + _msg.TranslatedString(  | 
                    |
| 945 | 
                        + _msg.WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE,  | 
                    |
| 551 | 946 | 
                        ),  | 
                    
| 552 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 553 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_IMPORT_HELP_TEXT,  | 
                    |
| 554 | 
                        - metavar=_msg.TranslatedString(  | 
                    |
| 555 | 
                        - _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 947 | 
                        + )  | 
                    |
| 948 | 
                        + for service_name, service_obj in maybe_config['services'].items():  | 
                    |
| 949 | 
                        + has_key = _types.js_truthiness(  | 
                    |
| 950 | 
                        +                service_obj.get('key')
                       | 
                    |
| 951 | 
                        +            ) or _types.js_truthiness(global_obj.get('key'))
                       | 
                    |
| 952 | 
                        + has_phrase = _types.js_truthiness(  | 
                    |
| 953 | 
                        +                service_obj.get('phrase')
                       | 
                    |
| 954 | 
                        +            ) or _types.js_truthiness(global_obj.get('phrase'))
                       | 
                    |
| 955 | 
                        + if has_key and has_phrase:  | 
                    |
| 956 | 
                        + self.warning(  | 
                    |
| 957 | 
                        + _msg.TranslatedString(  | 
                    |
| 958 | 
                        + _msg.WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE,  | 
                    |
| 959 | 
                        + service=json.dumps(service_name),  | 
                    |
| 556 | 960 | 
                        ),  | 
                    
| 961 | 
                        + )  | 
                    |
| 962 | 
                        + if overwrite_config:  | 
                    |
| 963 | 
                        + self.put_config(maybe_config)  | 
                    |
| 964 | 
                        + else:  | 
                    |
| 965 | 
                        + configuration = self.get_config()  | 
                    |
| 966 | 
                        + merged_config: collections.ChainMap[str, Any] = (  | 
                    |
| 967 | 
                        + collections.ChainMap(  | 
                    |
| 968 | 
                        +                    {
                       | 
                    |
| 969 | 
                        + 'services': collections.ChainMap(  | 
                    |
| 970 | 
                        + maybe_config['services'],  | 
                    |
| 971 | 
                        + configuration['services'],  | 
                    |
| 557 | 972 | 
                        ),  | 
                    
| 558 | 
                        - cls=cli_machinery.StorageManagementOption,  | 
                    |
| 559 | 
                        - shell_complete=cli_helpers.shell_complete_path,  | 
                    |
| 973 | 
                        + },  | 
                    |
| 974 | 
                        +                    {'global': maybe_config['global']}
                       | 
                    |
| 975 | 
                        + if 'global' in maybe_config  | 
                    |
| 976 | 
                        +                    else {},
                       | 
                    |
| 977 | 
                        +                    {'global': configuration['global']}
                       | 
                    |
| 978 | 
                        + if 'global' in configuration  | 
                    |
| 979 | 
                        +                    else {},
                       | 
                    |
| 560 | 980 | 
                        )  | 
                    
| 561 | 
                        -@click.option(  | 
                    |
| 562 | 
                        - '--overwrite-existing/--merge-existing',  | 
                    |
| 563 | 
                        - 'overwrite_config',  | 
                    |
| 564 | 
                        - default=False,  | 
                    |
| 565 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 566 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_OVERWRITE_HELP_TEXT  | 
                    |
| 981 | 
                        + )  | 
                    |
| 982 | 
                        +            new_config: Any = {
                       | 
                    |
| 983 | 
                        + k: dict(v) if isinstance(v, collections.ChainMap) else v  | 
                    |
| 984 | 
                        + for k, v in sorted(merged_config.items())  | 
                    |
| 985 | 
                        + }  | 
                    |
| 986 | 
                        + assert _types.is_vault_config(new_config)  | 
                    |
| 987 | 
                        + self.put_config(new_config)  | 
                    |
| 988 | 
                        +  | 
                    |
| 989 | 
                        + def run_op_export_settings(self) -> None:  | 
                    |
| 990 | 
                        + """Export settings to a given file.  | 
                    |
| 991 | 
                        +  | 
                    |
| 992 | 
                        + Respect the `--export-as` option when writing the exported  | 
                    |
| 993 | 
                        + configuration to the file.  | 
                    |
| 994 | 
                        +  | 
                    |
| 995 | 
                        + """  | 
                    |
| 996 | 
                        + export_settings = self.ctx.params['export_settings']  | 
                    |
| 997 | 
                        + export_as = self.ctx.params['export_as']  | 
                    |
| 998 | 
                        + configuration = self.get_config()  | 
                    |
| 999 | 
                        + try:  | 
                    |
| 1000 | 
                        + # TODO(the-13th-letter): keep track of auto-close; try  | 
                    |
| 1001 | 
                        + # os.dup if feasible  | 
                    |
| 1002 | 
                        + outfile = cast(  | 
                    |
| 1003 | 
                        + 'TextIO',  | 
                    |
| 1004 | 
                        + (  | 
                    |
| 1005 | 
                        + export_settings  | 
                    |
| 1006 | 
                        + if hasattr(export_settings, 'close')  | 
                    |
| 1007 | 
                        + else click.open_file(os.fspath(export_settings), 'wt')  | 
                    |
| 567 | 1008 | 
                        ),  | 
                    
| 568 | 
                        - cls=cli_machinery.CompatibilityOption,  | 
                    |
| 569 | 1009 | 
                        )  | 
                    
| 570 | 
                        -@click.option(  | 
                    |
| 571 | 
                        - '--unset',  | 
                    |
| 572 | 
                        - 'unset_settings',  | 
                    |
| 573 | 
                        - multiple=True,  | 
                    |
| 574 | 
                        - type=click.Choice([  | 
                    |
| 575 | 
                        - 'phrase',  | 
                    |
| 576 | 
                        - 'key',  | 
                    |
| 1010 | 
                        + # Don't specifically catch TypeError or ValueError here if  | 
                    |
| 1011 | 
                        + # the passed-in fileobj is not a writable text stream. This  | 
                    |
| 1012 | 
                        + # will never happen on the command-line (thanks to `click`),  | 
                    |
| 1013 | 
                        + # and for programmatic use, our caller may want accurate  | 
                    |
| 1014 | 
                        + # error information.  | 
                    |
| 1015 | 
                        + with outfile:  | 
                    |
| 1016 | 
                        + if export_as == 'sh':  | 
                    |
| 1017 | 
                        + this_ctx = self.ctx  | 
                    |
| 1018 | 
                        + prog_name_pieces = collections.deque([  | 
                    |
| 1019 | 
                        + this_ctx.info_name or 'vault',  | 
                    |
| 1020 | 
                        + ])  | 
                    |
| 1021 | 
                        + while (  | 
                    |
| 1022 | 
                        + this_ctx.parent is not None  | 
                    |
| 1023 | 
                        + and this_ctx.parent.info_name is not None  | 
                    |
| 1024 | 
                        + ):  | 
                    |
| 1025 | 
                        + prog_name_pieces.appendleft(this_ctx.parent.info_name)  | 
                    |
| 1026 | 
                        + this_ctx = this_ctx.parent  | 
                    |
| 1027 | 
                        + cli_helpers.print_config_as_sh_script(  | 
                    |
| 1028 | 
                        + configuration,  | 
                    |
| 1029 | 
                        + outfile=outfile,  | 
                    |
| 1030 | 
                        + prog_name_list=prog_name_pieces,  | 
                    |
| 1031 | 
                        + )  | 
                    |
| 1032 | 
                        + else:  | 
                    |
| 1033 | 
                        + json.dump(  | 
                    |
| 1034 | 
                        + configuration,  | 
                    |
| 1035 | 
                        + outfile,  | 
                    |
| 1036 | 
                        + ensure_ascii=False,  | 
                    |
| 1037 | 
                        + indent=2,  | 
                    |
| 1038 | 
                        + sort_keys=True,  | 
                    |
| 1039 | 
                        + )  | 
                    |
| 1040 | 
                        + except OSError as exc:  | 
                    |
| 1041 | 
                        + self.err(  | 
                    |
| 1042 | 
                        + _msg.TranslatedString(  | 
                    |
| 1043 | 
                        + _msg.ErrMsgTemplate.CANNOT_EXPORT_VAULT_SETTINGS,  | 
                    |
| 1044 | 
                        + error=exc.strerror,  | 
                    |
| 1045 | 
                        + filename=exc.filename,  | 
                    |
| 1046 | 
                        + ).maybe_without_filename(),  | 
                    |
| 1047 | 
                        + )  | 
                    |
| 1048 | 
                        +  | 
                    |
| 1049 | 
                        + def run_subop_query_phrase_or_key_change( # noqa: C901,PLR0912  | 
                    |
| 1050 | 
                        + self,  | 
                    |
| 1051 | 
                        + *,  | 
                    |
| 1052 | 
                        + empty_service_permitted: bool,  | 
                    |
| 1053 | 
                        + configuration: _types.VaultConfig | None = None,  | 
                    |
| 1054 | 
                        + ) -> collections.ChainMap[str, Any]:  | 
                    |
| 1055 | 
                        + """Query the master passphrase or master SSH key, if changed.  | 
                    |
| 1056 | 
                        +  | 
                    |
| 1057 | 
                        + If the user indicates they want to change the master passphrase  | 
                    |
| 1058 | 
                        + or master SSH key for this call, or if they want to configure  | 
                    |
| 1059 | 
                        + a stored global or service-specific master passphrase or master  | 
                    |
| 1060 | 
                        + SSH key, then query the user.  | 
                    |
| 1061 | 
                        +  | 
                    |
| 1062 | 
                        + This is not a complete command-line call operation in and of  | 
                    |
| 1063 | 
                        + itself.  | 
                    |
| 1064 | 
                        +  | 
                    |
| 1065 | 
                        + Args:  | 
                    |
| 1066 | 
                        + empty_service_permitted:  | 
                    |
| 1067 | 
                        + True if an empty service name is permitted, False otherwise.  | 
                    |
| 1068 | 
                        + configuration:  | 
                    |
| 1069 | 
                        + The vault configuration, parsed from disk. If not  | 
                    |
| 1070 | 
                        + given, we read the configuration from disk ourselves.  | 
                    |
| 1071 | 
                        +  | 
                    |
| 1072 | 
                        + The returned effective configuration already contains  | 
                    |
| 1073 | 
                        + a relevant slice of the vault configuration. However,  | 
                    |
| 1074 | 
                        + some callers need access to the full configuration *and*  | 
                    |
| 1075 | 
                        + need the slice within the effective configuration to  | 
                    |
| 1076 | 
                        + refer to the same object.  | 
                    |
| 1077 | 
                        +  | 
                    |
| 1078 | 
                        + Returns:  | 
                    |
| 1079 | 
                        + The effective configuration for the (possibly empty) given  | 
                    |
| 1080 | 
                        + service, as a [chained map][collections.ChainMap]. Any  | 
                    |
| 1081 | 
                        + master passphrase or master SSH key overrides that may be in  | 
                    |
| 1082 | 
                        + effect are stored in the first map.  | 
                    |
| 1083 | 
                        +  | 
                    |
| 1084 | 
                        + Raises:  | 
                    |
| 1085 | 
                        + click.UsageError:  | 
                    |
| 1086 | 
                        + The service name was empty, and an empty service name  | 
                    |
| 1087 | 
                        + was not permitted as per the method parameters.  | 
                    |
| 1088 | 
                        +  | 
                    |
| 1089 | 
                        + Warning:  | 
                    |
| 1090 | 
                        + It is the caller's responsibility to vet any interactively  | 
                    |
| 1091 | 
                        + entered master passphrase for Unicode normalization issues.  | 
                    |
| 1092 | 
                        +  | 
                    |
| 1093 | 
                        + """  | 
                    |
| 1094 | 
                        + service = self.ctx.params['service']  | 
                    |
| 1095 | 
                        + use_key = self.ctx.params['use_key']  | 
                    |
| 1096 | 
                        + use_phrase = self.ctx.params['use_phrase']  | 
                    |
| 1097 | 
                        + if configuration is None:  | 
                    |
| 1098 | 
                        + configuration = self.get_config()  | 
                    |
| 1099 | 
                        +        service_keys_on_commandline = {
                       | 
                    |
| 577 | 1100 | 
                        'length',  | 
                    
| 578 | 1101 | 
                        'repeat',  | 
                    
| 579 | 1102 | 
                        'lower',  | 
                    
| ... | ... | 
                      @@ -582,706 +1105,329 @@ def derivepassphrase_export_vault(  | 
                  
| 582 | 1105 | 
                        'space',  | 
                    
| 583 | 1106 | 
                        'dash',  | 
                    
| 584 | 1107 | 
                        'symbol',  | 
                    
| 585 | 
                        - 'notes',  | 
                    |
| 586 | 
                        - ]),  | 
                    |
| 587 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 588 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_UNSET_HELP_TEXT  | 
                    |
| 1108 | 
                        + }  | 
                    |
| 1109 | 
                        + settings: collections.ChainMap[str, Any] = collections.ChainMap(  | 
                    |
| 1110 | 
                        +            {
                       | 
                    |
| 1111 | 
                        + k: v  | 
                    |
| 1112 | 
                        + for k in service_keys_on_commandline  | 
                    |
| 1113 | 
                        + if (v := self.ctx.params.get(k)) is not None  | 
                    |
| 1114 | 
                        + },  | 
                    |
| 1115 | 
                        + cast(  | 
                    |
| 1116 | 
                        + 'dict[str, Any]',  | 
                    |
| 1117 | 
                        +                configuration['services'].get(service, {}) if service else {},
                       | 
                    |
| 589 | 1118 | 
                        ),  | 
                    
| 590 | 
                        - cls=cli_machinery.CompatibilityOption,  | 
                    |
| 1119 | 
                        +            cast('dict[str, Any]', configuration.get('global', {})),
                       | 
                    |
| 591 | 1120 | 
                        )  | 
                    
| 592 | 
                        -@click.option(  | 
                    |
| 593 | 
                        - '--export-as',  | 
                    |
| 594 | 
                        - type=click.Choice(['json', 'sh']),  | 
                    |
| 595 | 
                        - default='json',  | 
                    |
| 596 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 597 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_EXPORT_AS_HELP_TEXT  | 
                    |
| 1121 | 
                        + if not service and not empty_service_permitted:  | 
                    |
| 1122 | 
                        + err_msg = _msg.TranslatedString(  | 
                    |
| 1123 | 
                        + _msg.ErrMsgTemplate.SERVICE_REQUIRED,  | 
                    |
| 1124 | 
                        + service_metavar=_msg.TranslatedString(  | 
                    |
| 1125 | 
                        + _msg.Label.VAULT_METAVAR_SERVICE  | 
                    |
| 598 | 1126 | 
                        ),  | 
                    
| 599 | 
                        - cls=cli_machinery.CompatibilityOption,  | 
                    |
| 600 | 1127 | 
                        )  | 
                    
| 601 | 
                        -@click.option(  | 
                    |
| 602 | 
                        - '--modern-editor-interface/--vault-legacy-editor-interface',  | 
                    |
| 603 | 
                        - 'modern_editor_interface',  | 
                    |
| 604 | 
                        - default=False,  | 
                    |
| 605 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 606 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT  | 
                    |
| 1128 | 
                        + raise click.UsageError(str(err_msg))  | 
                    |
| 1129 | 
                        + if use_key:  | 
                    |
| 1130 | 
                        + try:  | 
                    |
| 1131 | 
                        + settings.maps[0]['key'] = base64.standard_b64encode(  | 
                    |
| 1132 | 
                        + cli_helpers.select_ssh_key(ctx=self.ctx)  | 
                    |
| 1133 | 
                        +                ).decode('ASCII')
                       | 
                    |
| 1134 | 
                        + except IndexError:  | 
                    |
| 1135 | 
                        + self.err(  | 
                    |
| 1136 | 
                        + _msg.TranslatedString(  | 
                    |
| 1137 | 
                        + _msg.ErrMsgTemplate.USER_ABORTED_SSH_KEY_SELECTION  | 
                    |
| 607 | 1138 | 
                        ),  | 
                    
| 608 | 
                        - cls=cli_machinery.CompatibilityOption,  | 
                    |
| 609 | 1139 | 
                        )  | 
                    
| 610 | 
                        -@click.option(  | 
                    |
| 611 | 
                        - '--print-notes-before/--print-notes-after',  | 
                    |
| 612 | 
                        - 'print_notes_before',  | 
                    |
| 613 | 
                        - default=False,  | 
                    |
| 614 | 
                        - help=_msg.TranslatedString(  | 
                    |
| 615 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_PRINT_NOTES_BEFORE_HELP_TEXT  | 
                    |
| 1140 | 
                        + except KeyError:  | 
                    |
| 1141 | 
                        + self.err(  | 
                    |
| 1142 | 
                        + _msg.TranslatedString(  | 
                    |
| 1143 | 
                        + _msg.ErrMsgTemplate.NO_SSH_AGENT_FOUND  | 
                    |
| 616 | 1144 | 
                        ),  | 
                    
| 617 | 
                        - cls=cli_machinery.CompatibilityOption,  | 
                    |
| 618 | 1145 | 
                        )  | 
                    
| 619 | 
                        -@cli_machinery.version_option(cli_machinery.vault_version_option_callback)  | 
                    |
| 620 | 
                        -@cli_machinery.color_forcing_pseudo_option  | 
                    |
| 621 | 
                        -@cli_machinery.standard_logging_options  | 
                    |
| 622 | 
                        -@click.argument(  | 
                    |
| 623 | 
                        - 'service',  | 
                    |
| 624 | 
                        - metavar=_msg.TranslatedString(_msg.Label.VAULT_METAVAR_SERVICE),  | 
                    |
| 625 | 
                        - required=False,  | 
                    |
| 626 | 
                        - default=None,  | 
                    |
| 627 | 
                        - shell_complete=cli_helpers.shell_complete_service,  | 
                    |
| 1146 | 
                        + except LookupError:  | 
                    |
| 1147 | 
                        + self.err(  | 
                    |
| 1148 | 
                        + _msg.TranslatedString(  | 
                    |
| 1149 | 
                        + _msg.ErrMsgTemplate.NO_SUITABLE_SSH_KEYS,  | 
                    |
| 1150 | 
                        + PROG_NAME=PROG_NAME,  | 
                    |
| 628 | 1151 | 
                        )  | 
                    
| 629 | 
                        -@click.pass_context  | 
                    |
| 630 | 
                        -def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915  | 
                    |
| 631 | 
                        - ctx: click.Context,  | 
                    |
| 632 | 
                        - /,  | 
                    |
| 633 | 
                        - *,  | 
                    |
| 634 | 
                        - service: str | None = None,  | 
                    |
| 635 | 
                        - use_phrase: bool = False,  | 
                    |
| 636 | 
                        - use_key: bool = False,  | 
                    |
| 637 | 
                        - length: int | None = None,  | 
                    |
| 638 | 
                        - repeat: int | None = None,  | 
                    |
| 639 | 
                        - lower: int | None = None,  | 
                    |
| 640 | 
                        - upper: int | None = None,  | 
                    |
| 641 | 
                        - number: int | None = None,  | 
                    |
| 642 | 
                        - space: int | None = None,  | 
                    |
| 643 | 
                        - dash: int | None = None,  | 
                    |
| 644 | 
                        - symbol: int | None = None,  | 
                    |
| 645 | 
                        - edit_notes: bool = False,  | 
                    |
| 646 | 
                        - store_config_only: bool = False,  | 
                    |
| 647 | 
                        - delete_service_settings: bool = False,  | 
                    |
| 648 | 
                        - delete_globals: bool = False,  | 
                    |
| 649 | 
                        - clear_all_settings: bool = False,  | 
                    |
| 650 | 
                        - export_settings: TextIO | os.PathLike[str] | None = None,  | 
                    |
| 651 | 
                        - import_settings: TextIO | os.PathLike[str] | None = None,  | 
                    |
| 652 | 
                        - overwrite_config: bool = False,  | 
                    |
| 653 | 
                        - unset_settings: Sequence[str] = (),  | 
                    |
| 654 | 
                        - export_as: Literal['json', 'sh'] = 'json',  | 
                    |
| 655 | 
                        - modern_editor_interface: bool = False,  | 
                    |
| 656 | 
                        - print_notes_before: bool = False,  | 
                    |
| 657 | 
                        -) -> None:  | 
                    |
| 658 | 
                        - """Derive a passphrase using the vault(1) derivation scheme.  | 
                    |
| 1152 | 
                        + )  | 
                    |
| 1153 | 
                        + except NotImplementedError:  | 
                    |
| 1154 | 
                        + self.err(_msg.TranslatedString(_msg.ErrMsgTemplate.NO_AF_UNIX))  | 
                    |
| 1155 | 
                        + except OSError as exc:  | 
                    |
| 1156 | 
                        + self.err(  | 
                    |
| 1157 | 
                        + _msg.TranslatedString(  | 
                    |
| 1158 | 
                        + _msg.ErrMsgTemplate.CANNOT_CONNECT_TO_AGENT,  | 
                    |
| 1159 | 
                        + error=exc.strerror,  | 
                    |
| 1160 | 
                        + filename=exc.filename,  | 
                    |
| 1161 | 
                        + ).maybe_without_filename(),  | 
                    |
| 1162 | 
                        + )  | 
                    |
| 1163 | 
                        + except ssh_agent.SSHAgentFailedError as exc:  | 
                    |
| 1164 | 
                        + self.err(  | 
                    |
| 1165 | 
                        + _msg.TranslatedString(  | 
                    |
| 1166 | 
                        + _msg.ErrMsgTemplate.AGENT_REFUSED_LIST_KEYS  | 
                    |
| 1167 | 
                        + ),  | 
                    |
| 1168 | 
                        + exc_info=exc,  | 
                    |
| 1169 | 
                        + )  | 
                    |
| 1170 | 
                        + except RuntimeError as exc:  | 
                    |
| 1171 | 
                        + self.err(  | 
                    |
| 1172 | 
                        + _msg.TranslatedString(  | 
                    |
| 1173 | 
                        + _msg.ErrMsgTemplate.CANNOT_UNDERSTAND_AGENT  | 
                    |
| 1174 | 
                        + ),  | 
                    |
| 1175 | 
                        + exc_info=exc,  | 
                    |
| 1176 | 
                        + )  | 
                    |
| 1177 | 
                        + elif use_phrase:  | 
                    |
| 1178 | 
                        + maybe_phrase = cli_helpers.prompt_for_passphrase()  | 
                    |
| 1179 | 
                        + if not maybe_phrase:  | 
                    |
| 1180 | 
                        + self.err(  | 
                    |
| 1181 | 
                        + _msg.TranslatedString(  | 
                    |
| 1182 | 
                        + _msg.ErrMsgTemplate.USER_ABORTED_PASSPHRASE  | 
                    |
| 1183 | 
                        + )  | 
                    |
| 1184 | 
                        + )  | 
                    |
| 1185 | 
                        + else:  | 
                    |
| 1186 | 
                        + settings.maps[0]['phrase'] = maybe_phrase  | 
                    |
| 1187 | 
                        + return settings  | 
                    |
| 659 | 1188 | 
                         | 
                    
| 660 | 
                        - This is a [`click`][CLICK]-powered command-line interface function,  | 
                    |
| 661 | 
                        - and not intended for programmatic use. See the  | 
                    |
| 662 | 
                        - derivepassphrase-vault(1) manpage for full documentation of the  | 
                    |
| 663 | 
                        - interface. (See also [`click.testing.CliRunner`][] for controlled,  | 
                    |
| 664 | 
                        - programmatic invocation.)  | 
                    |
| 665 | 
                        -  | 
                    |
| 666 | 
                        - [CLICK]: https://pypi.org/package/click/  | 
                    |
| 1189 | 
                        + def run_op_store_config_only(self) -> None: # noqa: C901,PLR0912,PLR0914,PLR0915  | 
                    |
| 1190 | 
                        + """Update the stored vault configuration.  | 
                    |
| 667 | 1191 | 
                         | 
                    
| 668 | 
                        - Parameters:  | 
                    |
| 669 | 
                        - ctx (click.Context):  | 
                    |
| 670 | 
                        - The `click` context.  | 
                    |
| 1192 | 
                        + Depending on the presence or the absence of a service name,  | 
                    |
| 1193 | 
                        + update either the service-specific settings, or the global  | 
                    |
| 1194 | 
                        + settings. (An empty service name is respected, i.e., updates  | 
                    |
| 1195 | 
                        + the former.)  | 
                    |
| 671 | 1196 | 
                         | 
                    
| 672 | 
                        - Other Parameters:  | 
                    |
| 673 | 
                        - service:  | 
                    |
| 674 | 
                        - A service name. Required, unless operating on global  | 
                    |
| 675 | 
                        - settings or importing/exporting settings.  | 
                    |
| 676 | 
                        - use_phrase:  | 
                    |
| 677 | 
                        - Command-line argument `-p`/`--phrase`. If given, query the  | 
                    |
| 678 | 
                        - user for a passphrase instead of an SSH key.  | 
                    |
| 679 | 
                        - use_key:  | 
                    |
| 680 | 
                        - Command-line argument `-k`/`--key`. If given, query the  | 
                    |
| 681 | 
                        - user for an SSH key instead of a passphrase.  | 
                    |
| 682 | 
                        - length:  | 
                    |
| 683 | 
                        - Command-line argument `-l`/`--length`. Override the default  | 
                    |
| 684 | 
                        - length of the generated passphrase.  | 
                    |
| 685 | 
                        - repeat:  | 
                    |
| 686 | 
                        - Command-line argument `-r`/`--repeat`. Override the default  | 
                    |
| 687 | 
                        - repetition limit if positive, or disable the repetition  | 
                    |
| 688 | 
                        - limit if 0.  | 
                    |
| 689 | 
                        - lower:  | 
                    |
| 690 | 
                        - Command-line argument `--lower`. Require a given amount of  | 
                    |
| 691 | 
                        - ASCII lowercase characters if positive, else forbid ASCII  | 
                    |
| 692 | 
                        - lowercase characters if 0.  | 
                    |
| 693 | 
                        - upper:  | 
                    |
| 694 | 
                        - Command-line argument `--upper`. Same as `lower`, but for  | 
                    |
| 695 | 
                        - ASCII uppercase characters.  | 
                    |
| 696 | 
                        - number:  | 
                    |
| 697 | 
                        - Command-line argument `--number`. Same as `lower`, but for  | 
                    |
| 698 | 
                        - ASCII digits.  | 
                    |
| 699 | 
                        - space:  | 
                    |
| 700 | 
                        - Command-line argument `--space`. Same as `lower`, but for  | 
                    |
| 701 | 
                        - the space character.  | 
                    |
| 702 | 
                        - dash:  | 
                    |
| 703 | 
                        - Command-line argument `--dash`. Same as `lower`, but for  | 
                    |
| 704 | 
                        - the hyphen-minus and underscore characters.  | 
                    |
| 705 | 
                        - symbol:  | 
                    |
| 706 | 
                        - Command-line argument `--symbol`. Same as `lower`, but for  | 
                    |
| 707 | 
                        - all other ASCII printable characters except lowercase  | 
                    |
| 708 | 
                        - characters, uppercase characters, digits, space and  | 
                    |
| 709 | 
                        - backquote.  | 
                    |
| 710 | 
                        - edit_notes:  | 
                    |
| 711 | 
                        - Command-line argument `-n`/`--notes`. If given, spawn an  | 
                    |
| 712 | 
                        - editor to edit notes for `service`.  | 
                    |
| 713 | 
                        - store_config_only:  | 
                    |
| 714 | 
                        - Command-line argument `-c`/`--config`. If given, saves the  | 
                    |
| 715 | 
                        - other given settings (`--key`, ..., `--symbol`) to the  | 
                    |
| 716 | 
                        - configuration file, either specifically for `service` or as  | 
                    |
| 717 | 
                        - global settings.  | 
                    |
| 718 | 
                        - delete_service_settings:  | 
                    |
| 719 | 
                        - Command-line argument `-x`/`--delete`. If given, removes  | 
                    |
| 720 | 
                        - the settings for `service` from the configuration file.  | 
                    |
| 721 | 
                        - delete_globals:  | 
                    |
| 722 | 
                        - Command-line argument `--delete-globals`. If given, removes  | 
                    |
| 723 | 
                        - the global settings from the configuration file.  | 
                    |
| 724 | 
                        - clear_all_settings:  | 
                    |
| 725 | 
                        - Command-line argument `-X`/`--clear`. If given, removes all  | 
                    |
| 726 | 
                        - settings from the configuration file.  | 
                    |
| 727 | 
                        - export_settings:  | 
                    |
| 728 | 
                        - Command-line argument `-e`/`--export`. If a file object,  | 
                    |
| 729 | 
                        - then it must be open for writing and accept `str` inputs.  | 
                    |
| 730 | 
                        - Otherwise, a filename to open for writing. Using `-` for  | 
                    |
| 731 | 
                        - standard output is supported.  | 
                    |
| 732 | 
                        - import_settings:  | 
                    |
| 733 | 
                        - Command-line argument `-i`/`--import`. If a file object, it  | 
                    |
| 734 | 
                        - must be open for reading and yield `str` values. Otherwise,  | 
                    |
| 735 | 
                        - a filename to open for reading. Using `-` for standard  | 
                    |
| 736 | 
                        - input is supported.  | 
                    |
| 737 | 
                        - overwrite_config:  | 
                    |
| 738 | 
                        - Command-line arguments `--overwrite-existing` (True) and  | 
                    |
| 739 | 
                        - `--merge-existing` (False). Controls whether config saving  | 
                    |
| 740 | 
                        - and config importing overwrite existing configurations, or  | 
                    |
| 741 | 
                        - merge them section-wise instead.  | 
                    |
| 742 | 
                        - unset_settings:  | 
                    |
| 743 | 
                        - Command-line argument `--unset`. If given together with  | 
                    |
| 744 | 
                        - `--config`, unsets the specified settings (in addition to  | 
                    |
| 745 | 
                        - any other changes requested).  | 
                    |
| 746 | 
                        - export_as:  | 
                    |
| 747 | 
                        - Command-line argument `--export-as`. If given together with  | 
                    |
| 748 | 
                        - `--export`, selects the format to export the current  | 
                    |
| 749 | 
                        -            configuration as: JSON ("json", default) or POSIX sh ("sh").
                       | 
                    |
| 750 | 
                        - modern_editor_interface:  | 
                    |
| 751 | 
                        - Command-line arguments `--modern-editor-interface` (True)  | 
                    |
| 752 | 
                        - and `--vault-legacy-editor-interface` (False). Controls  | 
                    |
| 753 | 
                        - whether editing notes uses a modern editor interface  | 
                    |
| 754 | 
                        - (supporting comments and aborting) or a vault(1)-compatible  | 
                    |
| 755 | 
                        - legacy editor interface (WYSIWYG notes contents).  | 
                    |
| 756 | 
                        - print_notes_before:  | 
                    |
| 757 | 
                        - Command-line arguments `--print-notes-before` (True) and  | 
                    |
| 758 | 
                        - `--print-notes-after` (False). Controls whether the service  | 
                    |
| 759 | 
                        - notes (if any) are printed before the passphrase, or after.  | 
                    |
| 1197 | 
                        + Respect the `--unset=SETTING` option to unset the named  | 
                    |
| 1198 | 
                        + settings, and the `--notes` option to edit notes interactively  | 
                    |
| 1199 | 
                        + in a spawned text editor. Issue multiple warnings, if  | 
                    |
| 1200 | 
                        + appropriate, e.g. for Unicode normalization issues with stored  | 
                    |
| 1201 | 
                        + passphrases or conflicting stored passphrases and keys. Respect  | 
                    |
| 1202 | 
                        + the `--overwrite-config` and `--merge-config` options when  | 
                    |
| 1203 | 
                        + writing the imported configuration to disk, and the  | 
                    |
| 1204 | 
                        + `--modern-editor-interface` and  | 
                    |
| 1205 | 
                        + `--vault-legacy-editor-interface` options when editing notes.  | 
                    |
| 760 | 1206 | 
                         | 
                    
| 761 | 
                        - """ # noqa: DOC501  | 
                    |
| 762 | 
                        - logger = logging.getLogger(PROG_NAME)  | 
                    |
| 763 | 
                        - deprecation = logging.getLogger(PROG_NAME + '.deprecation')  | 
                    |
| 764 | 
                        - service_metavar = _msg.TranslatedString(_msg.Label.VAULT_METAVAR_SERVICE)  | 
                    |
| 765 | 
                        -    options_in_group: dict[type[click.Option], list[click.Option]] = {}
                       | 
                    |
| 766 | 
                        -    params_by_str: dict[str, click.Parameter] = {}
                       | 
                    |
| 767 | 
                        - for param in ctx.command.params:  | 
                    |
| 768 | 
                        - if isinstance(param, click.Option):  | 
                    |
| 769 | 
                        - group: type[click.Option]  | 
                    |
| 770 | 
                        - known_option_groups = [  | 
                    |
| 771 | 
                        - cli_machinery.PassphraseGenerationOption,  | 
                    |
| 772 | 
                        - cli_machinery.ConfigurationOption,  | 
                    |
| 773 | 
                        - cli_machinery.StorageManagementOption,  | 
                    |
| 774 | 
                        - cli_machinery.LoggingOption,  | 
                    |
| 775 | 
                        - cli_machinery.CompatibilityOption,  | 
                    |
| 776 | 
                        - cli_machinery.StandardOption,  | 
                    |
| 777 | 
                        - ]  | 
                    |
| 778 | 
                        - if isinstance(param, cli_machinery.OptionGroupOption):  | 
                    |
| 779 | 
                        - for class_ in known_option_groups:  | 
                    |
| 780 | 
                        - if isinstance(param, class_):  | 
                    |
| 781 | 
                        - group = class_  | 
                    |
| 782 | 
                        - break  | 
                    |
| 783 | 
                        - else: # pragma: no cover  | 
                    |
| 784 | 
                        - raise AssertionError( # noqa: TRY003  | 
                    |
| 785 | 
                        -                        f'Unknown option group for {param!r}'  # noqa: EM102
                       | 
                    |
| 786 | 
                        - )  | 
                    |
| 787 | 
                        - else:  | 
                    |
| 788 | 
                        - group = click.Option  | 
                    |
| 789 | 
                        - options_in_group.setdefault(group, []).append(param)  | 
                    |
| 790 | 
                        - params_by_str[param.human_readable_name] = param  | 
                    |
| 791 | 
                        - for name in param.opts + param.secondary_opts:  | 
                    |
| 792 | 
                        - params_by_str[name] = param  | 
                    |
| 793 | 
                        -  | 
                    |
| 794 | 
                        - @functools.cache  | 
                    |
| 795 | 
                        - def is_param_set(param: click.Parameter) -> bool:  | 
                    |
| 796 | 
                        - return bool(ctx.params.get(param.human_readable_name))  | 
                    |
| 797 | 
                        -  | 
                    |
| 798 | 
                        - def option_name(param: click.Parameter | str) -> str:  | 
                    |
| 799 | 
                        - # Annoyingly, `param.human_readable_name` contains the *function*  | 
                    |
| 800 | 
                        - # parameter name, not the list of option names. *Those* are  | 
                    |
| 801 | 
                        - # stashed in the `.opts` and `.secondary_opts` attributes, which  | 
                    |
| 802 | 
                        - # are visible in the `.to_info_dict()` output, but not otherwise  | 
                    |
| 803 | 
                        - # documented.  | 
                    |
| 804 | 
                        - param = params_by_str[param] if isinstance(param, str) else param  | 
                    |
| 805 | 
                        - names = [param.human_readable_name, *param.opts, *param.secondary_opts]  | 
                    |
| 806 | 
                        -        option_names = [n for n in names if n.startswith('--')]
                       | 
                    |
| 807 | 
                        - return min(option_names, key=len)  | 
                    |
| 1207 | 
                        + Raises:  | 
                    |
| 1208 | 
                        + click.UsageError:  | 
                    |
| 1209 | 
                        + The user requested that the same setting be both unset  | 
                    |
| 1210 | 
                        + and set.  | 
                    |
| 808 | 1211 | 
                         | 
                    
| 809 | 
                        - def check_incompatible_options(  | 
                    |
| 810 | 
                        - param1: click.Parameter | str,  | 
                    |
| 811 | 
                        - param2: click.Parameter | str,  | 
                    |
| 812 | 
                        - ) -> None:  | 
                    |
| 813 | 
                        - param1 = params_by_str[param1] if isinstance(param1, str) else param1  | 
                    |
| 814 | 
                        - param2 = params_by_str[param2] if isinstance(param2, str) else param2  | 
                    |
| 815 | 
                        - if param1 == param2:  | 
                    |
| 816 | 
                        - return  | 
                    |
| 817 | 
                        - if not is_param_set(param1):  | 
                    |
| 818 | 
                        - return  | 
                    |
| 819 | 
                        - if is_param_set(param2):  | 
                    |
| 820 | 
                        - param1_str = option_name(param1)  | 
                    |
| 821 | 
                        - param2_str = option_name(param2)  | 
                    |
| 822 | 
                        - raise click.BadOptionUsage(  | 
                    |
| 823 | 
                        - param1_str,  | 
                    |
| 824 | 
                        - str(  | 
                    |
| 825 | 
                        - _msg.TranslatedString(  | 
                    |
| 826 | 
                        - _msg.ErrMsgTemplate.PARAMS_MUTUALLY_EXCLUSIVE,  | 
                    |
| 827 | 
                        - param1=param1_str,  | 
                    |
| 828 | 
                        - param2=param2_str,  | 
                    |
| 829 | 
                        - )  | 
                    |
| 830 | 
                        - ),  | 
                    |
| 831 | 
                        - ctx=ctx,  | 
                    |
| 1212 | 
                        + """  | 
                    |
| 1213 | 
                        + service = self.ctx.params['service']  | 
                    |
| 1214 | 
                        + use_key = self.ctx.params['use_key']  | 
                    |
| 1215 | 
                        + use_phrase = self.ctx.params['use_phrase']  | 
                    |
| 1216 | 
                        + unset_settings = self.ctx.params['unset_settings']  | 
                    |
| 1217 | 
                        + overwrite_config = self.ctx.params['overwrite_config']  | 
                    |
| 1218 | 
                        + edit_notes = self.ctx.params['edit_notes']  | 
                    |
| 1219 | 
                        + modern_editor_interface = self.ctx.params['modern_editor_interface']  | 
                    |
| 1220 | 
                        + configuration = self.get_config()  | 
                    |
| 1221 | 
                        + user_config = self.get_user_config()  | 
                    |
| 1222 | 
                        + settings = self.run_subop_query_phrase_or_key_change(  | 
                    |
| 1223 | 
                        + configuration=configuration, empty_service_permitted=True  | 
                    |
| 1224 | 
                        + )  | 
                    |
| 1225 | 
                        + overrides = settings.maps[0]  | 
                    |
| 1226 | 
                        + view: collections.ChainMap[str, Any]  | 
                    |
| 1227 | 
                        + view = (  | 
                    |
| 1228 | 
                        + collections.ChainMap(*settings.maps[:2])  | 
                    |
| 1229 | 
                        + if service  | 
                    |
| 1230 | 
                        + else collections.ChainMap(settings.maps[0], settings.maps[2])  | 
                    |
| 832 | 1231 | 
                        )  | 
                    
| 833 | 
                        - return  | 
                    |
| 834 | 
                        -  | 
                    |
| 835 | 
                        - def err(msg: Any, /, **kwargs: Any) -> NoReturn: # noqa: ANN401  | 
                    |
| 836 | 
                        -        stacklevel = kwargs.pop('stacklevel', 1)
                       | 
                    |
| 837 | 
                        - stacklevel += 1  | 
                    |
| 838 | 
                        -        extra = kwargs.pop('extra', {})
                       | 
                    |
| 839 | 
                        -        extra.setdefault('color', ctx.color)
                       | 
                    |
| 840 | 
                        - logger.error(msg, stacklevel=stacklevel, extra=extra, **kwargs)  | 
                    |
| 841 | 
                        - ctx.exit(1)  | 
                    |
| 842 | 
                        -  | 
                    |
| 843 | 
                        - def get_config() -> _types.VaultConfig:  | 
                    |
| 844 | 
                        - try:  | 
                    |
| 845 | 
                        - return cli_helpers.load_config()  | 
                    |
| 846 | 
                        - except FileNotFoundError:  | 
                    |
| 1232 | 
                        + if use_key:  | 
                    |
| 1233 | 
                        + view['key'] = overrides['key']  | 
                    |
| 1234 | 
                        + elif use_phrase:  | 
                    |
| 1235 | 
                        + view['phrase'] = overrides['phrase']  | 
                    |
| 847 | 1236 | 
                        try:  | 
                    
| 848 | 
                        - backup_config, exc = cli_helpers.migrate_and_load_old_config()  | 
                    |
| 849 | 
                        - except FileNotFoundError:  | 
                    |
| 850 | 
                        -                return {'services': {}}
                       | 
                    |
| 851 | 
                        - old_name = cli_helpers.config_filename(  | 
                    |
| 852 | 
                        - subsystem='old settings.json'  | 
                    |
| 853 | 
                        - ).name  | 
                    |
| 854 | 
                        - new_name = cli_helpers.config_filename(subsystem='vault').name  | 
                    |
| 855 | 
                        - deprecation.warning(  | 
                    |
| 856 | 
                        - _msg.TranslatedString(  | 
                    |
| 857 | 
                        - _msg.WarnMsgTemplate.V01_STYLE_CONFIG,  | 
                    |
| 858 | 
                        - old=old_name,  | 
                    |
| 859 | 
                        - new=new_name,  | 
                    |
| 860 | 
                        - ),  | 
                    |
| 861 | 
                        -                extra={'color': ctx.color},
                       | 
                    |
| 1237 | 
                        + cli_helpers.check_for_misleading_passphrase(  | 
                    |
| 1238 | 
                        +                    ('services', service) if service else ('global',),
                       | 
                    |
| 1239 | 
                        + overrides,  | 
                    |
| 1240 | 
                        + main_config=user_config,  | 
                    |
| 1241 | 
                        + ctx=self.ctx,  | 
                    |
| 862 | 1242 | 
                        )  | 
                    
| 863 | 
                        - if isinstance(exc, OSError):  | 
                    |
| 864 | 
                        - logger.warning(  | 
                    |
| 1243 | 
                        + except AssertionError as exc:  | 
                    |
| 1244 | 
                        + self.err(  | 
                    |
| 865 | 1245 | 
                        _msg.TranslatedString(  | 
                    
| 866 | 
                        - _msg.WarnMsgTemplate.FAILED_TO_MIGRATE_CONFIG,  | 
                    |
| 867 | 
                        - path=new_name,  | 
                    |
| 868 | 
                        - error=exc.strerror,  | 
                    |
| 869 | 
                        - filename=exc.filename,  | 
                    |
| 1246 | 
                        + _msg.ErrMsgTemplate.INVALID_USER_CONFIG,  | 
                    |
| 1247 | 
                        + error=exc,  | 
                    |
| 1248 | 
                        + filename=None,  | 
                    |
| 870 | 1249 | 
                        ).maybe_without_filename(),  | 
                    
| 871 | 
                        -                    extra={'color': ctx.color},
                       | 
                    |
| 1250 | 
                        + )  | 
                    |
| 1251 | 
                        + if 'key' in settings:  | 
                    |
| 1252 | 
                        + if service:  | 
                    |
| 1253 | 
                        + w_msg = _msg.TranslatedString(  | 
                    |
| 1254 | 
                        + _msg.WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE,  | 
                    |
| 1255 | 
                        + service=json.dumps(service),  | 
                    |
| 872 | 1256 | 
                        )  | 
                    
| 873 | 1257 | 
                        else:  | 
                    
| 874 | 
                        - deprecation.info(  | 
                    |
| 875 | 
                        - _msg.TranslatedString(  | 
                    |
| 876 | 
                        - _msg.InfoMsgTemplate.SUCCESSFULLY_MIGRATED,  | 
                    |
| 877 | 
                        - path=new_name,  | 
                    |
| 1258 | 
                        + w_msg = _msg.TranslatedString(  | 
                    |
| 1259 | 
                        + _msg.WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE  | 
                    |
| 1260 | 
                        + )  | 
                    |
| 1261 | 
                        + self.warning(w_msg)  | 
                    |
| 1262 | 
                        + if not view.maps[0] and not unset_settings and not edit_notes:  | 
                    |
| 1263 | 
                        + err_msg = _msg.TranslatedString(  | 
                    |
| 1264 | 
                        + _msg.ErrMsgTemplate.CANNOT_UPDATE_SETTINGS_NO_SETTINGS,  | 
                    |
| 1265 | 
                        + settings_type=_msg.TranslatedString(  | 
                    |
| 1266 | 
                        + _msg.Label.CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_SERVICE  | 
                    |
| 1267 | 
                        + if service  | 
                    |
| 1268 | 
                        + else _msg.Label.CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_GLOBAL # noqa: E501  | 
                    |
| 878 | 1269 | 
                        ),  | 
                    
| 879 | 
                        -                    extra={'color': ctx.color},
                       | 
                    |
| 880 | 1270 | 
                        )  | 
                    
| 881 | 
                        - return backup_config  | 
                    |
| 882 | 
                        - except OSError as exc:  | 
                    |
| 883 | 
                        - err(  | 
                    |
| 884 | 
                        - _msg.TranslatedString(  | 
                    |
| 885 | 
                        - _msg.ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS,  | 
                    |
| 886 | 
                        - error=exc.strerror,  | 
                    |
| 887 | 
                        - filename=exc.filename,  | 
                    |
| 888 | 
                        - ).maybe_without_filename(),  | 
                    |
| 1271 | 
                        + raise click.UsageError(str(err_msg))  | 
                    |
| 1272 | 
                        + for setting in unset_settings:  | 
                    |
| 1273 | 
                        + if setting in view.maps[0]:  | 
                    |
| 1274 | 
                        + err_msg = _msg.TranslatedString(  | 
                    |
| 1275 | 
                        + _msg.ErrMsgTemplate.SET_AND_UNSET_SAME_SETTING,  | 
                    |
| 1276 | 
                        + setting=setting,  | 
                    |
| 889 | 1277 | 
                        )  | 
                    
| 890 | 
                        - except Exception as exc: # noqa: BLE001  | 
                    |
| 891 | 
                        - err(  | 
                    |
| 1278 | 
                        + raise click.UsageError(str(err_msg))  | 
                    |
| 1279 | 
                        + if not cli_helpers.is_completable_item(service):  | 
                    |
| 1280 | 
                        + self.warning(  | 
                    |
| 892 | 1281 | 
                        _msg.TranslatedString(  | 
                    
| 893 | 
                        - _msg.ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS,  | 
                    |
| 894 | 
                        - error=str(exc),  | 
                    |
| 895 | 
                        - filename=None,  | 
                    |
| 896 | 
                        - ).maybe_without_filename(),  | 
                    |
| 897 | 
                        - exc_info=exc,  | 
                    |
| 1282 | 
                        + _msg.WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE,  | 
                    |
| 1283 | 
                        + service=service,  | 
                    |
| 1284 | 
                        + ),  | 
                    |
| 898 | 1285 | 
                        )  | 
                    
| 899 | 
                        -  | 
                    |
| 900 | 
                        - def put_config(config: _types.VaultConfig, /) -> None:  | 
                    |
| 901 | 
                        - try:  | 
                    |
| 902 | 
                        - cli_helpers.save_config(config)  | 
                    |
| 903 | 
                        - except OSError as exc:  | 
                    |
| 904 | 
                        - err(  | 
                    |
| 905 | 
                        - _msg.TranslatedString(  | 
                    |
| 906 | 
                        - _msg.ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS,  | 
                    |
| 907 | 
                        - error=exc.strerror,  | 
                    |
| 908 | 
                        - filename=exc.filename,  | 
                    |
| 909 | 
                        - ).maybe_without_filename(),  | 
                    |
| 1286 | 
                        + subtree: dict[str, Any] = (  | 
                    |
| 1287 | 
                        +            configuration['services'].setdefault(service, {})  # type: ignore[assignment]
                       | 
                    |
| 1288 | 
                        + if service  | 
                    |
| 1289 | 
                        +            else configuration.setdefault('global', {})
                       | 
                    |
| 910 | 1290 | 
                        )  | 
                    
| 911 | 
                        - except Exception as exc: # noqa: BLE001  | 
                    |
| 912 | 
                        - err(  | 
                    |
| 913 | 
                        - _msg.TranslatedString(  | 
                    |
| 914 | 
                        - _msg.ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS,  | 
                    |
| 915 | 
                        - error=str(exc),  | 
                    |
| 916 | 
                        - filename=None,  | 
                    |
| 917 | 
                        - ).maybe_without_filename(),  | 
                    |
| 918 | 
                        - exc_info=exc,  | 
                    |
| 1291 | 
                        + if overwrite_config:  | 
                    |
| 1292 | 
                        + subtree.clear()  | 
                    |
| 1293 | 
                        + else:  | 
                    |
| 1294 | 
                        + for setting in unset_settings:  | 
                    |
| 1295 | 
                        + subtree.pop(setting, None)  | 
                    |
| 1296 | 
                        + subtree.update(view)  | 
                    |
| 1297 | 
                        + assert _types.is_vault_config(configuration), (  | 
                    |
| 1298 | 
                        +            f'Invalid vault configuration: {configuration!r}'
                       | 
                    |
| 919 | 1299 | 
                        )  | 
                    
| 920 | 
                        -  | 
                    |
| 921 | 
                        - def get_user_config() -> dict[str, Any]:  | 
                    |
| 922 | 
                        - try:  | 
                    |
| 923 | 
                        - return cli_helpers.load_user_config()  | 
                    |
| 924 | 
                        - except FileNotFoundError:  | 
                    |
| 925 | 
                        -            return {}
                       | 
                    |
| 926 | 
                        - except OSError as exc:  | 
                    |
| 927 | 
                        - err(  | 
                    |
| 928 | 
                        - _msg.TranslatedString(  | 
                    |
| 929 | 
                        - _msg.ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG,  | 
                    |
| 930 | 
                        - error=exc.strerror,  | 
                    |
| 931 | 
                        - filename=exc.filename,  | 
                    |
| 932 | 
                        - ).maybe_without_filename(),  | 
                    |
| 933 | 
                        - )  | 
                    |
| 934 | 
                        - except Exception as exc: # noqa: BLE001  | 
                    |
| 935 | 
                        - err(  | 
                    |
| 936 | 
                        - _msg.TranslatedString(  | 
                    |
| 937 | 
                        - _msg.ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG,  | 
                    |
| 938 | 
                        - error=str(exc),  | 
                    |
| 939 | 
                        - filename=None,  | 
                    |
| 940 | 
                        - ).maybe_without_filename(),  | 
                    |
| 941 | 
                        - exc_info=exc,  | 
                    |
| 942 | 
                        - )  | 
                    |
| 943 | 
                        -  | 
                    |
| 944 | 
                        - configuration: _types.VaultConfig  | 
                    |
| 945 | 
                        -  | 
                    |
| 946 | 
                        -    check_incompatible_options('--phrase', '--key')
                       | 
                    |
| 947 | 
                        - for group in (  | 
                    |
| 948 | 
                        - cli_machinery.ConfigurationOption,  | 
                    |
| 949 | 
                        - cli_machinery.StorageManagementOption,  | 
                    |
| 950 | 
                        - ):  | 
                    |
| 951 | 
                        - for opt in options_in_group[group]:  | 
                    |
| 952 | 
                        -            if opt not in {
                       | 
                    |
| 953 | 
                        - params_by_str['--config'],  | 
                    |
| 954 | 
                        - params_by_str['--notes'],  | 
                    |
| 955 | 
                        - }:  | 
                    |
| 956 | 
                        - for other_opt in options_in_group[  | 
                    |
| 957 | 
                        - cli_machinery.PassphraseGenerationOption  | 
                    |
| 958 | 
                        - ]:  | 
                    |
| 959 | 
                        - check_incompatible_options(opt, other_opt)  | 
                    |
| 960 | 
                        -  | 
                    |
| 961 | 
                        - for group in (  | 
                    |
| 962 | 
                        - cli_machinery.ConfigurationOption,  | 
                    |
| 963 | 
                        - cli_machinery.StorageManagementOption,  | 
                    |
| 964 | 
                        - ):  | 
                    |
| 965 | 
                        - for opt in options_in_group[group]:  | 
                    |
| 966 | 
                        - for other_opt in options_in_group[  | 
                    |
| 967 | 
                        - cli_machinery.ConfigurationOption  | 
                    |
| 968 | 
                        - ]:  | 
                    |
| 969 | 
                        -                if {opt, other_opt} != {
                       | 
                    |
| 970 | 
                        - params_by_str['--config'],  | 
                    |
| 971 | 
                        - params_by_str['--notes'],  | 
                    |
| 972 | 
                        - }:  | 
                    |
| 973 | 
                        - check_incompatible_options(opt, other_opt)  | 
                    |
| 974 | 
                        - for other_opt in options_in_group[  | 
                    |
| 975 | 
                        - cli_machinery.StorageManagementOption  | 
                    |
| 976 | 
                        - ]:  | 
                    |
| 977 | 
                        - check_incompatible_options(opt, other_opt)  | 
                    |
| 978 | 
                        - sv_or_global_options = options_in_group[  | 
                    |
| 979 | 
                        - cli_machinery.PassphraseGenerationOption  | 
                    |
| 980 | 
                        - ]  | 
                    |
| 981 | 
                        - for param in sv_or_global_options:  | 
                    |
| 982 | 
                        - if is_param_set(param) and not (  | 
                    |
| 983 | 
                        - service is not None or is_param_set(params_by_str['--config'])  | 
                    |
| 984 | 
                        - ):  | 
                    |
| 985 | 
                        - err_msg = _msg.TranslatedString(  | 
                    |
| 986 | 
                        - _msg.ErrMsgTemplate.PARAMS_NEEDS_SERVICE_OR_CONFIG,  | 
                    |
| 987 | 
                        - param=param.opts[0],  | 
                    |
| 988 | 
                        - service_metavar=service_metavar,  | 
                    |
| 989 | 
                        - )  | 
                    |
| 990 | 
                        - raise click.UsageError(str(err_msg))  | 
                    |
| 991 | 
                        - sv_options = [params_by_str['--notes'], params_by_str['--delete']]  | 
                    |
| 992 | 
                        - for param in sv_options:  | 
                    |
| 993 | 
                        - if is_param_set(param) and not service is not None:  | 
                    |
| 994 | 
                        - err_msg = _msg.TranslatedString(  | 
                    |
| 995 | 
                        - _msg.ErrMsgTemplate.PARAMS_NEEDS_SERVICE,  | 
                    |
| 996 | 
                        - param=param.opts[0],  | 
                    |
| 997 | 
                        - service_metavar=service_metavar,  | 
                    |
| 998 | 
                        - )  | 
                    |
| 999 | 
                        - raise click.UsageError(str(err_msg))  | 
                    |
| 1000 | 
                        - no_sv_options = [  | 
                    |
| 1001 | 
                        - params_by_str['--delete-globals'],  | 
                    |
| 1002 | 
                        - params_by_str['--clear'],  | 
                    |
| 1003 | 
                        - *options_in_group[cli_machinery.StorageManagementOption],  | 
                    |
| 1004 | 
                        - ]  | 
                    |
| 1005 | 
                        - for param in no_sv_options:  | 
                    |
| 1006 | 
                        - if is_param_set(param) and service is not None:  | 
                    |
| 1007 | 
                        - err_msg = _msg.TranslatedString(  | 
                    |
| 1008 | 
                        - _msg.ErrMsgTemplate.PARAMS_NO_SERVICE,  | 
                    |
| 1009 | 
                        - param=param.opts[0],  | 
                    |
| 1010 | 
                        - service_metavar=service_metavar,  | 
                    |
| 1011 | 
                        - )  | 
                    |
| 1012 | 
                        - raise click.UsageError(str(err_msg))  | 
                    |
| 1013 | 
                        -  | 
                    |
| 1014 | 
                        - user_config = get_user_config()  | 
                    |
| 1015 | 
                        -  | 
                    |
| 1016 | 
                        - if service == '': # noqa: PLC1901  | 
                    |
| 1017 | 
                        - logger.warning(  | 
                    |
| 1018 | 
                        - _msg.TranslatedString(  | 
                    |
| 1019 | 
                        - _msg.WarnMsgTemplate.EMPTY_SERVICE_NOT_SUPPORTED,  | 
                    |
| 1020 | 
                        - service_metavar=service_metavar,  | 
                    |
| 1021 | 
                        - ),  | 
                    |
| 1022 | 
                        -            extra={'color': ctx.color},
                       | 
                    |
| 1023 | 
                        - )  | 
                    |
| 1024 | 
                        -  | 
                    |
| 1025 | 
                        - if edit_notes and not store_config_only:  | 
                    |
| 1026 | 
                        - logger.warning(  | 
                    |
| 1027 | 
                        - _msg.TranslatedString(  | 
                    |
| 1028 | 
                        - _msg.WarnMsgTemplate.EDITING_NOTES_BUT_NOT_STORING_CONFIG,  | 
                    |
| 1029 | 
                        - service_metavar=service_metavar,  | 
                    |
| 1030 | 
                        - ),  | 
                    |
| 1031 | 
                        -            extra={'color': ctx.color},
                       | 
                    |
| 1032 | 
                        - )  | 
                    |
| 1033 | 
                        -  | 
                    |
| 1034 | 
                        - readwrite_ops = [  | 
                    |
| 1035 | 
                        - delete_service_settings,  | 
                    |
| 1036 | 
                        - delete_globals,  | 
                    |
| 1037 | 
                        - clear_all_settings,  | 
                    |
| 1038 | 
                        - import_settings,  | 
                    |
| 1039 | 
                        - store_config_only,  | 
                    |
| 1040 | 
                        - ]  | 
                    |
| 1041 | 
                        - mutex: Callable[[], contextlib.AbstractContextManager[None]] = (  | 
                    |
| 1042 | 
                        - cli_helpers.configuration_mutex  | 
                    |
| 1043 | 
                        - if any(readwrite_ops)  | 
                    |
| 1044 | 
                        - else contextlib.nullcontext  | 
                    |
| 1045 | 
                        - )  | 
                    |
| 1046 | 
                        -  | 
                    |
| 1047 | 
                        - with mutex(): # noqa: PLR1702  | 
                    |
| 1048 | 
                        - if delete_service_settings:  | 
                    |
| 1300 | 
                        + if edit_notes:  | 
                    |
| 1049 | 1301 | 
                        assert service is not None  | 
                    
| 1050 | 
                        - configuration = get_config()  | 
                    |
| 1051 | 
                        - if service in configuration['services']:  | 
                    |
| 1052 | 
                        - del configuration['services'][service]  | 
                    |
| 1053 | 
                        - put_config(configuration)  | 
                    |
| 1054 | 
                        - elif delete_globals:  | 
                    |
| 1055 | 
                        - configuration = get_config()  | 
                    |
| 1056 | 
                        - if 'global' in configuration:  | 
                    |
| 1057 | 
                        - del configuration['global']  | 
                    |
| 1058 | 
                        - put_config(configuration)  | 
                    |
| 1059 | 
                        - elif clear_all_settings:  | 
                    |
| 1060 | 
                        -            put_config({'services': {}})
                       | 
                    |
| 1061 | 
                        - elif import_settings:  | 
                    |
| 1062 | 
                        - try:  | 
                    |
| 1063 | 
                        - # TODO(the-13th-letter): keep track of auto-close; try  | 
                    |
| 1064 | 
                        - # os.dup if feasible  | 
                    |
| 1065 | 
                        - infile = cast(  | 
                    |
| 1066 | 
                        - 'TextIO',  | 
                    |
| 1067 | 
                        - (  | 
                    |
| 1068 | 
                        - import_settings  | 
                    |
| 1069 | 
                        - if hasattr(import_settings, 'close')  | 
                    |
| 1070 | 
                        - else click.open_file(os.fspath(import_settings), 'rt')  | 
                    |
| 1071 | 
                        - ),  | 
                    |
| 1302 | 
                        + notes_instructions = _msg.TranslatedString(  | 
                    |
| 1303 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT  | 
                    |
| 1072 | 1304 | 
                        )  | 
                    
| 1073 | 
                        - # Don't specifically catch TypeError or ValueError here if  | 
                    |
| 1074 | 
                        - # the passed-in fileobj is not a readable text stream. This  | 
                    |
| 1075 | 
                        - # will never happen on the command-line (thanks to `click`),  | 
                    |
| 1076 | 
                        - # and for programmatic use, our caller may want accurate  | 
                    |
| 1077 | 
                        - # error information.  | 
                    |
| 1078 | 
                        - with infile:  | 
                    |
| 1079 | 
                        - maybe_config = json.load(infile)  | 
                    |
| 1080 | 
                        - except json.JSONDecodeError as exc:  | 
                    |
| 1081 | 
                        - err(  | 
                    |
| 1082 | 
                        - _msg.TranslatedString(  | 
                    |
| 1083 | 
                        - _msg.ErrMsgTemplate.CANNOT_DECODEIMPORT_VAULT_SETTINGS,  | 
                    |
| 1084 | 
                        - error=exc,  | 
                    |
| 1305 | 
                        + notes_marker = _msg.TranslatedString(  | 
                    |
| 1306 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER  | 
                    |
| 1085 | 1307 | 
                        )  | 
                    
| 1308 | 
                        + notes_legacy_instructions = _msg.TranslatedString(  | 
                    |
| 1309 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT  | 
                    |
| 1086 | 1310 | 
                        )  | 
                    
| 1087 | 
                        - except OSError as exc:  | 
                    |
| 1088 | 
                        - err(  | 
                    |
| 1089 | 
                        - _msg.TranslatedString(  | 
                    |
| 1090 | 
                        - _msg.ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS,  | 
                    |
| 1091 | 
                        - error=exc.strerror,  | 
                    |
| 1092 | 
                        - filename=exc.filename,  | 
                    |
| 1093 | 
                        - ).maybe_without_filename()  | 
                    |
| 1311 | 
                        +            old_notes_value = subtree.get('notes', '')
                       | 
                    |
| 1312 | 
                        + if modern_editor_interface:  | 
                    |
| 1313 | 
                        + text = '\n'.join([  | 
                    |
| 1314 | 
                        + str(notes_instructions),  | 
                    |
| 1315 | 
                        + str(notes_marker),  | 
                    |
| 1316 | 
                        + old_notes_value,  | 
                    |
| 1317 | 
                        + ])  | 
                    |
| 1318 | 
                        + else:  | 
                    |
| 1319 | 
                        + text = old_notes_value or str(notes_legacy_instructions)  | 
                    |
| 1320 | 
                        + notes_value = click.edit(text=text, require_save=False)  | 
                    |
| 1321 | 
                        + assert notes_value is not None  | 
                    |
| 1322 | 
                        + if (  | 
                    |
| 1323 | 
                        + not modern_editor_interface  | 
                    |
| 1324 | 
                        + and notes_value.strip() != old_notes_value.strip()  | 
                    |
| 1325 | 
                        + ):  | 
                    |
| 1326 | 
                        + backup_file = cli_helpers.config_filename(  | 
                    |
| 1327 | 
                        + subsystem='notes backup'  | 
                    |
| 1094 | 1328 | 
                        )  | 
                    
| 1095 | 
                        - cleaned = _types.clean_up_falsy_vault_config_values(maybe_config)  | 
                    |
| 1096 | 
                        - if not _types.is_vault_config(maybe_config):  | 
                    |
| 1097 | 
                        - err(  | 
                    |
| 1329 | 
                        + backup_file.write_text(old_notes_value, encoding='UTF-8')  | 
                    |
| 1330 | 
                        + self.warning(  | 
                    |
| 1098 | 1331 | 
                        _msg.TranslatedString(  | 
                    
| 1099 | 
                        - _msg.ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS,  | 
                    |
| 1100 | 
                        - error=_msg.TranslatedString(  | 
                    |
| 1101 | 
                        - _msg.ErrMsgTemplate.INVALID_VAULT_CONFIG,  | 
                    |
| 1102 | 
                        - config=maybe_config,  | 
                    |
| 1332 | 
                        + _msg.WarnMsgTemplate.LEGACY_EDITOR_INTERFACE_NOTES_BACKUP,  | 
                    |
| 1333 | 
                        + filename=str(backup_file),  | 
                    |
| 1103 | 1334 | 
                        ),  | 
                    
| 1104 | 
                        - filename=None,  | 
                    |
| 1105 | 
                        - ).maybe_without_filename()  | 
                    |
| 1106 | 1335 | 
                        )  | 
                    
| 1107 | 
                        - assert cleaned is not None  | 
                    |
| 1108 | 
                        - for step in cleaned:  | 
                    |
| 1109 | 
                        - # These are never fatal errors, because the semantics of  | 
                    |
| 1110 | 
                        - # vault upon encountering these settings are ill-specified,  | 
                    |
| 1111 | 
                        - # but not ill-defined.  | 
                    |
| 1112 | 
                        - if step.action == 'replace':  | 
                    |
| 1113 | 
                        - logger.warning(  | 
                    |
| 1114 | 
                        - _msg.TranslatedString(  | 
                    |
| 1115 | 
                        - _msg.WarnMsgTemplate.STEP_REPLACE_INVALID_VALUE,  | 
                    |
| 1116 | 
                        - old=json.dumps(step.old_value),  | 
                    |
| 1117 | 
                        - path=_types.json_path(step.path),  | 
                    |
| 1118 | 
                        - new=json.dumps(step.new_value),  | 
                    |
| 1119 | 
                        - ),  | 
                    |
| 1120 | 
                        -                        extra={'color': ctx.color},
                       | 
                    |
| 1336 | 
                        + subtree['notes'] = notes_value.strip()  | 
                    |
| 1337 | 
                        + elif (  | 
                    |
| 1338 | 
                        + modern_editor_interface and notes_value.strip() != text.strip()  | 
                    |
| 1339 | 
                        + ):  | 
                    |
| 1340 | 
                        + notes_lines = collections.deque(  | 
                    |
| 1341 | 
                        + notes_value.splitlines(keepends=True)  | 
                    |
| 1121 | 1342 | 
                        )  | 
                    
| 1343 | 
                        + while notes_lines:  | 
                    |
| 1344 | 
                        + line = notes_lines.popleft()  | 
                    |
| 1345 | 
                        + if line.startswith(str(notes_marker)):  | 
                    |
| 1346 | 
                        + notes_value = ''.join(notes_lines)  | 
                    |
| 1347 | 
                        + break  | 
                    |
| 1122 | 1348 | 
                        else:  | 
                    
| 1123 | 
                        - logger.warning(  | 
                    |
| 1349 | 
                        + if not notes_value.strip():  | 
                    |
| 1350 | 
                        + self.err(  | 
                    |
| 1124 | 1351 | 
                        _msg.TranslatedString(  | 
                    
| 1125 | 
                        - _msg.WarnMsgTemplate.STEP_REMOVE_INEFFECTIVE_VALUE,  | 
                    |
| 1126 | 
                        - path=_types.json_path(step.path),  | 
                    |
| 1127 | 
                        - old=json.dumps(step.old_value),  | 
                    |
| 1128 | 
                        - ),  | 
                    |
| 1129 | 
                        -                        extra={'color': ctx.color},
                       | 
                    |
| 1352 | 
                        + _msg.ErrMsgTemplate.USER_ABORTED_EDIT  | 
                    |
| 1130 | 1353 | 
                        )  | 
                    
| 1131 | 
                        - if '' in maybe_config['services']:  | 
                    |
| 1132 | 
                        - logger.warning(  | 
                    |
| 1133 | 
                        - _msg.TranslatedString(  | 
                    |
| 1134 | 
                        - _msg.WarnMsgTemplate.EMPTY_SERVICE_SETTINGS_INACCESSIBLE,  | 
                    |
| 1135 | 
                        - service_metavar=service_metavar,  | 
                    |
| 1136 | 
                        - PROG_NAME=PROG_NAME,  | 
                    |
| 1137 | 
                        - ),  | 
                    |
| 1138 | 
                        -                    extra={'color': ctx.color},
                       | 
                    |
| 1139 | 1354 | 
                        )  | 
                    
| 1140 | 
                        - for service_name in sorted(maybe_config['services'].keys()):  | 
                    |
| 1141 | 
                        - if not cli_helpers.is_completable_item(service_name):  | 
                    |
| 1142 | 
                        - logger.warning(  | 
                    |
| 1143 | 
                        - _msg.TranslatedString(  | 
                    |
| 1144 | 
                        - _msg.WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE,  | 
                    |
| 1145 | 
                        - service=service_name,  | 
                    |
| 1146 | 
                        - ),  | 
                    |
| 1147 | 
                        -                        extra={'color': ctx.color},
                       | 
                    |
| 1355 | 
                        + subtree['notes'] = notes_value.strip()  | 
                    |
| 1356 | 
                        + self.put_config(configuration)  | 
                    |
| 1357 | 
                        +  | 
                    |
| 1358 | 
                        + def run_op_derive_passphrase(self) -> None:  | 
                    |
| 1359 | 
                        + """Derive a passphrase.  | 
                    |
| 1360 | 
                        +  | 
                    |
| 1361 | 
                        + Derive a service passphrase using the effective settings from  | 
                    |
| 1362 | 
                        + both the command-line and the stored configuration. If any  | 
                    |
| 1363 | 
                        + service notes are stored for this service, print them as well.  | 
                    |
| 1364 | 
                        +  | 
                    |
| 1365 | 
                        + (An empty service name is permitted, though discouraged for  | 
                    |
| 1366 | 
                        + compatibility reasons.)  | 
                    |
| 1367 | 
                        +  | 
                    |
| 1368 | 
                        + Issue a warning (if appropriate) for Unicode normalization  | 
                    |
| 1369 | 
                        + issues with the interactive passphrase. Respect the  | 
                    |
| 1370 | 
                        + `--print-notes-before` and `--print-notes-after` options when  | 
                    |
| 1371 | 
                        + printing notes.  | 
                    |
| 1372 | 
                        +  | 
                    |
| 1373 | 
                        + Raises:  | 
                    |
| 1374 | 
                        + click.UsageError:  | 
                    |
| 1375 | 
                        + No master passphrase or master SSH key was given on both  | 
                    |
| 1376 | 
                        + the command-line and in the vault configuration on disk.  | 
                    |
| 1377 | 
                        + """  | 
                    |
| 1378 | 
                        + service = self.ctx.params['service']  | 
                    |
| 1379 | 
                        + use_key = self.ctx.params['use_key']  | 
                    |
| 1380 | 
                        + use_phrase = self.ctx.params['use_phrase']  | 
                    |
| 1381 | 
                        + print_notes_before = self.ctx.params['print_notes_before']  | 
                    |
| 1382 | 
                        + user_config = self.get_user_config()  | 
                    |
| 1383 | 
                        + settings = self.run_subop_query_phrase_or_key_change(  | 
                    |
| 1384 | 
                        + empty_service_permitted=False  | 
                    |
| 1148 | 1385 | 
                        )  | 
                    
| 1386 | 
                        + if use_phrase:  | 
                    |
| 1149 | 1387 | 
                        try:  | 
                    
| 1150 | 1388 | 
                        cli_helpers.check_for_misleading_passphrase(  | 
                    
| 1151 | 
                        -                    ('global',),
                       | 
                    |
| 1152 | 
                        -                    cast('dict[str, Any]', maybe_config.get('global', {})),
                       | 
                    |
| 1153 | 
                        - main_config=user_config,  | 
                    |
| 1154 | 
                        - ctx=ctx,  | 
                    |
| 1155 | 
                        - )  | 
                    |
| 1156 | 
                        - for key, value in maybe_config['services'].items():  | 
                    |
| 1157 | 
                        - cli_helpers.check_for_misleading_passphrase(  | 
                    |
| 1158 | 
                        -                        ('services', key),
                       | 
                    |
| 1159 | 
                        -                        cast('dict[str, Any]', value),
                       | 
                    |
| 1389 | 
                        + cli_helpers.ORIGIN.INTERACTIVE,  | 
                    |
| 1390 | 
                        +                    {'phrase': settings['phrase']},
                       | 
                    |
| 1160 | 1391 | 
                        main_config=user_config,  | 
                    
| 1161 | 
                        - ctx=ctx,  | 
                    |
| 1392 | 
                        + ctx=self.ctx,  | 
                    |
| 1162 | 1393 | 
                        )  | 
                    
| 1163 | 1394 | 
                        except AssertionError as exc:  | 
                    
| 1164 | 
                        - err(  | 
                    |
| 1395 | 
                        + self.err(  | 
                    |
| 1165 | 1396 | 
                        _msg.TranslatedString(  | 
                    
| 1166 | 1397 | 
                        _msg.ErrMsgTemplate.INVALID_USER_CONFIG,  | 
                    
| 1167 | 1398 | 
                        error=exc,  | 
                    
| 1168 | 1399 | 
                        filename=None,  | 
                    
| 1169 | 1400 | 
                        ).maybe_without_filename(),  | 
                    
| 1170 | 1401 | 
                        )  | 
                    
| 1171 | 
                        -            global_obj = maybe_config.get('global', {})
                       | 
                    |
| 1172 | 
                        -            has_key = _types.js_truthiness(global_obj.get('key'))
                       | 
                    |
| 1173 | 
                        -            has_phrase = _types.js_truthiness(global_obj.get('phrase'))
                       | 
                    |
| 1174 | 
                        - if has_key and has_phrase:  | 
                    |
| 1175 | 
                        - logger.warning(  | 
                    |
| 1176 | 
                        - _msg.TranslatedString(  | 
                    |
| 1177 | 
                        - _msg.WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE,  | 
                    |
| 1178 | 
                        - ),  | 
                    |
| 1179 | 
                        -                    extra={'color': ctx.color},
                       | 
                    |
| 1402 | 
                        + phrase: str | bytes  | 
                    |
| 1403 | 
                        +        overrides = cast('dict[str, int | str]', settings.maps[0])
                       | 
                    |
| 1404 | 
                        + # If either --key or --phrase are given, use that setting.  | 
                    |
| 1405 | 
                        + # Otherwise, if both key and phrase are set in the config,  | 
                    |
| 1406 | 
                        + # use the key. Otherwise, if only one of key and phrase is  | 
                    |
| 1407 | 
                        + # set in the config, use that one. In all these above  | 
                    |
| 1408 | 
                        + # cases, set the phrase via vault.Vault.phrase_from_key if  | 
                    |
| 1409 | 
                        + # a key is given. Finally, if nothing is set, error out.  | 
                    |
| 1410 | 
                        + if use_key:  | 
                    |
| 1411 | 
                        + phrase = cli_helpers.key_to_phrase(  | 
                    |
| 1412 | 
                        +                cast('str', overrides['key']), error_callback=self.err
                       | 
                    |
| 1180 | 1413 | 
                        )  | 
                    
| 1181 | 
                        - for service_name, service_obj in maybe_config['services'].items():  | 
                    |
| 1182 | 
                        - has_key = _types.js_truthiness(  | 
                    |
| 1183 | 
                        -                    service_obj.get('key')
                       | 
                    |
| 1184 | 
                        -                ) or _types.js_truthiness(global_obj.get('key'))
                       | 
                    |
| 1185 | 
                        - has_phrase = _types.js_truthiness(  | 
                    |
| 1186 | 
                        -                    service_obj.get('phrase')
                       | 
                    |
| 1187 | 
                        -                ) or _types.js_truthiness(global_obj.get('phrase'))
                       | 
                    |
| 1188 | 
                        - if has_key and has_phrase:  | 
                    |
| 1189 | 
                        - logger.warning(  | 
                    |
| 1190 | 
                        - _msg.TranslatedString(  | 
                    |
| 1191 | 
                        - _msg.WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE,  | 
                    |
| 1192 | 
                        - service=json.dumps(service_name),  | 
                    |
| 1193 | 
                        - ),  | 
                    |
| 1194 | 
                        -                        extra={'color': ctx.color},
                       | 
                    |
| 1414 | 
                        + elif use_phrase:  | 
                    |
| 1415 | 
                        +            phrase = cast('str', overrides['phrase'])
                       | 
                    |
| 1416 | 
                        +        elif settings.get('key'):
                       | 
                    |
| 1417 | 
                        + phrase = cli_helpers.key_to_phrase(  | 
                    |
| 1418 | 
                        +                cast('str', settings['key']), error_callback=self.err
                       | 
                    |
| 1195 | 1419 | 
                        )  | 
                    
| 1196 | 
                        - if overwrite_config:  | 
                    |
| 1197 | 
                        - put_config(maybe_config)  | 
                    |
| 1420 | 
                        +        elif settings.get('phrase'):
                       | 
                    |
| 1421 | 
                        +            phrase = cast('str', settings['phrase'])
                       | 
                    |
| 1198 | 1422 | 
                        else:  | 
                    
| 1199 | 
                        - configuration = get_config()  | 
                    |
| 1200 | 
                        - merged_config: collections.ChainMap[str, Any] = (  | 
                    |
| 1201 | 
                        - collections.ChainMap(  | 
                    |
| 1202 | 
                        -                        {
                       | 
                    |
| 1203 | 
                        - 'services': collections.ChainMap(  | 
                    |
| 1204 | 
                        - maybe_config['services'],  | 
                    |
| 1205 | 
                        - configuration['services'],  | 
                    |
| 1206 | 
                        - ),  | 
                    |
| 1207 | 
                        - },  | 
                    |
| 1208 | 
                        -                        {'global': maybe_config['global']}
                       | 
                    |
| 1209 | 
                        - if 'global' in maybe_config  | 
                    |
| 1210 | 
                        -                        else {},
                       | 
                    |
| 1211 | 
                        -                        {'global': configuration['global']}
                       | 
                    |
| 1212 | 
                        - if 'global' in configuration  | 
                    |
| 1213 | 
                        -                        else {},
                       | 
                    |
| 1214 | 
                        - )  | 
                    |
| 1423 | 
                        + err_msg = _msg.TranslatedString(  | 
                    |
| 1424 | 
                        + _msg.ErrMsgTemplate.NO_KEY_OR_PHRASE  | 
                    |
| 1215 | 1425 | 
                        )  | 
                    
| 1216 | 
                        -                new_config: Any = {
                       | 
                    |
| 1217 | 
                        - k: dict(v) if isinstance(v, collections.ChainMap) else v  | 
                    |
| 1218 | 
                        - for k, v in sorted(merged_config.items())  | 
                    |
| 1219 | 
                        - }  | 
                    |
| 1220 | 
                        - assert _types.is_vault_config(new_config)  | 
                    |
| 1221 | 
                        - put_config(new_config)  | 
                    |
| 1222 | 
                        - elif export_settings:  | 
                    |
| 1223 | 
                        - configuration = get_config()  | 
                    |
| 1224 | 
                        - try:  | 
                    |
| 1225 | 
                        - # TODO(the-13th-letter): keep track of auto-close; try  | 
                    |
| 1226 | 
                        - # os.dup if feasible  | 
                    |
| 1227 | 
                        - outfile = cast(  | 
                    |
| 1228 | 
                        - 'TextIO',  | 
                    |
| 1229 | 
                        - (  | 
                    |
| 1230 | 
                        - export_settings  | 
                    |
| 1231 | 
                        - if hasattr(export_settings, 'close')  | 
                    |
| 1232 | 
                        - else click.open_file(os.fspath(export_settings), 'wt')  | 
                    |
| 1233 | 
                        - ),  | 
                    |
| 1234 | 
                        - )  | 
                    |
| 1235 | 
                        - # Don't specifically catch TypeError or ValueError here if  | 
                    |
| 1236 | 
                        - # the passed-in fileobj is not a writable text stream. This  | 
                    |
| 1237 | 
                        - # will never happen on the command-line (thanks to `click`),  | 
                    |
| 1238 | 
                        - # and for programmatic use, our caller may want accurate  | 
                    |
| 1239 | 
                        - # error information.  | 
                    |
| 1240 | 
                        - with outfile:  | 
                    |
| 1241 | 
                        - if export_as == 'sh':  | 
                    |
| 1242 | 
                        - this_ctx = ctx  | 
                    |
| 1243 | 
                        - prog_name_pieces = collections.deque([  | 
                    |
| 1244 | 
                        - this_ctx.info_name or 'vault',  | 
                    |
| 1245 | 
                        - ])  | 
                    |
| 1246 | 
                        - while (  | 
                    |
| 1247 | 
                        - this_ctx.parent is not None  | 
                    |
| 1248 | 
                        - and this_ctx.parent.info_name is not None  | 
                    |
| 1249 | 
                        - ):  | 
                    |
| 1250 | 
                        - prog_name_pieces.appendleft(  | 
                    |
| 1251 | 
                        - this_ctx.parent.info_name  | 
                    |
| 1252 | 
                        - )  | 
                    |
| 1253 | 
                        - this_ctx = this_ctx.parent  | 
                    |
| 1254 | 
                        - cli_helpers.print_config_as_sh_script(  | 
                    |
| 1255 | 
                        - configuration,  | 
                    |
| 1256 | 
                        - outfile=outfile,  | 
                    |
| 1257 | 
                        - prog_name_list=prog_name_pieces,  | 
                    |
| 1258 | 
                        - )  | 
                    |
| 1259 | 
                        - else:  | 
                    |
| 1260 | 
                        - json.dump(  | 
                    |
| 1261 | 
                        - configuration,  | 
                    |
| 1262 | 
                        - outfile,  | 
                    |
| 1263 | 
                        - ensure_ascii=False,  | 
                    |
| 1264 | 
                        - indent=2,  | 
                    |
| 1265 | 
                        - sort_keys=True,  | 
                    |
| 1266 | 
                        - )  | 
                    |
| 1267 | 
                        - except OSError as exc:  | 
                    |
| 1268 | 
                        - err(  | 
                    |
| 1269 | 
                        - _msg.TranslatedString(  | 
                    |
| 1270 | 
                        - _msg.ErrMsgTemplate.CANNOT_EXPORT_VAULT_SETTINGS,  | 
                    |
| 1271 | 
                        - error=exc.strerror,  | 
                    |
| 1272 | 
                        - filename=exc.filename,  | 
                    |
| 1273 | 
                        - ).maybe_without_filename(),  | 
                    |
| 1274 | 
                        - )  | 
                    |
| 1275 | 
                        - else:  | 
                    |
| 1276 | 
                        - configuration = get_config()  | 
                    |
| 1277 | 
                        - # This block could be type checked more stringently, but this  | 
                    |
| 1278 | 
                        - # would probably involve a lot of code repetition. Since we  | 
                    |
| 1279 | 
                        - # have a type guarding function anyway, assert that we didn't  | 
                    |
| 1280 | 
                        - # make any mistakes at the end instead.  | 
                    |
| 1281 | 
                        -            global_keys = {'key', 'phrase'}
                       | 
                    |
| 1282 | 
                        -            service_keys = {
                       | 
                    |
| 1283 | 
                        - 'key',  | 
                    |
| 1284 | 
                        - 'phrase',  | 
                    |
| 1426 | 
                        + raise click.UsageError(str(err_msg))  | 
                    |
| 1427 | 
                        +        overrides.pop('key', '')
                       | 
                    |
| 1428 | 
                        +        overrides.pop('phrase', '')
                       | 
                    |
| 1429 | 
                        + assert service is not None  | 
                    |
| 1430 | 
                        +        vault_service_keys = {
                       | 
                    |
| 1285 | 1431 | 
                        'length',  | 
                    
| 1286 | 1432 | 
                        'repeat',  | 
                    
| 1287 | 1433 | 
                        'lower',  | 
                    
| ... | ... | 
                      @@ -1291,283 +1437,442 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915  | 
                  
| 1291 | 1437 | 
                        'dash',  | 
                    
| 1292 | 1438 | 
                        'symbol',  | 
                    
| 1293 | 1439 | 
                        }  | 
                    
| 1294 | 
                        - settings: collections.ChainMap[str, Any] = collections.ChainMap(  | 
                    |
| 1295 | 
                        -                {
                       | 
                    |
| 1296 | 
                        - k: v  | 
                    |
| 1297 | 
                        - for k, v in locals().items()  | 
                    |
| 1298 | 
                        - if k in service_keys and v is not None  | 
                    |
| 1299 | 
                        - },  | 
                    |
| 1300 | 
                        - cast(  | 
                    |
| 1301 | 
                        - 'dict[str, Any]',  | 
                    |
| 1302 | 
                        -                    configuration['services'].get(service, {})
                       | 
                    |
| 1303 | 
                        - if service  | 
                    |
| 1304 | 
                        -                    else {},
                       | 
                    |
| 1305 | 
                        - ),  | 
                    |
| 1306 | 
                        -                cast('dict[str, Any]', configuration.get('global', {})),
                       | 
                    |
| 1307 | 
                        - )  | 
                    |
| 1308 | 
                        - if not store_config_only and not service:  | 
                    |
| 1309 | 
                        - err_msg = _msg.TranslatedString(  | 
                    |
| 1310 | 
                        - _msg.ErrMsgTemplate.SERVICE_REQUIRED,  | 
                    |
| 1440 | 
                        +        kwargs = {
                       | 
                    |
| 1441 | 
                        +            cast('str', k): cast('int', settings[k])
                       | 
                    |
| 1442 | 
                        + for k in vault_service_keys  | 
                    |
| 1443 | 
                        + if k in settings  | 
                    |
| 1444 | 
                        + }  | 
                    |
| 1445 | 
                        + result = vault.Vault(phrase=phrase, **kwargs).generate(service)  | 
                    |
| 1446 | 
                        +        service_notes = cast('str', settings.get('notes', '')).strip()
                       | 
                    |
| 1447 | 
                        + if print_notes_before and service_notes.strip():  | 
                    |
| 1448 | 
                        +            click.echo(f'{service_notes}\n', err=True, color=self.ctx.color)
                       | 
                    |
| 1449 | 
                        +        click.echo(result.decode('ASCII'), color=self.ctx.color)
                       | 
                    |
| 1450 | 
                        + if not print_notes_before and service_notes.strip():  | 
                    |
| 1451 | 
                        +            click.echo(f'\n{service_notes}\n', err=True, color=self.ctx.color)
                       | 
                    |
| 1452 | 
                        +  | 
                    |
| 1453 | 
                        +  | 
                    |
| 1454 | 
                        +@derivepassphrase.command(  | 
                    |
| 1455 | 
                        + 'vault',  | 
                    |
| 1456 | 
                        +    context_settings={'help_option_names': ['-h', '--help']},
                       | 
                    |
| 1457 | 
                        + cls=cli_machinery.CommandWithHelpGroups,  | 
                    |
| 1458 | 
                        + help=(  | 
                    |
| 1459 | 
                        + _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_01),  | 
                    |
| 1460 | 
                        + _msg.TranslatedString(  | 
                    |
| 1461 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_02,  | 
                    |
| 1311 | 1462 | 
                        service_metavar=_msg.TranslatedString(  | 
                    
| 1312 | 1463 | 
                        _msg.Label.VAULT_METAVAR_SERVICE  | 
                    
| 1313 | 1464 | 
                        ),  | 
                    
| 1465 | 
                        + ),  | 
                    |
| 1466 | 
                        + ),  | 
                    |
| 1467 | 
                        + epilog=(  | 
                    |
| 1468 | 
                        + _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_EPILOG_01),  | 
                    |
| 1469 | 
                        + _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_EPILOG_02),  | 
                    |
| 1470 | 
                        + ),  | 
                    |
| 1314 | 1471 | 
                        )  | 
                    
| 1315 | 
                        - raise click.UsageError(str(err_msg))  | 
                    |
| 1316 | 
                        - if use_key:  | 
                    |
| 1317 | 
                        - try:  | 
                    |
| 1318 | 
                        - key = base64.standard_b64encode(  | 
                    |
| 1319 | 
                        - cli_helpers.select_ssh_key(ctx=ctx)  | 
                    |
| 1320 | 
                        -                    ).decode('ASCII')
                       | 
                    |
| 1321 | 
                        - except IndexError:  | 
                    |
| 1322 | 
                        - err(  | 
                    |
| 1323 | 
                        - _msg.TranslatedString(  | 
                    |
| 1324 | 
                        - _msg.ErrMsgTemplate.USER_ABORTED_SSH_KEY_SELECTION  | 
                    |
| 1472 | 
                        +@click.option(  | 
                    |
| 1473 | 
                        + '-p',  | 
                    |
| 1474 | 
                        + '--phrase',  | 
                    |
| 1475 | 
                        + 'use_phrase',  | 
                    |
| 1476 | 
                        + is_flag=True,  | 
                    |
| 1477 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1478 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_PHRASE_HELP_TEXT  | 
                    |
| 1325 | 1479 | 
                        ),  | 
                    
| 1480 | 
                        + cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 1326 | 1481 | 
                        )  | 
                    
| 1327 | 
                        - except KeyError:  | 
                    |
| 1328 | 
                        - err(  | 
                    |
| 1329 | 
                        - _msg.TranslatedString(  | 
                    |
| 1330 | 
                        - _msg.ErrMsgTemplate.NO_SSH_AGENT_FOUND  | 
                    |
| 1482 | 
                        +@click.option(  | 
                    |
| 1483 | 
                        + '-k',  | 
                    |
| 1484 | 
                        + '--key',  | 
                    |
| 1485 | 
                        + 'use_key',  | 
                    |
| 1486 | 
                        + is_flag=True,  | 
                    |
| 1487 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1488 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_KEY_HELP_TEXT  | 
                    |
| 1331 | 1489 | 
                        ),  | 
                    
| 1490 | 
                        + cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 1332 | 1491 | 
                        )  | 
                    
| 1333 | 
                        - except LookupError:  | 
                    |
| 1334 | 
                        - err(  | 
                    |
| 1335 | 
                        - _msg.TranslatedString(  | 
                    |
| 1336 | 
                        - _msg.ErrMsgTemplate.NO_SUITABLE_SSH_KEYS,  | 
                    |
| 1337 | 
                        - PROG_NAME=PROG_NAME,  | 
                    |
| 1492 | 
                        +@click.option(  | 
                    |
| 1493 | 
                        + '-l',  | 
                    |
| 1494 | 
                        + '--length',  | 
                    |
| 1495 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1496 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1497 | 
                        + ),  | 
                    |
| 1498 | 
                        + callback=cli_machinery.validate_length,  | 
                    |
| 1499 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1500 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_LENGTH_HELP_TEXT,  | 
                    |
| 1501 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1502 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1503 | 
                        + ),  | 
                    |
| 1504 | 
                        + ),  | 
                    |
| 1505 | 
                        + cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 1338 | 1506 | 
                        )  | 
                    
| 1507 | 
                        +@click.option(  | 
                    |
| 1508 | 
                        + '-r',  | 
                    |
| 1509 | 
                        + '--repeat',  | 
                    |
| 1510 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1511 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1512 | 
                        + ),  | 
                    |
| 1513 | 
                        + callback=cli_machinery.validate_occurrence_constraint,  | 
                    |
| 1514 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1515 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_REPEAT_HELP_TEXT,  | 
                    |
| 1516 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1517 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1518 | 
                        + ),  | 
                    |
| 1519 | 
                        + ),  | 
                    |
| 1520 | 
                        + cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 1339 | 1521 | 
                        )  | 
                    
| 1340 | 
                        - except NotImplementedError:  | 
                    |
| 1341 | 
                        - err(_msg.TranslatedString(_msg.ErrMsgTemplate.NO_AF_UNIX))  | 
                    |
| 1342 | 
                        - except OSError as exc:  | 
                    |
| 1343 | 
                        - err(  | 
                    |
| 1344 | 
                        - _msg.TranslatedString(  | 
                    |
| 1345 | 
                        - _msg.ErrMsgTemplate.CANNOT_CONNECT_TO_AGENT,  | 
                    |
| 1346 | 
                        - error=exc.strerror,  | 
                    |
| 1347 | 
                        - filename=exc.filename,  | 
                    |
| 1348 | 
                        - ).maybe_without_filename(),  | 
                    |
| 1522 | 
                        +@click.option(  | 
                    |
| 1523 | 
                        + '--lower',  | 
                    |
| 1524 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1525 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1526 | 
                        + ),  | 
                    |
| 1527 | 
                        + callback=cli_machinery.validate_occurrence_constraint,  | 
                    |
| 1528 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1529 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_LOWER_HELP_TEXT,  | 
                    |
| 1530 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1531 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1532 | 
                        + ),  | 
                    |
| 1533 | 
                        + ),  | 
                    |
| 1534 | 
                        + cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 1349 | 1535 | 
                        )  | 
                    
| 1350 | 
                        - except ssh_agent.SSHAgentFailedError as exc:  | 
                    |
| 1351 | 
                        - err(  | 
                    |
| 1352 | 
                        - _msg.TranslatedString(  | 
                    |
| 1353 | 
                        - _msg.ErrMsgTemplate.AGENT_REFUSED_LIST_KEYS  | 
                    |
| 1536 | 
                        +@click.option(  | 
                    |
| 1537 | 
                        + '--upper',  | 
                    |
| 1538 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1539 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1354 | 1540 | 
                        ),  | 
                    
| 1355 | 
                        - exc_info=exc,  | 
                    |
| 1541 | 
                        + callback=cli_machinery.validate_occurrence_constraint,  | 
                    |
| 1542 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1543 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_UPPER_HELP_TEXT,  | 
                    |
| 1544 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1545 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1546 | 
                        + ),  | 
                    |
| 1547 | 
                        + ),  | 
                    |
| 1548 | 
                        + cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 1356 | 1549 | 
                        )  | 
                    
| 1357 | 
                        - except RuntimeError as exc:  | 
                    |
| 1358 | 
                        - err(  | 
                    |
| 1359 | 
                        - _msg.TranslatedString(  | 
                    |
| 1360 | 
                        - _msg.ErrMsgTemplate.CANNOT_UNDERSTAND_AGENT  | 
                    |
| 1550 | 
                        +@click.option(  | 
                    |
| 1551 | 
                        + '--number',  | 
                    |
| 1552 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1553 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1361 | 1554 | 
                        ),  | 
                    
| 1362 | 
                        - exc_info=exc,  | 
                    |
| 1555 | 
                        + callback=cli_machinery.validate_occurrence_constraint,  | 
                    |
| 1556 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1557 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_NUMBER_HELP_TEXT,  | 
                    |
| 1558 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1559 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1560 | 
                        + ),  | 
                    |
| 1561 | 
                        + ),  | 
                    |
| 1562 | 
                        + cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 1363 | 1563 | 
                        )  | 
                    
| 1364 | 
                        - elif use_phrase:  | 
                    |
| 1365 | 
                        - maybe_phrase = cli_helpers.prompt_for_passphrase()  | 
                    |
| 1366 | 
                        - if not maybe_phrase:  | 
                    |
| 1367 | 
                        - err(  | 
                    |
| 1368 | 
                        - _msg.TranslatedString(  | 
                    |
| 1369 | 
                        - _msg.ErrMsgTemplate.USER_ABORTED_PASSPHRASE  | 
                    |
| 1564 | 
                        +@click.option(  | 
                    |
| 1565 | 
                        + '--space',  | 
                    |
| 1566 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1567 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1568 | 
                        + ),  | 
                    |
| 1569 | 
                        + callback=cli_machinery.validate_occurrence_constraint,  | 
                    |
| 1570 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1571 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_SPACE_HELP_TEXT,  | 
                    |
| 1572 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1573 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1574 | 
                        + ),  | 
                    |
| 1575 | 
                        + ),  | 
                    |
| 1576 | 
                        + cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 1370 | 1577 | 
                        )  | 
                    
| 1578 | 
                        +@click.option(  | 
                    |
| 1579 | 
                        + '--dash',  | 
                    |
| 1580 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1581 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1582 | 
                        + ),  | 
                    |
| 1583 | 
                        + callback=cli_machinery.validate_occurrence_constraint,  | 
                    |
| 1584 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1585 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_DASH_HELP_TEXT,  | 
                    |
| 1586 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1587 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1588 | 
                        + ),  | 
                    |
| 1589 | 
                        + ),  | 
                    |
| 1590 | 
                        + cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 1371 | 1591 | 
                        )  | 
                    
| 1372 | 
                        - else:  | 
                    |
| 1373 | 
                        - phrase = maybe_phrase  | 
                    |
| 1374 | 
                        - if store_config_only:  | 
                    |
| 1375 | 
                        - view: collections.ChainMap[str, Any]  | 
                    |
| 1376 | 
                        - view = (  | 
                    |
| 1377 | 
                        - collections.ChainMap(*settings.maps[:2])  | 
                    |
| 1378 | 
                        - if service  | 
                    |
| 1379 | 
                        - else collections.ChainMap(  | 
                    |
| 1380 | 
                        - settings.maps[0], settings.maps[2]  | 
                    |
| 1592 | 
                        +@click.option(  | 
                    |
| 1593 | 
                        + '--symbol',  | 
                    |
| 1594 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1595 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1596 | 
                        + ),  | 
                    |
| 1597 | 
                        + callback=cli_machinery.validate_occurrence_constraint,  | 
                    |
| 1598 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1599 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_SYMBOL_HELP_TEXT,  | 
                    |
| 1600 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1601 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1602 | 
                        + ),  | 
                    |
| 1603 | 
                        + ),  | 
                    |
| 1604 | 
                        + cls=cli_machinery.PassphraseGenerationOption,  | 
                    |
| 1381 | 1605 | 
                        )  | 
                    
| 1606 | 
                        +@click.option(  | 
                    |
| 1607 | 
                        + '-n',  | 
                    |
| 1608 | 
                        + '--notes',  | 
                    |
| 1609 | 
                        + 'edit_notes',  | 
                    |
| 1610 | 
                        + is_flag=True,  | 
                    |
| 1611 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1612 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_HELP_TEXT,  | 
                    |
| 1613 | 
                        + service_metavar=_msg.TranslatedString(  | 
                    |
| 1614 | 
                        + _msg.Label.VAULT_METAVAR_SERVICE  | 
                    |
| 1615 | 
                        + ),  | 
                    |
| 1616 | 
                        + ),  | 
                    |
| 1617 | 
                        + cls=cli_machinery.ConfigurationOption,  | 
                    |
| 1382 | 1618 | 
                        )  | 
                    
| 1383 | 
                        - if use_key:  | 
                    |
| 1384 | 
                        - view['key'] = key  | 
                    |
| 1385 | 
                        - elif use_phrase:  | 
                    |
| 1386 | 
                        - view['phrase'] = phrase  | 
                    |
| 1387 | 
                        - try:  | 
                    |
| 1388 | 
                        - cli_helpers.check_for_misleading_passphrase(  | 
                    |
| 1389 | 
                        -                            ('services', service) if service else ('global',),
                       | 
                    |
| 1390 | 
                        -                            {'phrase': phrase},
                       | 
                    |
| 1391 | 
                        - main_config=user_config,  | 
                    |
| 1392 | 
                        - ctx=ctx,  | 
                    |
| 1393 | 
                        - )  | 
                    |
| 1394 | 
                        - except AssertionError as exc:  | 
                    |
| 1395 | 
                        - err(  | 
                    |
| 1396 | 
                        - _msg.TranslatedString(  | 
                    |
| 1397 | 
                        - _msg.ErrMsgTemplate.INVALID_USER_CONFIG,  | 
                    |
| 1398 | 
                        - error=exc,  | 
                    |
| 1399 | 
                        - filename=None,  | 
                    |
| 1400 | 
                        - ).maybe_without_filename(),  | 
                    |
| 1401 | 
                        - )  | 
                    |
| 1402 | 
                        - if 'key' in settings:  | 
                    |
| 1403 | 
                        - if service:  | 
                    |
| 1404 | 
                        - w_msg = _msg.TranslatedString(  | 
                    |
| 1405 | 
                        - _msg.WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE,  | 
                    |
| 1406 | 
                        - service=json.dumps(service),  | 
                    |
| 1407 | 
                        - )  | 
                    |
| 1408 | 
                        - else:  | 
                    |
| 1409 | 
                        - w_msg = _msg.TranslatedString(  | 
                    |
| 1410 | 
                        - _msg.WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE  | 
                    |
| 1411 | 
                        - )  | 
                    |
| 1412 | 
                        -                        logger.warning(w_msg, extra={'color': ctx.color})
                       | 
                    |
| 1413 | 
                        - if not view.maps[0] and not unset_settings and not edit_notes:  | 
                    |
| 1414 | 
                        - err_msg = _msg.TranslatedString(  | 
                    |
| 1415 | 
                        - _msg.ErrMsgTemplate.CANNOT_UPDATE_SETTINGS_NO_SETTINGS,  | 
                    |
| 1416 | 
                        - settings_type=_msg.TranslatedString(  | 
                    |
| 1417 | 
                        - _msg.Label.CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_SERVICE  | 
                    |
| 1418 | 
                        - if service  | 
                    |
| 1419 | 
                        - else _msg.Label.CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_GLOBAL # noqa: E501  | 
                    |
| 1619 | 
                        +@click.option(  | 
                    |
| 1620 | 
                        + '-c',  | 
                    |
| 1621 | 
                        + '--config',  | 
                    |
| 1622 | 
                        + 'store_config_only',  | 
                    |
| 1623 | 
                        + is_flag=True,  | 
                    |
| 1624 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1625 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_CONFIG_HELP_TEXT,  | 
                    |
| 1626 | 
                        + service_metavar=_msg.TranslatedString(  | 
                    |
| 1627 | 
                        + _msg.Label.VAULT_METAVAR_SERVICE  | 
                    |
| 1420 | 1628 | 
                        ),  | 
                    
| 1421 | 
                        - )  | 
                    |
| 1422 | 
                        - raise click.UsageError(str(err_msg))  | 
                    |
| 1423 | 
                        - for setting in unset_settings:  | 
                    |
| 1424 | 
                        - if setting in view.maps[0]:  | 
                    |
| 1425 | 
                        - err_msg = _msg.TranslatedString(  | 
                    |
| 1426 | 
                        - _msg.ErrMsgTemplate.SET_AND_UNSET_SAME_SETTING,  | 
                    |
| 1427 | 
                        - setting=setting,  | 
                    |
| 1428 | 
                        - )  | 
                    |
| 1429 | 
                        - raise click.UsageError(str(err_msg))  | 
                    |
| 1430 | 
                        - if not cli_helpers.is_completable_item(service):  | 
                    |
| 1431 | 
                        - logger.warning(  | 
                    |
| 1432 | 
                        - _msg.TranslatedString(  | 
                    |
| 1433 | 
                        - _msg.WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE,  | 
                    |
| 1434 | 
                        - service=service,  | 
                    |
| 1435 | 1629 | 
                        ),  | 
                    
| 1436 | 
                        -                        extra={'color': ctx.color},
                       | 
                    |
| 1437 | 
                        - )  | 
                    |
| 1438 | 
                        - subtree: dict[str, Any] = (  | 
                    |
| 1439 | 
                        -                    configuration['services'].setdefault(service, {})  # type: ignore[assignment]
                       | 
                    |
| 1440 | 
                        - if service  | 
                    |
| 1441 | 
                        -                    else configuration.setdefault('global', {})
                       | 
                    |
| 1442 | 
                        - )  | 
                    |
| 1443 | 
                        - if overwrite_config:  | 
                    |
| 1444 | 
                        - subtree.clear()  | 
                    |
| 1445 | 
                        - else:  | 
                    |
| 1446 | 
                        - for setting in unset_settings:  | 
                    |
| 1447 | 
                        - subtree.pop(setting, None)  | 
                    |
| 1448 | 
                        - subtree.update(view)  | 
                    |
| 1449 | 
                        - assert _types.is_vault_config(configuration), (  | 
                    |
| 1450 | 
                        -                    f'Invalid vault configuration: {configuration!r}'
                       | 
                    |
| 1451 | 
                        - )  | 
                    |
| 1452 | 
                        - if edit_notes:  | 
                    |
| 1453 | 
                        - assert service is not None  | 
                    |
| 1454 | 
                        - notes_instructions = _msg.TranslatedString(  | 
                    |
| 1455 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT  | 
                    |
| 1456 | 
                        - )  | 
                    |
| 1457 | 
                        - notes_marker = _msg.TranslatedString(  | 
                    |
| 1458 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER  | 
                    |
| 1459 | 
                        - )  | 
                    |
| 1460 | 
                        - notes_legacy_instructions = _msg.TranslatedString(  | 
                    |
| 1461 | 
                        - _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT  | 
                    |
| 1630 | 
                        + cls=cli_machinery.ConfigurationOption,  | 
                    |
| 1462 | 1631 | 
                        )  | 
                    
| 1463 | 
                        -                    old_notes_value = subtree.get('notes', '')
                       | 
                    |
| 1464 | 
                        - if modern_editor_interface:  | 
                    |
| 1465 | 
                        - text = '\n'.join([  | 
                    |
| 1466 | 
                        - str(notes_instructions),  | 
                    |
| 1467 | 
                        - str(notes_marker),  | 
                    |
| 1468 | 
                        - old_notes_value,  | 
                    |
| 1469 | 
                        - ])  | 
                    |
| 1470 | 
                        - else:  | 
                    |
| 1471 | 
                        - text = old_notes_value or str(  | 
                    |
| 1472 | 
                        - notes_legacy_instructions  | 
                    |
| 1632 | 
                        +@click.option(  | 
                    |
| 1633 | 
                        + '-x',  | 
                    |
| 1634 | 
                        + '--delete',  | 
                    |
| 1635 | 
                        + 'delete_service_settings',  | 
                    |
| 1636 | 
                        + is_flag=True,  | 
                    |
| 1637 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1638 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_HELP_TEXT,  | 
                    |
| 1639 | 
                        + service_metavar=_msg.TranslatedString(  | 
                    |
| 1640 | 
                        + _msg.Label.VAULT_METAVAR_SERVICE  | 
                    |
| 1641 | 
                        + ),  | 
                    |
| 1642 | 
                        + ),  | 
                    |
| 1643 | 
                        + cls=cli_machinery.ConfigurationOption,  | 
                    |
| 1473 | 1644 | 
                        )  | 
                    
| 1474 | 
                        - notes_value = click.edit(text=text, require_save=False)  | 
                    |
| 1475 | 
                        - assert notes_value is not None  | 
                    |
| 1476 | 
                        - if (  | 
                    |
| 1477 | 
                        - not modern_editor_interface  | 
                    |
| 1478 | 
                        - and notes_value.strip() != old_notes_value.strip()  | 
                    |
| 1479 | 
                        - ):  | 
                    |
| 1480 | 
                        - backup_file = cli_helpers.config_filename(  | 
                    |
| 1481 | 
                        - subsystem='notes backup'  | 
                    |
| 1645 | 
                        +@click.option(  | 
                    |
| 1646 | 
                        + '--delete-globals',  | 
                    |
| 1647 | 
                        + is_flag=True,  | 
                    |
| 1648 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1649 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_GLOBALS_HELP_TEXT,  | 
                    |
| 1650 | 
                        + ),  | 
                    |
| 1651 | 
                        + cls=cli_machinery.ConfigurationOption,  | 
                    |
| 1482 | 1652 | 
                        )  | 
                    
| 1483 | 
                        - backup_file.write_text(  | 
                    |
| 1484 | 
                        - old_notes_value, encoding='UTF-8'  | 
                    |
| 1653 | 
                        +@click.option(  | 
                    |
| 1654 | 
                        + '-X',  | 
                    |
| 1655 | 
                        + '--clear',  | 
                    |
| 1656 | 
                        + 'clear_all_settings',  | 
                    |
| 1657 | 
                        + is_flag=True,  | 
                    |
| 1658 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1659 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_ALL_HELP_TEXT,  | 
                    |
| 1660 | 
                        + ),  | 
                    |
| 1661 | 
                        + cls=cli_machinery.ConfigurationOption,  | 
                    |
| 1485 | 1662 | 
                        )  | 
                    
| 1486 | 
                        - logger.warning(  | 
                    |
| 1487 | 
                        - _msg.TranslatedString(  | 
                    |
| 1488 | 
                        - _msg.WarnMsgTemplate.LEGACY_EDITOR_INTERFACE_NOTES_BACKUP,  | 
                    |
| 1489 | 
                        - filename=str(backup_file),  | 
                    |
| 1663 | 
                        +@click.option(  | 
                    |
| 1664 | 
                        + '-e',  | 
                    |
| 1665 | 
                        + '--export',  | 
                    |
| 1666 | 
                        + 'export_settings',  | 
                    |
| 1667 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1668 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1490 | 1669 | 
                        ),  | 
                    
| 1491 | 
                        -                            extra={'color': ctx.color},
                       | 
                    |
| 1670 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1671 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_EXPORT_HELP_TEXT,  | 
                    |
| 1672 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1673 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1674 | 
                        + ),  | 
                    |
| 1675 | 
                        + ),  | 
                    |
| 1676 | 
                        + cls=cli_machinery.StorageManagementOption,  | 
                    |
| 1677 | 
                        + shell_complete=cli_helpers.shell_complete_path,  | 
                    |
| 1492 | 1678 | 
                        )  | 
                    
| 1493 | 
                        - subtree['notes'] = notes_value.strip()  | 
                    |
| 1494 | 
                        - elif (  | 
                    |
| 1495 | 
                        - modern_editor_interface  | 
                    |
| 1496 | 
                        - and notes_value.strip() != text.strip()  | 
                    |
| 1497 | 
                        - ):  | 
                    |
| 1498 | 
                        - notes_lines = collections.deque(  | 
                    |
| 1499 | 
                        - notes_value.splitlines(True) # noqa: FBT003  | 
                    |
| 1679 | 
                        +@click.option(  | 
                    |
| 1680 | 
                        + '-i',  | 
                    |
| 1681 | 
                        + '--import',  | 
                    |
| 1682 | 
                        + 'import_settings',  | 
                    |
| 1683 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1684 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1685 | 
                        + ),  | 
                    |
| 1686 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1687 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_IMPORT_HELP_TEXT,  | 
                    |
| 1688 | 
                        + metavar=_msg.TranslatedString(  | 
                    |
| 1689 | 
                        + _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER  | 
                    |
| 1690 | 
                        + ),  | 
                    |
| 1691 | 
                        + ),  | 
                    |
| 1692 | 
                        + cls=cli_machinery.StorageManagementOption,  | 
                    |
| 1693 | 
                        + shell_complete=cli_helpers.shell_complete_path,  | 
                    |
| 1500 | 1694 | 
                        )  | 
                    
| 1501 | 
                        - while notes_lines:  | 
                    |
| 1502 | 
                        - line = notes_lines.popleft()  | 
                    |
| 1503 | 
                        - if line.startswith(str(notes_marker)):  | 
                    |
| 1504 | 
                        - notes_value = ''.join(notes_lines)  | 
                    |
| 1505 | 
                        - break  | 
                    |
| 1506 | 
                        - else:  | 
                    |
| 1507 | 
                        - if not notes_value.strip():  | 
                    |
| 1508 | 
                        - err(  | 
                    |
| 1509 | 
                        - _msg.TranslatedString(  | 
                    |
| 1510 | 
                        - _msg.ErrMsgTemplate.USER_ABORTED_EDIT  | 
                    |
| 1695 | 
                        +@click.option(  | 
                    |
| 1696 | 
                        + '--overwrite-existing/--merge-existing',  | 
                    |
| 1697 | 
                        + 'overwrite_config',  | 
                    |
| 1698 | 
                        + default=False,  | 
                    |
| 1699 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1700 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_OVERWRITE_HELP_TEXT  | 
                    |
| 1701 | 
                        + ),  | 
                    |
| 1702 | 
                        + cls=cli_machinery.CompatibilityOption,  | 
                    |
| 1511 | 1703 | 
                        )  | 
                    
| 1704 | 
                        +@click.option(  | 
                    |
| 1705 | 
                        + '--unset',  | 
                    |
| 1706 | 
                        + 'unset_settings',  | 
                    |
| 1707 | 
                        + multiple=True,  | 
                    |
| 1708 | 
                        + type=click.Choice([  | 
                    |
| 1709 | 
                        + 'phrase',  | 
                    |
| 1710 | 
                        + 'key',  | 
                    |
| 1711 | 
                        + 'length',  | 
                    |
| 1712 | 
                        + 'repeat',  | 
                    |
| 1713 | 
                        + 'lower',  | 
                    |
| 1714 | 
                        + 'upper',  | 
                    |
| 1715 | 
                        + 'number',  | 
                    |
| 1716 | 
                        + 'space',  | 
                    |
| 1717 | 
                        + 'dash',  | 
                    |
| 1718 | 
                        + 'symbol',  | 
                    |
| 1719 | 
                        + 'notes',  | 
                    |
| 1720 | 
                        + ]),  | 
                    |
| 1721 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1722 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_UNSET_HELP_TEXT  | 
                    |
| 1723 | 
                        + ),  | 
                    |
| 1724 | 
                        + cls=cli_machinery.CompatibilityOption,  | 
                    |
| 1512 | 1725 | 
                        )  | 
                    
| 1513 | 
                        - subtree['notes'] = notes_value.strip()  | 
                    |
| 1514 | 
                        - put_config(configuration)  | 
                    |
| 1515 | 
                        - else:  | 
                    |
| 1516 | 
                        - assert service is not None  | 
                    |
| 1517 | 
                        -                kwargs: dict[str, Any] = {
                       | 
                    |
| 1518 | 
                        - k: v  | 
                    |
| 1519 | 
                        - for k, v in settings.items()  | 
                    |
| 1520 | 
                        - if k in service_keys and v is not None  | 
                    |
| 1521 | 
                        - }  | 
                    |
| 1522 | 
                        - if use_phrase:  | 
                    |
| 1523 | 
                        - try:  | 
                    |
| 1524 | 
                        - cli_helpers.check_for_misleading_passphrase(  | 
                    |
| 1525 | 
                        - cli_helpers.ORIGIN.INTERACTIVE,  | 
                    |
| 1526 | 
                        -                            {'phrase': phrase},
                       | 
                    |
| 1527 | 
                        - main_config=user_config,  | 
                    |
| 1528 | 
                        - ctx=ctx,  | 
                    |
| 1726 | 
                        +@click.option(  | 
                    |
| 1727 | 
                        + '--export-as',  | 
                    |
| 1728 | 
                        + type=click.Choice(['json', 'sh']),  | 
                    |
| 1729 | 
                        + default='json',  | 
                    |
| 1730 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1731 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_EXPORT_AS_HELP_TEXT  | 
                    |
| 1732 | 
                        + ),  | 
                    |
| 1733 | 
                        + cls=cli_machinery.CompatibilityOption,  | 
                    |
| 1529 | 1734 | 
                        )  | 
                    
| 1530 | 
                        - except AssertionError as exc:  | 
                    |
| 1531 | 
                        - err(  | 
                    |
| 1532 | 
                        - _msg.TranslatedString(  | 
                    |
| 1533 | 
                        - _msg.ErrMsgTemplate.INVALID_USER_CONFIG,  | 
                    |
| 1534 | 
                        - error=exc,  | 
                    |
| 1535 | 
                        - filename=None,  | 
                    |
| 1536 | 
                        - ).maybe_without_filename(),  | 
                    |
| 1735 | 
                        +@click.option(  | 
                    |
| 1736 | 
                        + '--modern-editor-interface/--vault-legacy-editor-interface',  | 
                    |
| 1737 | 
                        + 'modern_editor_interface',  | 
                    |
| 1738 | 
                        + default=False,  | 
                    |
| 1739 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1740 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT  | 
                    |
| 1741 | 
                        + ),  | 
                    |
| 1742 | 
                        + cls=cli_machinery.CompatibilityOption,  | 
                    |
| 1537 | 1743 | 
                        )  | 
                    
| 1538 | 
                        - # If either --key or --phrase are given, use that setting.  | 
                    |
| 1539 | 
                        - # Otherwise, if both key and phrase are set in the config,  | 
                    |
| 1540 | 
                        - # use the key. Otherwise, if only one of key and phrase is  | 
                    |
| 1541 | 
                        - # set in the config, use that one. In all these above  | 
                    |
| 1542 | 
                        - # cases, set the phrase via vault.Vault.phrase_from_key if  | 
                    |
| 1543 | 
                        - # a key is given. Finally, if nothing is set, error out.  | 
                    |
| 1544 | 
                        - if use_key or use_phrase:  | 
                    |
| 1545 | 
                        - kwargs['phrase'] = (  | 
                    |
| 1546 | 
                        - cli_helpers.key_to_phrase(key, error_callback=err)  | 
                    |
| 1547 | 
                        - if use_key  | 
                    |
| 1548 | 
                        - else phrase  | 
                    |
| 1549 | 
                        - )  | 
                    |
| 1550 | 
                        -                elif kwargs.get('key'):
                       | 
                    |
| 1551 | 
                        - kwargs['phrase'] = cli_helpers.key_to_phrase(  | 
                    |
| 1552 | 
                        - kwargs['key'], error_callback=err  | 
                    |
| 1553 | 
                        - )  | 
                    |
| 1554 | 
                        -                elif kwargs.get('phrase'):
                       | 
                    |
| 1555 | 
                        - pass  | 
                    |
| 1556 | 
                        - else:  | 
                    |
| 1557 | 
                        - err_msg = _msg.TranslatedString(  | 
                    |
| 1558 | 
                        - _msg.ErrMsgTemplate.NO_KEY_OR_PHRASE  | 
                    |
| 1744 | 
                        +@click.option(  | 
                    |
| 1745 | 
                        + '--print-notes-before/--print-notes-after',  | 
                    |
| 1746 | 
                        + 'print_notes_before',  | 
                    |
| 1747 | 
                        + default=False,  | 
                    |
| 1748 | 
                        + help=_msg.TranslatedString(  | 
                    |
| 1749 | 
                        + _msg.Label.DERIVEPASSPHRASE_VAULT_PRINT_NOTES_BEFORE_HELP_TEXT  | 
                    |
| 1750 | 
                        + ),  | 
                    |
| 1751 | 
                        + cls=cli_machinery.CompatibilityOption,  | 
                    |
| 1559 | 1752 | 
                        )  | 
                    
| 1560 | 
                        - raise click.UsageError(str(err_msg))  | 
                    |
| 1561 | 
                        -                kwargs.pop('key', '')
                       | 
                    |
| 1562 | 
                        -                service_notes = settings.get('notes', '').strip()
                       | 
                    |
| 1563 | 
                        - result = vault.Vault(**kwargs).generate(service)  | 
                    |
| 1564 | 
                        - if print_notes_before and service_notes.strip():  | 
                    |
| 1565 | 
                        -                    click.echo(f'{service_notes}\n', err=True, color=ctx.color)
                       | 
                    |
| 1566 | 
                        -                click.echo(result.decode('ASCII'), color=ctx.color)
                       | 
                    |
| 1567 | 
                        - if not print_notes_before and service_notes.strip():  | 
                    |
| 1568 | 
                        - click.echo(  | 
                    |
| 1569 | 
                        -                        f'\n{service_notes}\n', err=True, color=ctx.color
                       | 
                    |
| 1753 | 
                        +@cli_machinery.version_option(cli_machinery.vault_version_option_callback)  | 
                    |
| 1754 | 
                        +@cli_machinery.color_forcing_pseudo_option  | 
                    |
| 1755 | 
                        +@cli_machinery.standard_logging_options  | 
                    |
| 1756 | 
                        +@click.argument(  | 
                    |
| 1757 | 
                        + 'service',  | 
                    |
| 1758 | 
                        + metavar=_msg.TranslatedString(_msg.Label.VAULT_METAVAR_SERVICE),  | 
                    |
| 1759 | 
                        + required=False,  | 
                    |
| 1760 | 
                        + default=None,  | 
                    |
| 1761 | 
                        + shell_complete=cli_helpers.shell_complete_service,  | 
                    |
| 1570 | 1762 | 
                        )  | 
                    
| 1763 | 
                        +@click.pass_context  | 
                    |
| 1764 | 
                        +def derivepassphrase_vault(  | 
                    |
| 1765 | 
                        + ctx: click.Context,  | 
                    |
| 1766 | 
                        + /,  | 
                    |
| 1767 | 
                        + **_kwargs: Any, # noqa: ANN401  | 
                    |
| 1768 | 
                        +) -> None:  | 
                    |
| 1769 | 
                        + """Derive a passphrase using the vault(1) derivation scheme.  | 
                    |
| 1770 | 
                        +  | 
                    |
| 1771 | 
                        + This is a [`click`][CLICK]-powered command-line interface function,  | 
                    |
| 1772 | 
                        + and not intended for programmatic use. See the  | 
                    |
| 1773 | 
                        + derivepassphrase-vault(1) manpage for full documentation of the  | 
                    |
| 1774 | 
                        + interface. (See also [`click.testing.CliRunner`][] for controlled,  | 
                    |
| 1775 | 
                        + programmatic invocation.)  | 
                    |
| 1776 | 
                        +  | 
                    |
| 1777 | 
                        + [CLICK]: https://pypi.org/package/click/  | 
                    |
| 1778 | 
                        +  | 
                    |
| 1779 | 
                        + Parameters:  | 
                    |
| 1780 | 
                        + ctx (click.Context):  | 
                    |
| 1781 | 
                        + The `click` context.  | 
                    |
| 1782 | 
                        +  | 
                    |
| 1783 | 
                        + Other Parameters:  | 
                    |
| 1784 | 
                        + service:  | 
                    |
| 1785 | 
                        + A service name. Required, unless operating on global  | 
                    |
| 1786 | 
                        + settings or importing/exporting settings.  | 
                    |
| 1787 | 
                        + use_phrase:  | 
                    |
| 1788 | 
                        + Command-line argument `-p`/`--phrase`. If given, query the  | 
                    |
| 1789 | 
                        + user for a passphrase instead of an SSH key.  | 
                    |
| 1790 | 
                        + use_key:  | 
                    |
| 1791 | 
                        + Command-line argument `-k`/`--key`. If given, query the  | 
                    |
| 1792 | 
                        + user for an SSH key instead of a passphrase.  | 
                    |
| 1793 | 
                        + length:  | 
                    |
| 1794 | 
                        + Command-line argument `-l`/`--length`. Override the default  | 
                    |
| 1795 | 
                        + length of the generated passphrase.  | 
                    |
| 1796 | 
                        + repeat:  | 
                    |
| 1797 | 
                        + Command-line argument `-r`/`--repeat`. Override the default  | 
                    |
| 1798 | 
                        + repetition limit if positive, or disable the repetition  | 
                    |
| 1799 | 
                        + limit if 0.  | 
                    |
| 1800 | 
                        + lower:  | 
                    |
| 1801 | 
                        + Command-line argument `--lower`. Require a given amount of  | 
                    |
| 1802 | 
                        + ASCII lowercase characters if positive, else forbid ASCII  | 
                    |
| 1803 | 
                        + lowercase characters if 0.  | 
                    |
| 1804 | 
                        + upper:  | 
                    |
| 1805 | 
                        + Command-line argument `--upper`. Same as `lower`, but for  | 
                    |
| 1806 | 
                        + ASCII uppercase characters.  | 
                    |
| 1807 | 
                        + number:  | 
                    |
| 1808 | 
                        + Command-line argument `--number`. Same as `lower`, but for  | 
                    |
| 1809 | 
                        + ASCII digits.  | 
                    |
| 1810 | 
                        + space:  | 
                    |
| 1811 | 
                        + Command-line argument `--space`. Same as `lower`, but for  | 
                    |
| 1812 | 
                        + the space character.  | 
                    |
| 1813 | 
                        + dash:  | 
                    |
| 1814 | 
                        + Command-line argument `--dash`. Same as `lower`, but for  | 
                    |
| 1815 | 
                        + the hyphen-minus and underscore characters.  | 
                    |
| 1816 | 
                        + symbol:  | 
                    |
| 1817 | 
                        + Command-line argument `--symbol`. Same as `lower`, but for  | 
                    |
| 1818 | 
                        + all other ASCII printable characters except lowercase  | 
                    |
| 1819 | 
                        + characters, uppercase characters, digits, space and  | 
                    |
| 1820 | 
                        + backquote.  | 
                    |
| 1821 | 
                        + edit_notes:  | 
                    |
| 1822 | 
                        + Command-line argument `-n`/`--notes`. If given, spawn an  | 
                    |
| 1823 | 
                        + editor to edit notes for `service`.  | 
                    |
| 1824 | 
                        + store_config_only:  | 
                    |
| 1825 | 
                        + Command-line argument `-c`/`--config`. If given, saves the  | 
                    |
| 1826 | 
                        + other given settings (`--key`, ..., `--symbol`) to the  | 
                    |
| 1827 | 
                        + configuration file, either specifically for `service` or as  | 
                    |
| 1828 | 
                        + global settings.  | 
                    |
| 1829 | 
                        + delete_service_settings:  | 
                    |
| 1830 | 
                        + Command-line argument `-x`/`--delete`. If given, removes  | 
                    |
| 1831 | 
                        + the settings for `service` from the configuration file.  | 
                    |
| 1832 | 
                        + delete_globals:  | 
                    |
| 1833 | 
                        + Command-line argument `--delete-globals`. If given, removes  | 
                    |
| 1834 | 
                        + the global settings from the configuration file.  | 
                    |
| 1835 | 
                        + clear_all_settings:  | 
                    |
| 1836 | 
                        + Command-line argument `-X`/`--clear`. If given, removes all  | 
                    |
| 1837 | 
                        + settings from the configuration file.  | 
                    |
| 1838 | 
                        + export_settings:  | 
                    |
| 1839 | 
                        + Command-line argument `-e`/`--export`. If a file object,  | 
                    |
| 1840 | 
                        + then it must be open for writing and accept `str` inputs.  | 
                    |
| 1841 | 
                        + Otherwise, a filename to open for writing. Using `-` for  | 
                    |
| 1842 | 
                        + standard output is supported.  | 
                    |
| 1843 | 
                        + import_settings:  | 
                    |
| 1844 | 
                        + Command-line argument `-i`/`--import`. If a file object, it  | 
                    |
| 1845 | 
                        + must be open for reading and yield `str` values. Otherwise,  | 
                    |
| 1846 | 
                        + a filename to open for reading. Using `-` for standard  | 
                    |
| 1847 | 
                        + input is supported.  | 
                    |
| 1848 | 
                        + overwrite_config:  | 
                    |
| 1849 | 
                        + Command-line arguments `--overwrite-existing` (True) and  | 
                    |
| 1850 | 
                        + `--merge-existing` (False). Controls whether config saving  | 
                    |
| 1851 | 
                        + and config importing overwrite existing configurations, or  | 
                    |
| 1852 | 
                        + merge them section-wise instead.  | 
                    |
| 1853 | 
                        + unset_settings:  | 
                    |
| 1854 | 
                        + Command-line argument `--unset`. If given together with  | 
                    |
| 1855 | 
                        + `--config`, unsets the specified settings (in addition to  | 
                    |
| 1856 | 
                        + any other changes requested).  | 
                    |
| 1857 | 
                        + export_as:  | 
                    |
| 1858 | 
                        + Command-line argument `--export-as`. If given together with  | 
                    |
| 1859 | 
                        + `--export`, selects the format to export the current  | 
                    |
| 1860 | 
                        +            configuration as: JSON ("json", default) or POSIX sh ("sh").
                       | 
                    |
| 1861 | 
                        + modern_editor_interface:  | 
                    |
| 1862 | 
                        + Command-line arguments `--modern-editor-interface` (True)  | 
                    |
| 1863 | 
                        + and `--vault-legacy-editor-interface` (False). Controls  | 
                    |
| 1864 | 
                        + whether editing notes uses a modern editor interface  | 
                    |
| 1865 | 
                        + (supporting comments and aborting) or a vault(1)-compatible  | 
                    |
| 1866 | 
                        + legacy editor interface (WYSIWYG notes contents).  | 
                    |
| 1867 | 
                        + print_notes_before:  | 
                    |
| 1868 | 
                        + Command-line arguments `--print-notes-before` (True) and  | 
                    |
| 1869 | 
                        + `--print-notes-after` (False). Controls whether the service  | 
                    |
| 1870 | 
                        + notes (if any) are printed before the passphrase, or after.  | 
                    |
| 1871 | 
                        +  | 
                    |
| 1872 | 
                        + """  | 
                    |
| 1873 | 
                        + vault_context = _VaultContext(ctx)  | 
                    |
| 1874 | 
                        + vault_context.validate_command_line()  | 
                    |
| 1875 | 
                        + vault_context.dispatch_op()  | 
                    |
| 1571 | 1876 | 
                         | 
                    
| 1572 | 1877 | 
                         | 
                    
| 1573 | 1878 | 
                        if __name__ == '__main__':  | 
                    
| ... | ... | 
                      @@ -4290,6 +4290,40 @@ class TestCLI:  | 
                  
| 4290 | 4290 | 
                        'expected error exit and known error message'  | 
                    
| 4291 | 4291 | 
                        )  | 
                    
| 4292 | 4292 | 
                         | 
                    
| 4293 | 
                        + def test_311_bad_user_config_is_a_directory(  | 
                    |
| 4294 | 
                        + self,  | 
                    |
| 4295 | 
                        + ) -> None:  | 
                    |
| 4296 | 
                        + """Loading a user configuration file in an invalid format fails."""  | 
                    |
| 4297 | 
                        + runner = click.testing.CliRunner(mix_stderr=False)  | 
                    |
| 4298 | 
                        + # TODO(the-13th-letter): Rewrite using parenthesized  | 
                    |
| 4299 | 
                        + # with-statements.  | 
                    |
| 4300 | 
                        + # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9  | 
                    |
| 4301 | 
                        + with contextlib.ExitStack() as stack:  | 
                    |
| 4302 | 
                        + monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())  | 
                    |
| 4303 | 
                        + stack.enter_context(  | 
                    |
| 4304 | 
                        + tests.isolated_vault_config(  | 
                    |
| 4305 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 4306 | 
                        + runner=runner,  | 
                    |
| 4307 | 
                        +                    vault_config={'services': {}},
                       | 
                    |
| 4308 | 
                        + main_config_str='',  | 
                    |
| 4309 | 
                        + )  | 
                    |
| 4310 | 
                        + )  | 
                    |
| 4311 | 
                        + user_config = cli_helpers.config_filename(  | 
                    |
| 4312 | 
                        + subsystem='user configuration'  | 
                    |
| 4313 | 
                        + )  | 
                    |
| 4314 | 
                        + user_config.unlink()  | 
                    |
| 4315 | 
                        + user_config.mkdir(parents=True, exist_ok=True)  | 
                    |
| 4316 | 
                        + result_ = runner.invoke(  | 
                    |
| 4317 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 4318 | 
                        + ['--phrase', '--', DUMMY_SERVICE],  | 
                    |
| 4319 | 
                        + input=DUMMY_PASSPHRASE,  | 
                    |
| 4320 | 
                        + catch_exceptions=False,  | 
                    |
| 4321 | 
                        + )  | 
                    |
| 4322 | 
                        + result = tests.ReadableResult.parse(result_)  | 
                    |
| 4323 | 
                        + assert result.error_exit(error='Cannot load user config:'), (  | 
                    |
| 4324 | 
                        + 'expected error exit and known error message'  | 
                    |
| 4325 | 
                        + )  | 
                    |
| 4326 | 
                        +  | 
                    |
| 4293 | 4327 | 
                        def test_400_missing_af_unix_support(  | 
                    
| 4294 | 4328 | 
                        self,  | 
                    
| 4295 | 4329 | 
                        ) -> None:  | 
                    
| 4296 | 4330 |