Marco Ricci commited on 2024-12-30 23:45:01
Zeige 2 geänderte Dateien mit 1170 Einfügungen und 336 Löschungen.
We implement the translations in a deferred manner, via objects that stringify to their correct translation. Since `click` 8.1, help texts are (mostly) stored first and processed only when the help page is emitted. Accordingly, this system can be adapted to support our translatable strings: we need only ensure the help text is stringified just before it is formatted and emitted. This in turn can be achieved by reimplementing the help page assembly and formatting in a custom `click.Command` class, as has already been done to implement help/option group support. Once this is in place, it remains to exchange all inline help texts with the corresponding translatable string, and adapt the tests to the new messages. While most of the changes are straightforward, some subtleties remain. First, the help texts are now supplied as (separately translatable) paragraphs to the `click.command` decorator, instead of being extracted from the respective function docstring. The docstrings in return have been shortened down. Second, since the new capabilities are implemented via custom `click.Command`, `click.MultiCommand` and `click.Option` subclasses, every option and command must derive from one of these classes. This necessitates the creation of an empty `StandardOption` class for options without an option group, and making `_DefaultToVaultGroup` a subclass of `CommandWithHelpGroups`. Third, since the `click.Option` class actually processes its help text eagerly, the subclass must hide the (non-string) help text from the `click.Option` constructor and later restore it. Finally, while the tests currently pass as before, they now implicitly rely on *not* loading any translations. Furthermore, there is no mechanism in place with which to isolate the translation system from the user's environment for testing purposes, nor is there a (controlled) mechanism to test `derivepassphrase` under a different language. Once there are actual translations, future commits will need to establish such a missing mechanism for translation loading/switching/forcing.
... | ... |
@@ -41,6 +41,7 @@ from typing_extensions import ( |
41 | 41 |
) |
42 | 42 |
|
43 | 43 |
import derivepassphrase as dpp |
44 |
+from derivepassphrase import _cli_msg as _msg |
|
44 | 45 |
from derivepassphrase import _types, exporter, ssh_agent, vault |
45 | 46 |
|
46 | 47 |
if sys.version_info >= (3, 11): |
... | ... |
@@ -63,14 +64,17 @@ __version__ = dpp.__version__ |
63 | 64 |
|
64 | 65 |
__all__ = ('derivepassphrase',) |
65 | 66 |
|
66 |
-PROG_NAME = 'derivepassphrase' |
|
67 |
+PROG_NAME = _msg.PROG_NAME |
|
67 | 68 |
KEY_DISPLAY_LENGTH = 50 |
68 | 69 |
|
69 | 70 |
# Error messages |
70 | 71 |
_INVALID_VAULT_CONFIG = 'Invalid vault config' |
71 | 72 |
_AGENT_COMMUNICATION_ERROR = 'Error communicating with the SSH agent' |
72 |
-_NO_USABLE_KEYS = 'No usable SSH keys were found' |
|
73 |
+_NO_SUITABLE_KEYS = 'No suitable SSH keys were found' |
|
73 | 74 |
_EMPTY_SELECTION = 'Empty selection' |
75 |
+_NOT_AN_INTEGER = 'not an integer' |
|
76 |
+_NOT_A_NONNEGATIVE_INTEGER = 'not a non-negative integer' |
|
77 |
+_NOT_A_POSITIVE_INTEGER = 'not a positive integer' |
|
74 | 78 |
|
75 | 79 |
|
76 | 80 |
# Logging |
... | ... |
@@ -181,10 +185,15 @@ class CLIofPackageFormatter(logging.Formatter): |
181 | 185 |
else: # pragma: no cover |
182 | 186 |
msg = f'Unsupported logging level: {record.levelname}' |
183 | 187 |
raise AssertionError(msg) |
184 |
- return ''.join( |
|
188 |
+ parts = [ |
|
189 |
+ ''.join( |
|
185 | 190 |
prefix + level_indicator + line |
186 | 191 |
for line in preliminary_result.splitlines(True) # noqa: FBT003 |
187 | 192 |
) |
193 |
+ ] |
|
194 |
+ if record.exc_info: |
|
195 |
+ parts.append(self.formatException(record.exc_info) + '\n') |
|
196 |
+ return ''.join(parts) |
|
188 | 197 |
|
189 | 198 |
|
190 | 199 |
class StandardCLILogging: |
... | ... |
@@ -393,27 +402,254 @@ class OptionGroupOption(click.Option): |
393 | 402 |
|
394 | 403 |
""" |
395 | 404 |
|
396 |
- option_group_name: str = '' |
|
405 |
+ option_group_name: object = '' |
|
397 | 406 |
"""""" |
398 |
- epilog: str = '' |
|
407 |
+ epilog: object = '' |
|
399 | 408 |
"""""" |
400 | 409 |
|
401 | 410 |
def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 |
402 | 411 |
if self.__class__ == __class__: # type: ignore[name-defined] |
403 | 412 |
raise NotImplementedError |
413 |
+ # Though click 8.1 mostly defers help text processing until the |
|
414 |
+ # `BaseCommand.format_*` methods are called, the Option |
|
415 |
+ # constructor still preprocesses the help text, and asserts that |
|
416 |
+ # the help text is a string. Work around this by removing the |
|
417 |
+ # help text from the constructor arguments and re-adding it, |
|
418 |
+ # unprocessed, after constructor finishes. |
|
419 |
+ unset = object() |
|
420 |
+ help = kwargs.pop('help', unset) # noqa: A001 |
|
404 | 421 |
super().__init__(*args, **kwargs) |
422 |
+ if help is not unset: # pragma: no branch |
|
423 |
+ self.help = help |
|
405 | 424 |
|
406 | 425 |
|
407 | 426 |
class CommandWithHelpGroups(click.Command): |
408 |
- """A [`click.Command`][] with support for help/option groups. |
|
427 |
+ """A [`click.Command`][] with support for some help text customizations. |
|
428 |
+ |
|
429 |
+ Supports help/option groups, group epilogs, and help text objects |
|
430 |
+ (objects that stringify to help texts). The latter is primarily |
|
431 |
+ used to implement translations. |
|
409 | 432 |
|
410 |
- Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and |
|
411 |
- further modified to support group epilogs. |
|
433 |
+ Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE] for |
|
434 |
+ help/option group support, and further modified to include group |
|
435 |
+ epilogs and help text objects. |
|
412 | 436 |
|
413 | 437 |
[CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746 |
414 | 438 |
|
415 | 439 |
""" |
416 | 440 |
|
441 |
+ @staticmethod |
|
442 |
+ def _text(text: object, /) -> str: |
|
443 |
+ if isinstance(text, (list, tuple)): |
|
444 |
+ return '\n\n'.join(str(x) for x in text) |
|
445 |
+ return str(text) |
|
446 |
+ |
|
447 |
+ def collect_usage_pieces(self, ctx: click.Context) -> list[str]: |
|
448 |
+ """Return the pieces for the usage string. |
|
449 |
+ |
|
450 |
+ Based on code from click 8.1. Subject to the following license |
|
451 |
+ (3-clause BSD license): |
|
452 |
+ |
|
453 |
+ Copyright 2024 Pallets |
|
454 |
+ |
|
455 |
+ Redistribution and use in source and binary forms, with or |
|
456 |
+ without modification, are permitted provided that the |
|
457 |
+ following conditions are met: |
|
458 |
+ |
|
459 |
+ 1. Redistributions of source code must retain the above |
|
460 |
+ copyright notice, this list of conditions and the |
|
461 |
+ following disclaimer. |
|
462 |
+ |
|
463 |
+ 2. Redistributions in binary form must reproduce the above |
|
464 |
+ copyright notice, this list of conditions and the |
|
465 |
+ following disclaimer in the documentation and/or other |
|
466 |
+ materials provided with the distribution. |
|
467 |
+ |
|
468 |
+ 3. Neither the name of the copyright holder nor the names |
|
469 |
+ of its contributors may be used to endorse or promote |
|
470 |
+ products derived from this software without specific |
|
471 |
+ prior written permission. |
|
472 |
+ |
|
473 |
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
|
474 |
+ CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, |
|
475 |
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
|
476 |
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
477 |
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
|
478 |
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
479 |
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
|
480 |
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|
481 |
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
|
482 |
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
|
483 |
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR |
|
484 |
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
485 |
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
486 |
+ |
|
487 |
+ Modifications are marked with respective comments. They too are |
|
488 |
+ released under the same license above. The original code did |
|
489 |
+ not contain any "noqa" or "pragma" comments. |
|
490 |
+ |
|
491 |
+ Args: |
|
492 |
+ ctx: |
|
493 |
+ The click context. |
|
494 |
+ |
|
495 |
+ """ |
|
496 |
+ rv = [str(self.options_metavar)] if self.options_metavar else [] |
|
497 |
+ for param in self.get_params(ctx): |
|
498 |
+ rv.extend(str(x) for x in param.get_usage_pieces(ctx)) |
|
499 |
+ return rv |
|
500 |
+ |
|
501 |
+ def get_short_help_str( |
|
502 |
+ self, |
|
503 |
+ limit: int = 45, |
|
504 |
+ ) -> str: |
|
505 |
+ """Return the short help string for a command. |
|
506 |
+ |
|
507 |
+ If only a long help string is given, shorten it. |
|
508 |
+ |
|
509 |
+ Based on code from click 8.1. Subject to the following license |
|
510 |
+ (3-clause BSD license): |
|
511 |
+ |
|
512 |
+ Copyright 2024 Pallets |
|
513 |
+ |
|
514 |
+ Redistribution and use in source and binary forms, with or |
|
515 |
+ without modification, are permitted provided that the |
|
516 |
+ following conditions are met: |
|
517 |
+ |
|
518 |
+ 1. Redistributions of source code must retain the above |
|
519 |
+ copyright notice, this list of conditions and the |
|
520 |
+ following disclaimer. |
|
521 |
+ |
|
522 |
+ 2. Redistributions in binary form must reproduce the above |
|
523 |
+ copyright notice, this list of conditions and the |
|
524 |
+ following disclaimer in the documentation and/or other |
|
525 |
+ materials provided with the distribution. |
|
526 |
+ |
|
527 |
+ 3. Neither the name of the copyright holder nor the names |
|
528 |
+ of its contributors may be used to endorse or promote |
|
529 |
+ products derived from this software without specific |
|
530 |
+ prior written permission. |
|
531 |
+ |
|
532 |
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
|
533 |
+ CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, |
|
534 |
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
|
535 |
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
536 |
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
|
537 |
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
538 |
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
|
539 |
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|
540 |
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
|
541 |
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
|
542 |
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR |
|
543 |
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
544 |
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
545 |
+ |
|
546 |
+ Modifications are marked with respective comments. They too are |
|
547 |
+ released under the same license above. The original code did |
|
548 |
+ not contain any "noqa" or "pragma" comments. |
|
549 |
+ |
|
550 |
+ Args: |
|
551 |
+ limit: |
|
552 |
+ The maximum width of the short help string. |
|
553 |
+ |
|
554 |
+ """ |
|
555 |
+ # Modification against click 8.1: Call `_text()` on `self.help` |
|
556 |
+ # to allow help texts to be general objects, not just strings. |
|
557 |
+ # Used to implement translatable strings, as objects that |
|
558 |
+ # stringify to the translation. |
|
559 |
+ if self.short_help: # pragma: no cover |
|
560 |
+ text = inspect.cleandoc(self._text(self.short_help)) |
|
561 |
+ elif self.help: |
|
562 |
+ text = click.utils.make_default_short_help( |
|
563 |
+ self._text(self.help), limit |
|
564 |
+ ) |
|
565 |
+ else: # pragma: no cover |
|
566 |
+ text = '' |
|
567 |
+ if self.deprecated: # pragma: no cover |
|
568 |
+ # Modification against click 8.1: The translated string is |
|
569 |
+ # looked up in the derivepassphrase message domain, not the |
|
570 |
+ # gettext default domain. |
|
571 |
+ text = str( |
|
572 |
+ _msg.TranslatedString(_msg.Label.DEPRECATED_COMMAND_LABEL) |
|
573 |
+ ).format(text=text) |
|
574 |
+ return text.strip() |
|
575 |
+ |
|
576 |
+ def format_help_text( |
|
577 |
+ self, |
|
578 |
+ ctx: click.Context, |
|
579 |
+ formatter: click.HelpFormatter, |
|
580 |
+ ) -> None: |
|
581 |
+ """Format the help text prologue, if any. |
|
582 |
+ |
|
583 |
+ Based on code from click 8.1. Subject to the following license |
|
584 |
+ (3-clause BSD license): |
|
585 |
+ |
|
586 |
+ Copyright 2024 Pallets |
|
587 |
+ |
|
588 |
+ Redistribution and use in source and binary forms, with or |
|
589 |
+ without modification, are permitted provided that the |
|
590 |
+ following conditions are met: |
|
591 |
+ |
|
592 |
+ 1. Redistributions of source code must retain the above |
|
593 |
+ copyright notice, this list of conditions and the |
|
594 |
+ following disclaimer. |
|
595 |
+ |
|
596 |
+ 2. Redistributions in binary form must reproduce the above |
|
597 |
+ copyright notice, this list of conditions and the |
|
598 |
+ following disclaimer in the documentation and/or other |
|
599 |
+ materials provided with the distribution. |
|
600 |
+ |
|
601 |
+ 3. Neither the name of the copyright holder nor the names |
|
602 |
+ of its contributors may be used to endorse or promote |
|
603 |
+ products derived from this software without specific |
|
604 |
+ prior written permission. |
|
605 |
+ |
|
606 |
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
|
607 |
+ CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, |
|
608 |
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
|
609 |
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
610 |
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
|
611 |
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
612 |
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
|
613 |
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|
614 |
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
|
615 |
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
|
616 |
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR |
|
617 |
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
618 |
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
619 |
+ |
|
620 |
+ Modifications are marked with respective comments. They too are |
|
621 |
+ released under the same license above. The original code did |
|
622 |
+ not contain any "noqa" or "pragma" comments. |
|
623 |
+ |
|
624 |
+ Args: |
|
625 |
+ ctx: |
|
626 |
+ The click context. |
|
627 |
+ formatter: |
|
628 |
+ The formatter for the `--help` listing. |
|
629 |
+ |
|
630 |
+ """ |
|
631 |
+ del ctx |
|
632 |
+ # Modification against click 8.1: Call `_text()` on `self.help` |
|
633 |
+ # to allow help texts to be general objects, not just strings. |
|
634 |
+ # Used to implement translatable strings, as objects that |
|
635 |
+ # stringify to the translation. |
|
636 |
+ text = ( |
|
637 |
+ inspect.cleandoc(self._text(self.help).partition('\f')[0]) |
|
638 |
+ if self.help is not None |
|
639 |
+ else '' |
|
640 |
+ ) |
|
641 |
+ if self.deprecated: # pragma: no cover |
|
642 |
+ # Modification against click 8.1: The translated string is |
|
643 |
+ # looked up in the derivepassphrase message domain, not the |
|
644 |
+ # gettext default domain. |
|
645 |
+ text = str( |
|
646 |
+ _msg.TranslatedString(_msg.Label.DEPRECATED_COMMAND_LABEL) |
|
647 |
+ ).format(text=text) |
|
648 |
+ if text: # pragma: no branch |
|
649 |
+ formatter.write_paragraph() |
|
650 |
+ with formatter.indentation(): |
|
651 |
+ formatter.write_text(text) |
|
652 |
+ |
|
417 | 653 |
def format_options( |
418 | 654 |
self, |
419 | 655 |
ctx: click.Context, |
... | ... |
@@ -433,6 +669,48 @@ class CommandWithHelpGroups(click.Command): |
433 | 669 |
section heading is "Options" (or "Other options" if there are |
434 | 670 |
other option groups) and the epilog is empty. |
435 | 671 |
|
672 |
+ We unconditionally call [`format_commands`][], and rely on it to |
|
673 |
+ act as a no-op if we aren't actually a [`click.MultiCommand`][]. |
|
674 |
+ |
|
675 |
+ Based on code from click 8.1. Subject to the following license |
|
676 |
+ (3-clause BSD license): |
|
677 |
+ |
|
678 |
+ Copyright 2024 Pallets |
|
679 |
+ |
|
680 |
+ Redistribution and use in source and binary forms, with or |
|
681 |
+ without modification, are permitted provided that the |
|
682 |
+ following conditions are met: |
|
683 |
+ |
|
684 |
+ 1. Redistributions of source code must retain the above |
|
685 |
+ copyright notice, this list of conditions and the |
|
686 |
+ following disclaimer. |
|
687 |
+ |
|
688 |
+ 2. Redistributions in binary form must reproduce the above |
|
689 |
+ copyright notice, this list of conditions and the |
|
690 |
+ following disclaimer in the documentation and/or other |
|
691 |
+ materials provided with the distribution. |
|
692 |
+ |
|
693 |
+ 3. Neither the name of the copyright holder nor the names |
|
694 |
+ of its contributors may be used to endorse or promote |
|
695 |
+ products derived from this software without specific |
|
696 |
+ prior written permission. |
|
697 |
+ |
|
698 |
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
|
699 |
+ CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, |
|
700 |
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
|
701 |
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
702 |
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
|
703 |
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
704 |
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
|
705 |
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|
706 |
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
|
707 |
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
|
708 |
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR |
|
709 |
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
710 |
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
711 |
+ |
|
712 |
+ Modifications are released under the same license above. |
|
713 |
+ |
|
436 | 714 |
Args: |
437 | 715 |
ctx: |
438 | 716 |
The click context. |
... | ... |
@@ -440,6 +718,7 @@ class CommandWithHelpGroups(click.Command): |
440 | 718 |
The formatter for the `--help` listing. |
441 | 719 |
|
442 | 720 |
""" |
721 |
+ default_group_name = '' |
|
443 | 722 |
help_records: dict[str, list[tuple[str, str]]] = {} |
444 | 723 |
epilogs: dict[str, str] = {} |
445 | 724 |
params = self.params[:] |
... | ... |
@@ -451,15 +730,22 @@ class CommandWithHelpGroups(click.Command): |
451 | 730 |
for param in params: |
452 | 731 |
rec = param.get_help_record(ctx) |
453 | 732 |
if rec is not None: |
733 |
+ rec = (rec[0], self._text(rec[1])) |
|
454 | 734 |
if isinstance(param, OptionGroupOption): |
455 |
- group_name = param.option_group_name |
|
456 |
- epilogs.setdefault(group_name, param.epilog) |
|
735 |
+ group_name = self._text(param.option_group_name) |
|
736 |
+ epilogs.setdefault(group_name, self._text(param.epilog)) |
|
457 | 737 |
else: |
458 |
- group_name = '' |
|
738 |
+ group_name = default_group_name |
|
459 | 739 |
help_records.setdefault(group_name, []).append(rec) |
460 |
- default_group = help_records.pop('') |
|
461 |
- default_group_name = ( |
|
462 |
- 'Other Options' if len(default_group) > 1 else 'Options' |
|
740 |
+ if default_group_name in help_records: # pragma: no branch |
|
741 |
+ default_group = help_records.pop(default_group_name) |
|
742 |
+ default_group_label = ( |
|
743 |
+ _msg.Label.OTHER_OPTIONS_LABEL |
|
744 |
+ if len(default_group) > 1 |
|
745 |
+ else _msg.Label.OPTIONS_LABEL |
|
746 |
+ ) |
|
747 |
+ default_group_name = self._text( |
|
748 |
+ _msg.TranslatedString(default_group_label) |
|
463 | 749 |
) |
464 | 750 |
help_records[default_group_name] = default_group |
465 | 751 |
for group_name, records in help_records.items(): |
... | ... |
@@ -470,12 +756,158 @@ class CommandWithHelpGroups(click.Command): |
470 | 756 |
formatter.write_paragraph() |
471 | 757 |
with formatter.indentation(): |
472 | 758 |
formatter.write_text(epilog) |
759 |
+ self.format_commands(ctx, formatter) |
|
760 |
+ |
|
761 |
+ def format_commands( |
|
762 |
+ self, |
|
763 |
+ ctx: click.Context, |
|
764 |
+ formatter: click.HelpFormatter, |
|
765 |
+ ) -> None: |
|
766 |
+ """Format the subcommands, if any. |
|
767 |
+ |
|
768 |
+ If called on a command object that isn't derived from |
|
769 |
+ [`click.MultiCommand`][], then do nothing. |
|
770 |
+ |
|
771 |
+ Based on code from click 8.1. Subject to the following license |
|
772 |
+ (3-clause BSD license): |
|
773 |
+ |
|
774 |
+ Copyright 2024 Pallets |
|
775 |
+ |
|
776 |
+ Redistribution and use in source and binary forms, with or |
|
777 |
+ without modification, are permitted provided that the |
|
778 |
+ following conditions are met: |
|
779 |
+ |
|
780 |
+ 1. Redistributions of source code must retain the above |
|
781 |
+ copyright notice, this list of conditions and the |
|
782 |
+ following disclaimer. |
|
783 |
+ |
|
784 |
+ 2. Redistributions in binary form must reproduce the above |
|
785 |
+ copyright notice, this list of conditions and the |
|
786 |
+ following disclaimer in the documentation and/or other |
|
787 |
+ materials provided with the distribution. |
|
788 |
+ |
|
789 |
+ 3. Neither the name of the copyright holder nor the names |
|
790 |
+ of its contributors may be used to endorse or promote |
|
791 |
+ products derived from this software without specific |
|
792 |
+ prior written permission. |
|
793 |
+ |
|
794 |
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
|
795 |
+ CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, |
|
796 |
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
|
797 |
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
798 |
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
|
799 |
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
800 |
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
|
801 |
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|
802 |
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
|
803 |
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
|
804 |
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR |
|
805 |
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
806 |
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
807 |
+ |
|
808 |
+ Modifications are marked with respective comments. They too are |
|
809 |
+ released under the same license above. The original code did |
|
810 |
+ not contain any "noqa" or "pragma" comments. |
|
811 |
+ |
|
812 |
+ Args: |
|
813 |
+ ctx: |
|
814 |
+ The click context. |
|
815 |
+ formatter: |
|
816 |
+ The formatter for the `--help` listing. |
|
817 |
+ |
|
818 |
+ """ |
|
819 |
+ if not isinstance(self, click.MultiCommand): |
|
820 |
+ return |
|
821 |
+ commands: list[tuple[str, click.Command]] = [] |
|
822 |
+ for subcommand in self.list_commands(ctx): |
|
823 |
+ cmd = self.get_command(ctx, subcommand) |
|
824 |
+ if cmd is None or cmd.hidden: # pragma: no cover |
|
825 |
+ continue |
|
826 |
+ commands.append((subcommand, cmd)) |
|
827 |
+ if commands: # pragma: no branch |
|
828 |
+ longest_command = max((cmd[0] for cmd in commands), key=len) |
|
829 |
+ limit = formatter.width - 6 - len(longest_command) |
|
830 |
+ rows: list[tuple[str, str]] = [] |
|
831 |
+ for subcommand, cmd in commands: |
|
832 |
+ help_str = self._text(cmd.get_short_help_str(limit) or '') |
|
833 |
+ rows.append((subcommand, help_str)) |
|
834 |
+ if rows: # pragma: no branch |
|
835 |
+ commands_label = self._text( |
|
836 |
+ _msg.TranslatedString(_msg.Label.COMMANDS_LABEL) |
|
837 |
+ ) |
|
838 |
+ with formatter.section(commands_label): |
|
839 |
+ formatter.write_dl(rows) |
|
840 |
+ |
|
841 |
+ def format_epilog( |
|
842 |
+ self, |
|
843 |
+ ctx: click.Context, |
|
844 |
+ formatter: click.HelpFormatter, |
|
845 |
+ ) -> None: |
|
846 |
+ """Format the epilog, if any. |
|
847 |
+ |
|
848 |
+ Based on code from click 8.1. Subject to the following license |
|
849 |
+ (3-clause BSD license): |
|
850 |
+ |
|
851 |
+ Copyright 2024 Pallets |
|
852 |
+ |
|
853 |
+ Redistribution and use in source and binary forms, with or |
|
854 |
+ without modification, are permitted provided that the |
|
855 |
+ following conditions are met: |
|
856 |
+ |
|
857 |
+ 1. Redistributions of source code must retain the above |
|
858 |
+ copyright notice, this list of conditions and the |
|
859 |
+ following disclaimer. |
|
860 |
+ |
|
861 |
+ 2. Redistributions in binary form must reproduce the above |
|
862 |
+ copyright notice, this list of conditions and the |
|
863 |
+ following disclaimer in the documentation and/or other |
|
864 |
+ materials provided with the distribution. |
|
865 |
+ |
|
866 |
+ 3. Neither the name of the copyright holder nor the names |
|
867 |
+ of its contributors may be used to endorse or promote |
|
868 |
+ products derived from this software without specific |
|
869 |
+ prior written permission. |
|
870 |
+ |
|
871 |
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
|
872 |
+ CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, |
|
873 |
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
|
874 |
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
875 |
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
|
876 |
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
877 |
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
|
878 |
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|
879 |
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
|
880 |
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
|
881 |
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR |
|
882 |
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
883 |
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
884 |
+ |
|
885 |
+ Modifications are marked with respective comments. They too are |
|
886 |
+ released under the same license above. |
|
887 |
+ |
|
888 |
+ Args: |
|
889 |
+ ctx: |
|
890 |
+ The click context. |
|
891 |
+ formatter: |
|
892 |
+ The formatter for the `--help` listing. |
|
893 |
+ |
|
894 |
+ """ |
|
895 |
+ del ctx |
|
896 |
+ if self.epilog: # pragma: no branch |
|
897 |
+ # Modification against click 8.1: Call `str()` on |
|
898 |
+ # `self.epilog` to allow help texts to be general objects, |
|
899 |
+ # not just strings. Used to implement translatable strings, |
|
900 |
+ # as objects that stringify to the translation. |
|
901 |
+ epilog = inspect.cleandoc(self._text(self.epilog)) |
|
902 |
+ formatter.write_paragraph() |
|
903 |
+ with formatter.indentation(): |
|
904 |
+ formatter.write_text(epilog) |
|
473 | 905 |
|
474 | 906 |
|
475 | 907 |
class LoggingOption(OptionGroupOption): |
476 | 908 |
"""Logging options for the CLI.""" |
477 | 909 |
|
478 |
- option_group_name = 'Logging' |
|
910 |
+ option_group_name = _msg.TranslatedString(_msg.Label.LOGGING_LABEL) |
|
479 | 911 |
epilog = '' |
480 | 912 |
|
481 | 913 |
|
... | ... |
@@ -486,7 +918,7 @@ debug_option = click.option( |
486 | 918 |
flag_value=logging.DEBUG, |
487 | 919 |
expose_value=False, |
488 | 920 |
callback=adjust_logging_level, |
489 |
- help='also emit debug information (implies --verbose)', |
|
921 |
+ help=_msg.TranslatedString(_msg.Label.DEBUG_OPTION_HELP_TEXT), |
|
490 | 922 |
cls=LoggingOption, |
491 | 923 |
) |
492 | 924 |
verbose_option = click.option( |
... | ... |
@@ -497,7 +929,7 @@ verbose_option = click.option( |
497 | 929 |
flag_value=logging.INFO, |
498 | 930 |
expose_value=False, |
499 | 931 |
callback=adjust_logging_level, |
500 |
- help='emit extra/progress information to standard error', |
|
932 |
+ help=_msg.TranslatedString(_msg.Label.VERBOSE_OPTION_HELP_TEXT), |
|
501 | 933 |
cls=LoggingOption, |
502 | 934 |
) |
503 | 935 |
quiet_option = click.option( |
... | ... |
@@ -508,7 +940,7 @@ quiet_option = click.option( |
508 | 940 |
flag_value=logging.ERROR, |
509 | 941 |
expose_value=False, |
510 | 942 |
callback=adjust_logging_level, |
511 |
- help='suppress even warnings, emit only errors', |
|
943 |
+ help=_msg.TranslatedString(_msg.Label.QUIET_OPTION_HELP_TEXT), |
|
512 | 944 |
cls=LoggingOption, |
513 | 945 |
) |
514 | 946 |
|
... | ... |
@@ -534,7 +966,7 @@ def standard_logging_options(f: Callable[P, R]) -> Callable[P, R]: |
534 | 966 |
# ========= |
535 | 967 |
|
536 | 968 |
|
537 |
-class _DefaultToVaultGroup(click.Group): |
|
969 |
+class _DefaultToVaultGroup(CommandWithHelpGroups, click.Group): |
|
538 | 970 |
"""A helper class to implement the default-to-"vault"-subcommand behavior. |
539 | 971 |
|
540 | 972 |
Modifies internal [`click.MultiCommand`][] methods, and thus is both |
... | ... |
@@ -548,16 +980,53 @@ class _DefaultToVaultGroup(click.Group): |
548 | 980 |
"""Resolve a command, but default to "vault" instead of erroring out. |
549 | 981 |
|
550 | 982 |
Based on code from click 8.1, which appears to be essentially |
551 |
- untouched since at least click 3.2. |
|
983 |
+ untouched since at least click 3.2. Subject to the following |
|
984 |
+ license (3-clause BSD license): |
|
985 |
+ |
|
986 |
+ Copyright 2024 Pallets |
|
987 |
+ |
|
988 |
+ Redistribution and use in source and binary forms, with or |
|
989 |
+ without modification, are permitted provided that the following |
|
990 |
+ conditions are met: |
|
991 |
+ |
|
992 |
+ 1. Redistributions of source code must retain the above |
|
993 |
+ copyright notice, this list of conditions and the following |
|
994 |
+ disclaimer. |
|
995 |
+ |
|
996 |
+ 2. Redistributions in binary form must reproduce the above |
|
997 |
+ copyright notice, this list of conditions and the following |
|
998 |
+ disclaimer in the documentation and/or other materials |
|
999 |
+ provided with the distribution. |
|
1000 |
+ |
|
1001 |
+ 3. Neither the name of the copyright holder nor the names of |
|
1002 |
+ its contributors may be used to endorse or promote products |
|
1003 |
+ derived from this software without specific prior written |
|
1004 |
+ permission. |
|
1005 |
+ |
|
1006 |
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
|
1007 |
+ CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, |
|
1008 |
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
|
1009 |
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
1010 |
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
|
1011 |
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
1012 |
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
1013 |
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
1014 |
+ USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
1015 |
+ AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
1016 |
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING |
|
1017 |
+ IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
|
1018 |
+ THE POSSIBILITY OF SUCH DAMAGE. |
|
1019 |
+ |
|
1020 |
+ Modifications to this routine are marked with "modifications for |
|
1021 |
+ derivepassphrase". Furthermore, all "pragma" and "noqa" comments |
|
1022 |
+ are also modifications for derivepassphrase. |
|
552 | 1023 |
|
553 | 1024 |
""" |
554 | 1025 |
cmd_name = click.utils.make_str(args[0]) |
555 | 1026 |
|
556 |
- # ORIGINAL COMMENT |
|
557 | 1027 |
# Get the command |
558 | 1028 |
cmd = self.get_command(ctx, cmd_name) |
559 | 1029 |
|
560 |
- # ORIGINAL COMMENT |
|
561 | 1030 |
# If we can't find the command but there is a normalization |
562 | 1031 |
# function available, we try with that one. |
563 | 1032 |
if ( # pragma: no cover |
... | ... |
@@ -566,7 +1035,6 @@ class _DefaultToVaultGroup(click.Group): |
566 | 1035 |
cmd_name = ctx.token_normalize_func(cmd_name) |
567 | 1036 |
cmd = self.get_command(ctx, cmd_name) |
568 | 1037 |
|
569 |
- # ORIGINAL COMMENT |
|
570 | 1038 |
# If we don't find the command we want to show an error message |
571 | 1039 |
# to the user that it was not provided. However, there is |
572 | 1040 |
# something else we should do: if the first argument looks like |
... | ... |
@@ -576,19 +1044,24 @@ class _DefaultToVaultGroup(click.Group): |
576 | 1044 |
if cmd is None and not ctx.resilient_parsing: |
577 | 1045 |
if click.parser.split_opt(cmd_name)[0]: |
578 | 1046 |
self.parse_args(ctx, ctx.args) |
1047 |
+ #### |
|
1048 |
+ # BEGIN modifications for derivepassphrase |
|
1049 |
+ # |
|
579 | 1050 |
# Instead of calling ctx.fail here, default to "vault", and |
580 | 1051 |
# issue a deprecation warning. |
581 |
- logger = logging.getLogger(PROG_NAME) |
|
582 | 1052 |
deprecation = logging.getLogger(f'{PROG_NAME}.deprecation') |
583 | 1053 |
deprecation.warning( |
584 |
- 'A subcommand will be required in v1.0. ' |
|
585 |
- 'See --help for available subcommands.' |
|
1054 |
+ _msg.TranslatedString( |
|
1055 |
+ _msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED |
|
1056 |
+ ) |
|
586 | 1057 |
) |
587 |
- logger.warning('Defaulting to subcommand "vault".') |
|
588 | 1058 |
cmd_name = 'vault' |
589 | 1059 |
cmd = self.get_command(ctx, cmd_name) |
590 | 1060 |
assert cmd is not None, 'Mandatory subcommand "vault" missing!' |
591 | 1061 |
args = [cmd_name, *args] |
1062 |
+ # |
|
1063 |
+ # END modifications for derivepassphrase |
|
1064 |
+ #### |
|
592 | 1065 |
return cmd_name if cmd else None, cmd, args[1:] # noqa: DOC201 |
593 | 1066 |
|
594 | 1067 |
|
... | ... |
@@ -628,14 +1101,14 @@ class _TopLevelCLIEntryPoint(_DefaultToVaultGroup): |
628 | 1101 |
'ignore_unknown_options': True, |
629 | 1102 |
'allow_interspersed_args': False, |
630 | 1103 |
}, |
631 |
- epilog=r""" |
|
632 |
- Configuration is stored in a directory according to the |
|
633 |
- DERIVEPASSPHRASE_PATH variable, which defaults to |
|
634 |
- `~/.derivepassphrase` on UNIX-like systems and |
|
635 |
- `C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows. |
|
636 |
- """, |
|
1104 |
+ epilog=_msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EPILOG_01), |
|
637 | 1105 |
invoke_without_command=True, |
638 | 1106 |
cls=_TopLevelCLIEntryPoint, |
1107 |
+ help=( |
|
1108 |
+ _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_01), |
|
1109 |
+ _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_02), |
|
1110 |
+ _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_03), |
|
1111 |
+ ), |
|
639 | 1112 |
) |
640 | 1113 |
@click.version_option(version=dpp.__version__, prog_name=PROG_NAME) |
641 | 1114 |
@standard_logging_options |
... | ... |
@@ -643,41 +1116,20 @@ class _TopLevelCLIEntryPoint(_DefaultToVaultGroup): |
643 | 1116 |
def derivepassphrase(ctx: click.Context, /) -> None: |
644 | 1117 |
"""Derive a strong passphrase, deterministically, from a master secret. |
645 | 1118 |
|
646 |
- Using a master secret, derive a passphrase for a named service, |
|
647 |
- subject to constraints e.g. on passphrase length, allowed |
|
648 |
- characters, etc. The exact derivation depends on the selected |
|
649 |
- derivation scheme. For each scheme, it is computationally |
|
650 |
- infeasible to discern the master secret from the derived passphrase. |
|
651 |
- The derivations are also deterministic, given the same inputs, thus |
|
652 |
- the resulting passphrases need not be stored explicitly. The |
|
653 |
- service name and constraints themselves also generally need not be |
|
654 |
- kept secret, depending on the scheme. |
|
655 |
- |
|
656 |
- The currently implemented subcommands are "vault" (for the scheme |
|
657 |
- used by vault) and "export" (for exporting foreign configuration |
|
658 |
- data). See the respective `--help` output for instructions. If no |
|
659 |
- subcommand is given, we default to "vault". |
|
660 |
- |
|
661 |
- Deprecation notice: Defaulting to "vault" is deprecated. Starting |
|
662 |
- in v1.0, the subcommand must be specified explicitly.\f |
|
663 |
- |
|
664 | 1119 |
This is a [`click`][CLICK]-powered command-line interface function, |
665 |
- and not intended for programmatic use. Call with arguments |
|
666 |
- `['--help']` to see full documentation of the interface. (See also |
|
1120 |
+ and not intended for programmatic use. See the derivepassphrase(1) |
|
1121 |
+ manpage for full documentation of the interface. (See also |
|
667 | 1122 |
[`click.testing.CliRunner`][] for controlled, programmatic |
668 | 1123 |
invocation.) |
669 | 1124 |
|
670 | 1125 |
[CLICK]: https://pypi.org/package/click/ |
671 | 1126 |
|
672 |
- """ # noqa: D301 |
|
673 |
- logger = logging.getLogger(PROG_NAME) |
|
1127 |
+ """ |
|
674 | 1128 |
deprecation = logging.getLogger(f'{PROG_NAME}.deprecation') |
675 | 1129 |
if ctx.invoked_subcommand is None: |
676 | 1130 |
deprecation.warning( |
677 |
- 'A subcommand will be required in v1.0. ' |
|
678 |
- 'See --help for available subcommands.' |
|
1131 |
+ _msg.TranslatedString(_msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED) |
|
679 | 1132 |
) |
680 |
- logger.warning('Defaulting to subcommand "vault".') |
|
681 | 1133 |
# See definition of click.Group.invoke, non-chained case. |
682 | 1134 |
with ctx: |
683 | 1135 |
sub_ctx = derivepassphrase_vault.make_context( |
... | ... |
@@ -701,6 +1153,11 @@ def derivepassphrase(ctx: click.Context, /) -> None: |
701 | 1153 |
}, |
702 | 1154 |
invoke_without_command=True, |
703 | 1155 |
cls=_DefaultToVaultGroup, |
1156 |
+ help=( |
|
1157 |
+ _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_01), |
|
1158 |
+ _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_02), |
|
1159 |
+ _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_03), |
|
1160 |
+ ), |
|
704 | 1161 |
) |
705 | 1162 |
@click.version_option(version=dpp.__version__, prog_name=PROG_NAME) |
706 | 1163 |
@standard_logging_options |
... | ... |
@@ -708,33 +1165,20 @@ def derivepassphrase(ctx: click.Context, /) -> None: |
708 | 1165 |
def derivepassphrase_export(ctx: click.Context, /) -> None: |
709 | 1166 |
"""Export a foreign configuration to standard output. |
710 | 1167 |
|
711 |
- Read a foreign system configuration, extract all information from |
|
712 |
- it, and export the resulting configuration to standard output. |
|
713 |
- |
|
714 |
- The only available subcommand is "vault", which implements the |
|
715 |
- vault-native configuration scheme. If no subcommand is given, we |
|
716 |
- default to "vault". |
|
717 |
- |
|
718 |
- Deprecation notice: Defaulting to "vault" is deprecated. Starting |
|
719 |
- in v1.0, the subcommand must be specified explicitly.\f |
|
720 |
- |
|
721 | 1168 |
This is a [`click`][CLICK]-powered command-line interface function, |
722 |
- and not intended for programmatic use. Call with arguments |
|
723 |
- `['--help']` to see full documentation of the interface. (See also |
|
724 |
- [`click.testing.CliRunner`][] for controlled, programmatic |
|
725 |
- invocation.) |
|
1169 |
+ and not intended for programmatic use. See the |
|
1170 |
+ derivepassphrase-export(1) manpage for full documentation of the |
|
1171 |
+ interface. (See also [`click.testing.CliRunner`][] for controlled, |
|
1172 |
+ programmatic invocation.) |
|
726 | 1173 |
|
727 | 1174 |
[CLICK]: https://pypi.org/package/click/ |
728 | 1175 |
|
729 |
- """ # noqa: D301 |
|
730 |
- logger = logging.getLogger(PROG_NAME) |
|
1176 |
+ """ |
|
731 | 1177 |
deprecation = logging.getLogger(f'{PROG_NAME}.deprecation') |
732 | 1178 |
if ctx.invoked_subcommand is None: |
733 | 1179 |
deprecation.warning( |
734 |
- 'A subcommand will be required in v1.0. ' |
|
735 |
- 'See --help for available subcommands.' |
|
1180 |
+ _msg.TranslatedString(_msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED) |
|
736 | 1181 |
) |
737 |
- logger.warning('Defaulting to subcommand "vault".') |
|
738 | 1182 |
# See definition of click.Group.invoke, non-chained case. |
739 | 1183 |
with ctx: |
740 | 1184 |
sub_ctx = derivepassphrase_export_vault.make_context( |
... | ... |
@@ -787,32 +1231,68 @@ def _load_data( |
787 | 1231 |
assert_never(fmt) |
788 | 1232 |
|
789 | 1233 |
|
1234 |
+class StandardOption(OptionGroupOption): |
|
1235 |
+ pass |
|
1236 |
+ |
|
1237 |
+ |
|
790 | 1238 |
@derivepassphrase_export.command( |
791 | 1239 |
'vault', |
792 | 1240 |
context_settings={'help_option_names': ['-h', '--help']}, |
1241 |
+ cls=CommandWithHelpGroups, |
|
1242 |
+ help=( |
|
1243 |
+ _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_VAULT_01), |
|
1244 |
+ _msg.TranslatedString( |
|
1245 |
+ _msg.Label.DERIVEPASSPHRASE_EXPORT_VAULT_02, |
|
1246 |
+ path_metavar=_msg.TranslatedString( |
|
1247 |
+ _msg.Label.EXPORT_VAULT_METAVAR_PATH, |
|
1248 |
+ ), |
|
1249 |
+ ), |
|
1250 |
+ _msg.TranslatedString( |
|
1251 |
+ _msg.Label.DERIVEPASSPHRASE_EXPORT_VAULT_03, |
|
1252 |
+ path_metavar=_msg.TranslatedString( |
|
1253 |
+ _msg.Label.EXPORT_VAULT_METAVAR_PATH, |
|
1254 |
+ ), |
|
1255 |
+ ), |
|
1256 |
+ ), |
|
793 | 1257 |
) |
794 | 1258 |
@standard_logging_options |
795 | 1259 |
@click.option( |
796 | 1260 |
'-f', |
797 | 1261 |
'--format', |
798 | 1262 |
'formats', |
799 |
- metavar='FMT', |
|
1263 |
+ metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_FORMAT_METAVAR_FMT), |
|
800 | 1264 |
multiple=True, |
801 | 1265 |
default=('v0.3', 'v0.2', 'storeroom'), |
802 | 1266 |
type=click.Choice(['v0.2', 'v0.3', 'storeroom']), |
803 |
- help='try the following storage formats, in order (default: v0.3, v0.2)', |
|
1267 |
+ help=_msg.TranslatedString( |
|
1268 |
+ _msg.Label.EXPORT_VAULT_FORMAT_HELP_TEXT, |
|
1269 |
+ defaults_hint=_msg.TranslatedString( |
|
1270 |
+ _msg.Label.EXPORT_VAULT_FORMAT_DEFAULTS_HELP_TEXT, |
|
1271 |
+ ), |
|
1272 |
+ metavar=_msg.TranslatedString( |
|
1273 |
+ _msg.Label.EXPORT_VAULT_FORMAT_METAVAR_FMT, |
|
1274 |
+ ), |
|
1275 |
+ ), |
|
1276 |
+ cls=StandardOption, |
|
804 | 1277 |
) |
805 | 1278 |
@click.option( |
806 | 1279 |
'-k', |
807 | 1280 |
'--key', |
808 |
- metavar='K', |
|
809 |
- help=( |
|
810 |
- 'use K as the storage master key ' |
|
811 |
- '(default: check the `VAULT_KEY`, `LOGNAME`, `USER` or ' |
|
812 |
- '`USERNAME` environment variables)' |
|
1281 |
+ metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_KEY_METAVAR_K), |
|
1282 |
+ help=_msg.TranslatedString( |
|
1283 |
+ _msg.Label.EXPORT_VAULT_KEY_HELP_TEXT, |
|
1284 |
+ metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_KEY_METAVAR_K), |
|
1285 |
+ defaults_hint=_msg.TranslatedString( |
|
1286 |
+ _msg.Label.EXPORT_VAULT_KEY_DEFAULTS_HELP_TEXT, |
|
813 | 1287 |
), |
1288 |
+ ), |
|
1289 |
+ cls=StandardOption, |
|
1290 |
+) |
|
1291 |
+@click.argument( |
|
1292 |
+ 'path', |
|
1293 |
+ metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_METAVAR_PATH), |
|
1294 |
+ required=True, |
|
814 | 1295 |
) |
815 |
-@click.argument('path', metavar='PATH', required=True) |
|
816 | 1296 |
@click.pass_context |
817 | 1297 |
def derivepassphrase_export_vault( |
818 | 1298 |
ctx: click.Context, |
... | ... |
@@ -824,16 +1304,13 @@ def derivepassphrase_export_vault( |
824 | 1304 |
) -> None: |
825 | 1305 |
"""Export a vault-native configuration to standard output. |
826 | 1306 |
|
827 |
- Read the vault-native configuration at PATH, extract all information |
|
828 |
- from it, and export the resulting configuration to standard output. |
|
829 |
- Depending on the configuration format, PATH may either be a file or |
|
830 |
- a directory. Supports the vault "v0.2", "v0.3" and "storeroom" |
|
831 |
- formats. |
|
1307 |
+ This is a [`click`][CLICK]-powered command-line interface function, |
|
1308 |
+ and not intended for programmatic use. See the |
|
1309 |
+ derivepassphrase-export-vault(1) manpage for full documentation of |
|
1310 |
+ the interface. (See also [`click.testing.CliRunner`][] for |
|
1311 |
+ controlled, programmatic invocation.) |
|
832 | 1312 |
|
833 |
- If PATH is explicitly given as `VAULT_PATH`, then use the |
|
834 |
- `VAULT_PATH` environment variable to determine the correct path. |
|
835 |
- (Use `./VAULT_PATH` or similar to indicate a file/directory actually |
|
836 |
- named `VAULT_PATH`.) |
|
1313 |
+ [CLICK]: https://pypi.org/package/click/ |
|
837 | 1314 |
|
838 | 1315 |
""" |
839 | 1316 |
logger = logging.getLogger(PROG_NAME) |
... | ... |
@@ -1093,7 +1570,7 @@ def _get_suitable_ssh_keys( |
1093 | 1570 |
if vault.Vault.is_suitable_ssh_key(key, client=client): |
1094 | 1571 |
yield pair |
1095 | 1572 |
if not suitable_keys: # pragma: no cover |
1096 |
- raise LookupError(_NO_USABLE_KEYS) |
|
1573 |
+ raise LookupError(_NO_SUITABLE_KEYS) |
|
1097 | 1574 |
|
1098 | 1575 |
|
1099 | 1576 |
def _prompt_for_selection( |
... | ... |
@@ -1317,9 +1794,11 @@ def _check_for_misleading_passphrase( |
1317 | 1794 |
if not unicodedata.is_normalized(form, phrase): |
1318 | 1795 |
logger.warning( |
1319 | 1796 |
( |
1320 |
- 'the %s passphrase is not %s-normalized. ' |
|
1321 |
- 'Make sure to double-check this is really the ' |
|
1322 |
- 'passphrase you want.' |
|
1797 |
+ 'The %s passphrase is not %s-normalized. Its ' |
|
1798 |
+ 'serialization as a byte string may not be what you ' |
|
1799 |
+ 'expect it to be, even if it *displays* correctly. ' |
|
1800 |
+ 'Please make sure to double-check any derived ' |
|
1801 |
+ 'passphrases for unexpected results.' |
|
1323 | 1802 |
), |
1324 | 1803 |
formatted_key, |
1325 | 1804 |
form, |
... | ... |
@@ -1350,19 +1829,35 @@ def _key_to_phrase( |
1350 | 1829 |
k == key for k, _ in keylist |
1351 | 1830 |
): |
1352 | 1831 |
error_callback( |
1353 |
- 'The requested SSH key is not loaded ' |
|
1354 |
- 'into the agent.' |
|
1832 |
+ _msg.TranslatedString( |
|
1833 |
+ _msg.ErrMsgTemplate.SSH_KEY_NOT_LOADED |
|
1834 |
+ ) |
|
1835 |
+ ) |
|
1836 |
+ error_callback( |
|
1837 |
+ _msg.TranslatedString( |
|
1838 |
+ _msg.ErrMsgTemplate.AGENT_REFUSED_SIGNATURE |
|
1839 |
+ ), |
|
1840 |
+ exc_info=exc, |
|
1355 | 1841 |
) |
1356 |
- error_callback(exc) |
|
1357 | 1842 |
except KeyError: |
1358 |
- error_callback('Cannot find running SSH agent; check SSH_AUTH_SOCK') |
|
1359 |
- except NotImplementedError: |
|
1360 | 1843 |
error_callback( |
1361 |
- 'Cannot connect to SSH agent because ' |
|
1362 |
- 'this Python version does not support UNIX domain sockets' |
|
1844 |
+ _msg.TranslatedString(_msg.ErrMsgTemplate.NO_SSH_AGENT_FOUND) |
|
1363 | 1845 |
) |
1846 |
+ except NotImplementedError: |
|
1847 |
+ error_callback(_msg.TranslatedString(_msg.ErrMsgTemplate.NO_AF_UNIX)) |
|
1364 | 1848 |
except OSError as exc: |
1365 |
- error_callback('Cannot connect to SSH agent: %s', exc.strerror) |
|
1849 |
+ error_callback( |
|
1850 |
+ _msg.TranslatedString( |
|
1851 |
+ _msg.ErrMsgTemplate.CANNOT_CONNECT_TO_AGENT, |
|
1852 |
+ error=exc.strerror, |
|
1853 |
+ filename=exc.filename, |
|
1854 |
+ ).maybe_without_filename() |
|
1855 |
+ ) |
|
1856 |
+ except RuntimeError as exc: |
|
1857 |
+ error_callback( |
|
1858 |
+ _msg.TranslatedString(_msg.ErrMsgTemplate.CANNOT_UNDERSTAND_AGENT), |
|
1859 |
+ exc_info=exc, |
|
1860 |
+ ) |
|
1366 | 1861 |
|
1367 | 1862 |
|
1368 | 1863 |
def _print_config_as_sh_script( |
... | ... |
@@ -1441,36 +1936,44 @@ def _print_config_as_sh_script( |
1441 | 1936 |
class PassphraseGenerationOption(OptionGroupOption): |
1442 | 1937 |
"""Passphrase generation options for the CLI.""" |
1443 | 1938 |
|
1444 |
- option_group_name = 'Passphrase generation' |
|
1445 |
- epilog = """ |
|
1446 |
- Use NUMBER=0, e.g. "--symbol 0", to exclude a character type |
|
1447 |
- from the output. |
|
1448 |
- """ |
|
1939 |
+ option_group_name = _msg.TranslatedString( |
|
1940 |
+ _msg.Label.PASSPHRASE_GENERATION_LABEL |
|
1941 |
+ ) |
|
1942 |
+ epilog = _msg.TranslatedString( |
|
1943 |
+ _msg.Label.PASSPHRASE_GENERATION_EPILOG, |
|
1944 |
+ metavar=_msg.TranslatedString( |
|
1945 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
1946 |
+ ), |
|
1947 |
+ ) |
|
1449 | 1948 |
|
1450 | 1949 |
|
1451 | 1950 |
class ConfigurationOption(OptionGroupOption): |
1452 | 1951 |
"""Configuration options for the CLI.""" |
1453 | 1952 |
|
1454 |
- option_group_name = 'Configuration' |
|
1455 |
- epilog = """ |
|
1456 |
- Use $VISUAL or $EDITOR to configure the spawned editor. |
|
1457 |
- """ |
|
1953 |
+ option_group_name = _msg.TranslatedString(_msg.Label.CONFIGURATION_LABEL) |
|
1954 |
+ epilog = _msg.TranslatedString(_msg.Label.CONFIGURATION_EPILOG) |
|
1458 | 1955 |
|
1459 | 1956 |
|
1460 | 1957 |
class StorageManagementOption(OptionGroupOption): |
1461 | 1958 |
"""Storage management options for the CLI.""" |
1462 | 1959 |
|
1463 |
- option_group_name = 'Storage management' |
|
1464 |
- epilog = """ |
|
1465 |
- Using "-" as PATH for standard input/standard output is |
|
1466 |
- supported. |
|
1467 |
- """ |
|
1960 |
+ option_group_name = _msg.TranslatedString( |
|
1961 |
+ _msg.Label.STORAGE_MANAGEMENT_LABEL |
|
1962 |
+ ) |
|
1963 |
+ epilog = _msg.TranslatedString( |
|
1964 |
+ _msg.Label.STORAGE_MANAGEMENT_EPILOG, |
|
1965 |
+ metavar=_msg.TranslatedString( |
|
1966 |
+ _msg.Label.STORAGE_MANAGEMENT_METAVAR_PATH |
|
1967 |
+ ), |
|
1968 |
+ ) |
|
1468 | 1969 |
|
1469 | 1970 |
|
1470 | 1971 |
class CompatibilityOption(OptionGroupOption): |
1471 | 1972 |
"""Compatibility and incompatibility options for the CLI.""" |
1472 | 1973 |
|
1473 |
- option_group_name = 'Compatibility and extension options' |
|
1974 |
+ option_group_name = _msg.TranslatedString( |
|
1975 |
+ _msg.Label.COMPATIBILITY_OPTION_LABEL |
|
1976 |
+ ) |
|
1474 | 1977 |
|
1475 | 1978 |
|
1476 | 1979 |
def _validate_occurrence_constraint( |
... | ... |
@@ -1502,11 +2005,9 @@ def _validate_occurrence_constraint( |
1502 | 2005 |
try: |
1503 | 2006 |
int_value = int(value, 10) |
1504 | 2007 |
except ValueError as exc: |
1505 |
- msg = 'not an integer' |
|
1506 |
- raise click.BadParameter(msg) from exc |
|
2008 |
+ raise click.BadParameter(_NOT_AN_INTEGER) from exc |
|
1507 | 2009 |
if int_value < 0: |
1508 |
- msg = 'not a non-negative integer' |
|
1509 |
- raise click.BadParameter(msg) |
|
2010 |
+ raise click.BadParameter(_NOT_A_NONNEGATIVE_INTEGER) |
|
1510 | 2011 |
return int_value |
1511 | 2012 |
|
1512 | 2013 |
|
... | ... |
@@ -1539,11 +2040,9 @@ def _validate_length( |
1539 | 2040 |
try: |
1540 | 2041 |
int_value = int(value, 10) |
1541 | 2042 |
except ValueError as exc: |
1542 |
- msg = 'not an integer' |
|
1543 |
- raise click.BadParameter(msg) from exc |
|
2043 |
+ raise click.BadParameter(_NOT_AN_INTEGER) from exc |
|
1544 | 2044 |
if int_value < 1: |
1545 |
- msg = 'not a positive integer' |
|
1546 |
- raise click.BadParameter(msg) |
|
2045 |
+ raise click.BadParameter(_NOT_A_POSITIVE_INTEGER) |
|
1547 | 2046 |
return int_value |
1548 | 2047 |
|
1549 | 2048 |
|
... | ... |
@@ -1565,23 +2064,28 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
1565 | 2064 |
'vault', |
1566 | 2065 |
context_settings={'help_option_names': ['-h', '--help']}, |
1567 | 2066 |
cls=CommandWithHelpGroups, |
1568 |
- epilog=r""" |
|
1569 |
- WARNING: There is NO WAY to retrieve the generated passphrases |
|
1570 |
- if the master passphrase, the SSH key, or the exact passphrase |
|
1571 |
- settings are lost, short of trying out all possible |
|
1572 |
- combinations. You are STRONGLY advised to keep independent |
|
1573 |
- backups of the settings and the SSH key, if any. |
|
1574 |
- |
|
1575 |
- The configuration is NOT encrypted, and you are STRONGLY |
|
1576 |
- discouraged from using a stored passphrase. |
|
1577 |
- """, |
|
2067 |
+ help=( |
|
2068 |
+ _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_01), |
|
2069 |
+ _msg.TranslatedString( |
|
2070 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_02, |
|
2071 |
+ service_metavar=_msg.TranslatedString( |
|
2072 |
+ _msg.Label.VAULT_METAVAR_SERVICE |
|
2073 |
+ ), |
|
2074 |
+ ), |
|
2075 |
+ ), |
|
2076 |
+ epilog=( |
|
2077 |
+ _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_EPILOG_01), |
|
2078 |
+ _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_EPILOG_02), |
|
2079 |
+ ), |
|
1578 | 2080 |
) |
1579 | 2081 |
@click.option( |
1580 | 2082 |
'-p', |
1581 | 2083 |
'--phrase', |
1582 | 2084 |
'use_phrase', |
1583 | 2085 |
is_flag=True, |
1584 |
- help='prompts you for your passphrase', |
|
2086 |
+ help=_msg.TranslatedString( |
|
2087 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_PHRASE_HELP_TEXT |
|
2088 |
+ ), |
|
1585 | 2089 |
cls=PassphraseGenerationOption, |
1586 | 2090 |
) |
1587 | 2091 |
@click.option( |
... | ... |
@@ -1589,65 +2093,123 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
1589 | 2093 |
'--key', |
1590 | 2094 |
'use_key', |
1591 | 2095 |
is_flag=True, |
1592 |
- help='uses your SSH private key to generate passwords', |
|
2096 |
+ help=_msg.TranslatedString( |
|
2097 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_KEY_HELP_TEXT |
|
2098 |
+ ), |
|
1593 | 2099 |
cls=PassphraseGenerationOption, |
1594 | 2100 |
) |
1595 | 2101 |
@click.option( |
1596 | 2102 |
'-l', |
1597 | 2103 |
'--length', |
1598 |
- metavar='NUMBER', |
|
2104 |
+ metavar=_msg.TranslatedString( |
|
2105 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2106 |
+ ), |
|
1599 | 2107 |
callback=_validate_length, |
1600 |
- help='emits password of length NUMBER', |
|
2108 |
+ help=_msg.TranslatedString( |
|
2109 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_LENGTH_HELP_TEXT, |
|
2110 |
+ metavar=_msg.TranslatedString( |
|
2111 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2112 |
+ ), |
|
2113 |
+ ), |
|
1601 | 2114 |
cls=PassphraseGenerationOption, |
1602 | 2115 |
) |
1603 | 2116 |
@click.option( |
1604 | 2117 |
'-r', |
1605 | 2118 |
'--repeat', |
1606 |
- metavar='NUMBER', |
|
2119 |
+ metavar=_msg.TranslatedString( |
|
2120 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2121 |
+ ), |
|
1607 | 2122 |
callback=_validate_occurrence_constraint, |
1608 |
- help='allows maximum of NUMBER repeated adjacent chars', |
|
2123 |
+ help=_msg.TranslatedString( |
|
2124 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_REPEAT_HELP_TEXT, |
|
2125 |
+ metavar=_msg.TranslatedString( |
|
2126 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2127 |
+ ), |
|
2128 |
+ ), |
|
1609 | 2129 |
cls=PassphraseGenerationOption, |
1610 | 2130 |
) |
1611 | 2131 |
@click.option( |
1612 | 2132 |
'--lower', |
1613 |
- metavar='NUMBER', |
|
2133 |
+ metavar=_msg.TranslatedString( |
|
2134 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2135 |
+ ), |
|
1614 | 2136 |
callback=_validate_occurrence_constraint, |
1615 |
- help='includes at least NUMBER lowercase letters', |
|
2137 |
+ help=_msg.TranslatedString( |
|
2138 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_LOWER_HELP_TEXT, |
|
2139 |
+ metavar=_msg.TranslatedString( |
|
2140 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2141 |
+ ), |
|
2142 |
+ ), |
|
1616 | 2143 |
cls=PassphraseGenerationOption, |
1617 | 2144 |
) |
1618 | 2145 |
@click.option( |
1619 | 2146 |
'--upper', |
1620 |
- metavar='NUMBER', |
|
2147 |
+ metavar=_msg.TranslatedString( |
|
2148 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2149 |
+ ), |
|
1621 | 2150 |
callback=_validate_occurrence_constraint, |
1622 |
- help='includes at least NUMBER uppercase letters', |
|
2151 |
+ help=_msg.TranslatedString( |
|
2152 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_UPPER_HELP_TEXT, |
|
2153 |
+ metavar=_msg.TranslatedString( |
|
2154 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2155 |
+ ), |
|
2156 |
+ ), |
|
1623 | 2157 |
cls=PassphraseGenerationOption, |
1624 | 2158 |
) |
1625 | 2159 |
@click.option( |
1626 | 2160 |
'--number', |
1627 |
- metavar='NUMBER', |
|
2161 |
+ metavar=_msg.TranslatedString( |
|
2162 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2163 |
+ ), |
|
1628 | 2164 |
callback=_validate_occurrence_constraint, |
1629 |
- help='includes at least NUMBER digits', |
|
2165 |
+ help=_msg.TranslatedString( |
|
2166 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_NUMBER_HELP_TEXT, |
|
2167 |
+ metavar=_msg.TranslatedString( |
|
2168 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2169 |
+ ), |
|
2170 |
+ ), |
|
1630 | 2171 |
cls=PassphraseGenerationOption, |
1631 | 2172 |
) |
1632 | 2173 |
@click.option( |
1633 | 2174 |
'--space', |
1634 |
- metavar='NUMBER', |
|
2175 |
+ metavar=_msg.TranslatedString( |
|
2176 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2177 |
+ ), |
|
1635 | 2178 |
callback=_validate_occurrence_constraint, |
1636 |
- help='includes at least NUMBER spaces', |
|
2179 |
+ help=_msg.TranslatedString( |
|
2180 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_SPACE_HELP_TEXT, |
|
2181 |
+ metavar=_msg.TranslatedString( |
|
2182 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2183 |
+ ), |
|
2184 |
+ ), |
|
1637 | 2185 |
cls=PassphraseGenerationOption, |
1638 | 2186 |
) |
1639 | 2187 |
@click.option( |
1640 | 2188 |
'--dash', |
1641 |
- metavar='NUMBER', |
|
2189 |
+ metavar=_msg.TranslatedString( |
|
2190 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2191 |
+ ), |
|
1642 | 2192 |
callback=_validate_occurrence_constraint, |
1643 |
- help='includes at least NUMBER "-" or "_"', |
|
2193 |
+ help=_msg.TranslatedString( |
|
2194 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_DASH_HELP_TEXT, |
|
2195 |
+ metavar=_msg.TranslatedString( |
|
2196 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2197 |
+ ), |
|
2198 |
+ ), |
|
1644 | 2199 |
cls=PassphraseGenerationOption, |
1645 | 2200 |
) |
1646 | 2201 |
@click.option( |
1647 | 2202 |
'--symbol', |
1648 |
- metavar='NUMBER', |
|
2203 |
+ metavar=_msg.TranslatedString( |
|
2204 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2205 |
+ ), |
|
1649 | 2206 |
callback=_validate_occurrence_constraint, |
1650 |
- help='includes at least NUMBER symbol chars', |
|
2207 |
+ help=_msg.TranslatedString( |
|
2208 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_SYMBOL_HELP_TEXT, |
|
2209 |
+ metavar=_msg.TranslatedString( |
|
2210 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2211 |
+ ), |
|
2212 |
+ ), |
|
1651 | 2213 |
cls=PassphraseGenerationOption, |
1652 | 2214 |
) |
1653 | 2215 |
@click.option( |
... | ... |
@@ -1655,7 +2217,12 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
1655 | 2217 |
'--notes', |
1656 | 2218 |
'edit_notes', |
1657 | 2219 |
is_flag=True, |
1658 |
- help='spawn an editor to edit notes for SERVICE', |
|
2220 |
+ help=_msg.TranslatedString( |
|
2221 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_HELP_TEXT, |
|
2222 |
+ service_metavar=_msg.TranslatedString( |
|
2223 |
+ _msg.Label.VAULT_METAVAR_SERVICE |
|
2224 |
+ ), |
|
2225 |
+ ), |
|
1659 | 2226 |
cls=ConfigurationOption, |
1660 | 2227 |
) |
1661 | 2228 |
@click.option( |
... | ... |
@@ -1663,7 +2230,12 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
1663 | 2230 |
'--config', |
1664 | 2231 |
'store_config_only', |
1665 | 2232 |
is_flag=True, |
1666 |
- help='saves the given settings for SERVICE or global', |
|
2233 |
+ help=_msg.TranslatedString( |
|
2234 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_CONFIG_HELP_TEXT, |
|
2235 |
+ service_metavar=_msg.TranslatedString( |
|
2236 |
+ _msg.Label.VAULT_METAVAR_SERVICE |
|
2237 |
+ ), |
|
2238 |
+ ), |
|
1667 | 2239 |
cls=ConfigurationOption, |
1668 | 2240 |
) |
1669 | 2241 |
@click.option( |
... | ... |
@@ -1671,13 +2243,20 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
1671 | 2243 |
'--delete', |
1672 | 2244 |
'delete_service_settings', |
1673 | 2245 |
is_flag=True, |
1674 |
- help='deletes settings for SERVICE', |
|
2246 |
+ help=_msg.TranslatedString( |
|
2247 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_HELP_TEXT, |
|
2248 |
+ service_metavar=_msg.TranslatedString( |
|
2249 |
+ _msg.Label.VAULT_METAVAR_SERVICE |
|
2250 |
+ ), |
|
2251 |
+ ), |
|
1675 | 2252 |
cls=ConfigurationOption, |
1676 | 2253 |
) |
1677 | 2254 |
@click.option( |
1678 | 2255 |
'--delete-globals', |
1679 | 2256 |
is_flag=True, |
1680 |
- help='deletes the global shared settings', |
|
2257 |
+ help=_msg.TranslatedString( |
|
2258 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_GLOBALS_HELP_TEXT, |
|
2259 |
+ ), |
|
1681 | 2260 |
cls=ConfigurationOption, |
1682 | 2261 |
) |
1683 | 2262 |
@click.option( |
... | ... |
@@ -1685,30 +2264,48 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
1685 | 2264 |
'--clear', |
1686 | 2265 |
'clear_all_settings', |
1687 | 2266 |
is_flag=True, |
1688 |
- help='deletes all settings', |
|
2267 |
+ help=_msg.TranslatedString( |
|
2268 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_ALL_HELP_TEXT, |
|
2269 |
+ ), |
|
1689 | 2270 |
cls=ConfigurationOption, |
1690 | 2271 |
) |
1691 | 2272 |
@click.option( |
1692 | 2273 |
'-e', |
1693 | 2274 |
'--export', |
1694 | 2275 |
'export_settings', |
1695 |
- metavar='PATH', |
|
1696 |
- help='export all saved settings into file PATH', |
|
2276 |
+ metavar=_msg.TranslatedString( |
|
2277 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2278 |
+ ), |
|
2279 |
+ help=_msg.TranslatedString( |
|
2280 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_EXPORT_HELP_TEXT, |
|
2281 |
+ metavar=_msg.TranslatedString( |
|
2282 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2283 |
+ ), |
|
2284 |
+ ), |
|
1697 | 2285 |
cls=StorageManagementOption, |
1698 | 2286 |
) |
1699 | 2287 |
@click.option( |
1700 | 2288 |
'-i', |
1701 | 2289 |
'--import', |
1702 | 2290 |
'import_settings', |
1703 |
- metavar='PATH', |
|
1704 |
- help='import saved settings from file PATH', |
|
2291 |
+ metavar=_msg.TranslatedString( |
|
2292 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2293 |
+ ), |
|
2294 |
+ help=_msg.TranslatedString( |
|
2295 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_IMPORT_HELP_TEXT, |
|
2296 |
+ metavar=_msg.TranslatedString( |
|
2297 |
+ _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER |
|
2298 |
+ ), |
|
2299 |
+ ), |
|
1705 | 2300 |
cls=StorageManagementOption, |
1706 | 2301 |
) |
1707 | 2302 |
@click.option( |
1708 | 2303 |
'--overwrite-existing/--merge-existing', |
1709 | 2304 |
'overwrite_config', |
1710 | 2305 |
default=False, |
1711 |
- help='overwrite or merge (default) the existing configuration', |
|
2306 |
+ help=_msg.TranslatedString( |
|
2307 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_OVERWRITE_HELP_TEXT |
|
2308 |
+ ), |
|
1712 | 2309 |
cls=CompatibilityOption, |
1713 | 2310 |
) |
1714 | 2311 |
@click.option( |
... | ... |
@@ -1727,9 +2324,8 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
1727 | 2324 |
'dash', |
1728 | 2325 |
'symbol', |
1729 | 2326 |
]), |
1730 |
- help=( |
|
1731 |
- 'with --config, also unsets the given setting; ' |
|
1732 |
- 'may be specified multiple times' |
|
2327 |
+ help=_msg.TranslatedString( |
|
2328 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_UNSET_HELP_TEXT |
|
1733 | 2329 |
), |
1734 | 2330 |
cls=CompatibilityOption, |
1735 | 2331 |
) |
... | ... |
@@ -1737,12 +2333,19 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
1737 | 2333 |
'--export-as', |
1738 | 2334 |
type=click.Choice(['json', 'sh']), |
1739 | 2335 |
default='json', |
1740 |
- help='when exporting, export as JSON (default) or POSIX sh', |
|
2336 |
+ help=_msg.TranslatedString( |
|
2337 |
+ _msg.Label.DERIVEPASSPHRASE_VAULT_EXPORT_AS_HELP_TEXT |
|
2338 |
+ ), |
|
1741 | 2339 |
cls=CompatibilityOption, |
1742 | 2340 |
) |
1743 | 2341 |
@click.version_option(version=dpp.__version__, prog_name=PROG_NAME) |
1744 | 2342 |
@standard_logging_options |
1745 |
-@click.argument('service', required=False) |
|
2343 |
+@click.argument( |
|
2344 |
+ 'service', |
|
2345 |
+ metavar=_msg.TranslatedString(_msg.Label.VAULT_METAVAR_SERVICE), |
|
2346 |
+ required=False, |
|
2347 |
+ default=None, |
|
2348 |
+) |
|
1746 | 2349 |
@click.pass_context |
1747 | 2350 |
def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1748 | 2351 |
ctx: click.Context, |
... | ... |
@@ -1772,24 +2375,11 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1772 | 2375 |
) -> None: |
1773 | 2376 |
"""Derive a passphrase using the vault(1) derivation scheme. |
1774 | 2377 |
|
1775 |
- Using a master passphrase or a master SSH key, derive a passphrase |
|
1776 |
- for SERVICE, subject to length, character and character repetition |
|
1777 |
- constraints. The derivation is cryptographically strong, meaning |
|
1778 |
- that even if a single passphrase is compromised, guessing the master |
|
1779 |
- passphrase or a different service's passphrase is computationally |
|
1780 |
- infeasible. The derivation is also deterministic, given the same |
|
1781 |
- inputs, thus the resulting passphrase need not be stored explicitly. |
|
1782 |
- The service name and constraints themselves also need not be kept |
|
1783 |
- secret; the latter are usually stored in a world-readable file. |
|
1784 |
- |
|
1785 |
- If operating on global settings, or importing/exporting settings, |
|
1786 |
- then SERVICE must be omitted. Otherwise it is required.\f |
|
1787 |
- |
|
1788 | 2378 |
This is a [`click`][CLICK]-powered command-line interface function, |
1789 |
- and not intended for programmatic use. Call with arguments |
|
1790 |
- `['--help']` to see full documentation of the interface. (See also |
|
1791 |
- [`click.testing.CliRunner`][] for controlled, programmatic |
|
1792 |
- invocation.) |
|
2379 |
+ and not intended for programmatic use. See the |
|
2380 |
+ derivepassphrase-vault(1) manpage for full documentation of the |
|
2381 |
+ interface. (See also [`click.testing.CliRunner`][] for controlled, |
|
2382 |
+ programmatic invocation.) |
|
1793 | 2383 |
|
1794 | 2384 |
[CLICK]: https://pypi.org/package/click/ |
1795 | 2385 |
|
... | ... |
@@ -1874,9 +2464,10 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1874 | 2464 |
`--export`, selects the format to export the current |
1875 | 2465 |
configuration as: JSON ("json", default) or POSIX sh ("sh"). |
1876 | 2466 |
|
1877 |
- """ # noqa: D301 |
|
2467 |
+ """ |
|
1878 | 2468 |
logger = logging.getLogger(PROG_NAME) |
1879 | 2469 |
deprecation = logging.getLogger(PROG_NAME + '.deprecation') |
2470 |
+ service_metavar = _msg.TranslatedString(_msg.Label.VAULT_METAVAR_SERVICE) |
|
1880 | 2471 |
options_in_group: dict[type[click.Option], list[click.Option]] = {} |
1881 | 2472 |
params_by_str: dict[str, click.Parameter] = {} |
1882 | 2473 |
for param in ctx.command.params: |
... | ... |
@@ -1934,7 +2525,13 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1934 | 2525 |
param2_str = option_name(param2) |
1935 | 2526 |
raise click.BadOptionUsage( |
1936 | 2527 |
param1_str, |
1937 |
- f'{param1_str} is mutually exclusive with {param2_str}', |
|
2528 |
+ str( |
|
2529 |
+ _msg.TranslatedString( |
|
2530 |
+ _msg.ErrMsgTemplate.PARAMS_MUTUALLY_EXCLUSIVE, |
|
2531 |
+ param1=param1_str, |
|
2532 |
+ param2=param2_str, |
|
2533 |
+ ) |
|
2534 |
+ ), |
|
1938 | 2535 |
ctx=ctx, |
1939 | 2536 |
) |
1940 | 2537 |
return |
... | ... |
@@ -1958,37 +2555,67 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1958 | 2555 |
) |
1959 | 2556 |
new_name = os.path.basename(_config_filename(subsystem='vault')) |
1960 | 2557 |
deprecation.warning( |
1961 |
- ( |
|
1962 |
- 'Using deprecated v0.1-style config file %r, ' |
|
1963 |
- 'instead of v0.2-style %r. ' |
|
1964 |
- 'Support for v0.1-style config filenames will be ' |
|
1965 |
- 'removed in v1.0.' |
|
2558 |
+ _msg.TranslatedString( |
|
2559 |
+ _msg.WarnMsgTemplate.V01_STYLE_CONFIG, |
|
2560 |
+ old=old_name, |
|
2561 |
+ new=new_name, |
|
1966 | 2562 |
), |
1967 |
- old_name, |
|
1968 |
- new_name, |
|
1969 | 2563 |
) |
1970 | 2564 |
if isinstance(exc, OSError): |
1971 | 2565 |
logger.warning( |
1972 |
- 'Failed to migrate to %r: %s: %r', |
|
1973 |
- new_name, |
|
1974 |
- exc.strerror, |
|
1975 |
- exc.filename, |
|
2566 |
+ _msg.TranslatedString( |
|
2567 |
+ _msg.WarnMsgTemplate.FAILED_TO_MIGRATE_CONFIG, |
|
2568 |
+ path=new_name, |
|
2569 |
+ error=exc.strerror, |
|
2570 |
+ filename=exc.filename, |
|
2571 |
+ ).maybe_without_filename(), |
|
1976 | 2572 |
) |
1977 | 2573 |
else: |
1978 |
- deprecation.info('Successfully migrated to %r.', new_name) |
|
2574 |
+ deprecation.info( |
|
2575 |
+ _msg.TranslatedString( |
|
2576 |
+ _msg.InfoMsgTemplate.SUCCESSFULLY_MIGRATED, |
|
2577 |
+ path=new_name, |
|
2578 |
+ ), |
|
2579 |
+ ) |
|
1979 | 2580 |
return backup_config |
1980 | 2581 |
except OSError as exc: |
1981 |
- err('Cannot load config: %s: %r', exc.strerror, exc.filename) |
|
2582 |
+ err( |
|
2583 |
+ _msg.TranslatedString( |
|
2584 |
+ _msg.ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS, |
|
2585 |
+ error=exc.strerror, |
|
2586 |
+ filename=exc.filename, |
|
2587 |
+ ).maybe_without_filename(), |
|
2588 |
+ ) |
|
1982 | 2589 |
except Exception as exc: # noqa: BLE001 |
1983 |
- err('Cannot load config: %s', str(exc), exc_info=exc) |
|
2590 |
+ err( |
|
2591 |
+ _msg.TranslatedString( |
|
2592 |
+ _msg.ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS, |
|
2593 |
+ error=str(exc), |
|
2594 |
+ filename=None, |
|
2595 |
+ ).maybe_without_filename(), |
|
2596 |
+ exc_info=exc, |
|
2597 |
+ ) |
|
1984 | 2598 |
|
1985 | 2599 |
def put_config(config: _types.VaultConfig, /) -> None: |
1986 | 2600 |
try: |
1987 | 2601 |
_save_config(config) |
1988 | 2602 |
except OSError as exc: |
1989 |
- err('Cannot store config: %s: %r', exc.strerror, exc.filename) |
|
2603 |
+ err( |
|
2604 |
+ _msg.TranslatedString( |
|
2605 |
+ _msg.ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS, |
|
2606 |
+ error=exc.strerror, |
|
2607 |
+ filename=exc.filename, |
|
2608 |
+ ).maybe_without_filename(), |
|
2609 |
+ ) |
|
1990 | 2610 |
except Exception as exc: # noqa: BLE001 |
1991 |
- err('Cannot store config: %s', str(exc), exc_info=exc) |
|
2611 |
+ err( |
|
2612 |
+ _msg.TranslatedString( |
|
2613 |
+ _msg.ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS, |
|
2614 |
+ error=str(exc), |
|
2615 |
+ filename=None, |
|
2616 |
+ ).maybe_without_filename(), |
|
2617 |
+ exc_info=exc, |
|
2618 |
+ ) |
|
1992 | 2619 |
|
1993 | 2620 |
def get_user_config() -> dict[str, Any]: |
1994 | 2621 |
try: |
... | ... |
@@ -1996,9 +2623,22 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1996 | 2623 |
except FileNotFoundError: |
1997 | 2624 |
return {} |
1998 | 2625 |
except OSError as exc: |
1999 |
- err('Cannot load user config: %s: %r', exc.strerror, exc.filename) |
|
2626 |
+ err( |
|
2627 |
+ _msg.TranslatedString( |
|
2628 |
+ _msg.ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG, |
|
2629 |
+ error=exc.strerror, |
|
2630 |
+ filename=exc.filename, |
|
2631 |
+ ).maybe_without_filename(), |
|
2632 |
+ ) |
|
2000 | 2633 |
except Exception as exc: # noqa: BLE001 |
2001 |
- err('Cannot load user config: %s', str(exc), exc_info=exc) |
|
2634 |
+ err( |
|
2635 |
+ _msg.TranslatedString( |
|
2636 |
+ _msg.ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG, |
|
2637 |
+ error=str(exc), |
|
2638 |
+ filename=None, |
|
2639 |
+ ).maybe_without_filename(), |
|
2640 |
+ exc_info=exc, |
|
2641 |
+ ) |
|
2002 | 2642 |
|
2003 | 2643 |
configuration: _types.VaultConfig |
2004 | 2644 |
|
... | ... |
@@ -2020,15 +2660,21 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2020 | 2660 |
if is_param_set(param) and not ( |
2021 | 2661 |
service or is_param_set(params_by_str['--config']) |
2022 | 2662 |
): |
2023 |
- opt_str = param.opts[0] |
|
2024 |
- msg = f'{opt_str} requires a SERVICE or --config' |
|
2025 |
- raise click.UsageError(msg) # noqa: DOC501 |
|
2663 |
+ err_msg = _msg.TranslatedString( |
|
2664 |
+ _msg.ErrMsgTemplate.PARAMS_NEEDS_SERVICE_OR_CONFIG, |
|
2665 |
+ param=param.opts[0], |
|
2666 |
+ service_metavar=service_metavar, |
|
2667 |
+ ) |
|
2668 |
+ raise click.UsageError(str(err_msg)) # noqa: DOC501 |
|
2026 | 2669 |
sv_options = [params_by_str['--notes'], params_by_str['--delete']] |
2027 | 2670 |
for param in sv_options: |
2028 | 2671 |
if is_param_set(param) and not service: |
2029 |
- opt_str = param.opts[0] |
|
2030 |
- msg = f'{opt_str} requires a SERVICE' |
|
2031 |
- raise click.UsageError(msg) |
|
2672 |
+ err_msg = _msg.TranslatedString( |
|
2673 |
+ _msg.ErrMsgTemplate.PARAMS_NEEDS_SERVICE, |
|
2674 |
+ param=param.opts[0], |
|
2675 |
+ service_metavar=service_metavar, |
|
2676 |
+ ) |
|
2677 |
+ raise click.UsageError(str(err_msg)) |
|
2032 | 2678 |
no_sv_options = [ |
2033 | 2679 |
params_by_str['--delete-globals'], |
2034 | 2680 |
params_by_str['--clear'], |
... | ... |
@@ -2036,18 +2682,21 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2036 | 2682 |
] |
2037 | 2683 |
for param in no_sv_options: |
2038 | 2684 |
if is_param_set(param) and service: |
2039 |
- opt_str = param.opts[0] |
|
2040 |
- msg = f'{opt_str} does not take a SERVICE argument' |
|
2041 |
- raise click.UsageError(msg) |
|
2685 |
+ err_msg = _msg.TranslatedString( |
|
2686 |
+ _msg.ErrMsgTemplate.PARAMS_NO_SERVICE, |
|
2687 |
+ param=param.opts[0], |
|
2688 |
+ service_metavar=service_metavar, |
|
2689 |
+ ) |
|
2690 |
+ raise click.UsageError(str(err_msg)) |
|
2042 | 2691 |
|
2043 | 2692 |
user_config = get_user_config() |
2044 | 2693 |
|
2045 | 2694 |
if service == '': # noqa: PLC1901 |
2046 | 2695 |
logger.warning( |
2047 |
- 'An empty SERVICE is not supported by vault(1). ' |
|
2048 |
- 'For compatibility, this will be treated as if SERVICE ' |
|
2049 |
- 'was not supplied, i.e., it will error out, or ' |
|
2050 |
- 'operate on global settings.' |
|
2696 |
+ _msg.TranslatedString( |
|
2697 |
+ _msg.WarnMsgTemplate.EMPTY_SERVICE_NOT_SUPPORTED, |
|
2698 |
+ service_metavar=service_metavar, |
|
2699 |
+ ) |
|
2051 | 2700 |
) |
2052 | 2701 |
|
2053 | 2702 |
if edit_notes: |
... | ... |
@@ -2066,7 +2715,11 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2066 | 2715 |
break |
2067 | 2716 |
else: |
2068 | 2717 |
if not notes_value.strip(): |
2069 |
- err('Not saving new notes: user aborted request') |
|
2718 |
+ err( |
|
2719 |
+ _msg.TranslatedString( |
|
2720 |
+ _msg.ErrMsgTemplate.USER_ABORTED_EDIT |
|
2721 |
+ ) |
|
2722 |
+ ) |
|
2070 | 2723 |
configuration['services'].setdefault(service, {})['notes'] = ( |
2071 | 2724 |
notes_value.strip('\n') |
2072 | 2725 |
) |
... | ... |
@@ -2104,12 +2757,32 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2104 | 2757 |
with infile: |
2105 | 2758 |
maybe_config = json.load(infile) |
2106 | 2759 |
except json.JSONDecodeError as exc: |
2107 |
- err('Cannot load config: cannot decode JSON: %s', exc) |
|
2760 |
+ err( |
|
2761 |
+ _msg.TranslatedString( |
|
2762 |
+ _msg.ErrMsgTemplate.CANNOT_DECODEIMPORT_VAULT_SETTINGS, |
|
2763 |
+ error=exc, |
|
2764 |
+ ) |
|
2765 |
+ ) |
|
2108 | 2766 |
except OSError as exc: |
2109 |
- err('Cannot load config: %s: %r', exc.strerror, exc.filename) |
|
2767 |
+ err( |
|
2768 |
+ _msg.TranslatedString( |
|
2769 |
+ _msg.ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS, |
|
2770 |
+ error=exc.strerror, |
|
2771 |
+ filename=exc.filename, |
|
2772 |
+ ).maybe_without_filename() |
|
2773 |
+ ) |
|
2110 | 2774 |
cleaned = _types.clean_up_falsy_vault_config_values(maybe_config) |
2111 | 2775 |
if not _types.is_vault_config(maybe_config): |
2112 |
- err('Cannot load config: %s', _INVALID_VAULT_CONFIG) |
|
2776 |
+ err( |
|
2777 |
+ _msg.TranslatedString( |
|
2778 |
+ _msg.ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS, |
|
2779 |
+ error=_msg.TranslatedString( |
|
2780 |
+ _msg.ErrMsgTemplate.INVALID_VAULT_CONFIG, |
|
2781 |
+ config=maybe_config, |
|
2782 |
+ ), |
|
2783 |
+ filename=None, |
|
2784 |
+ ).maybe_without_filename() |
|
2785 |
+ ) |
|
2113 | 2786 |
assert cleaned is not None |
2114 | 2787 |
for step in cleaned: |
2115 | 2788 |
# These are never fatal errors, because the semantics of |
... | ... |
@@ -2117,27 +2790,28 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2117 | 2790 |
# but not ill-defined. |
2118 | 2791 |
if step.action == 'replace': |
2119 | 2792 |
logger.warning( |
2120 |
- 'Replacing invalid value %s for key %s with %s.', |
|
2121 |
- json.dumps(step.old_value), |
|
2122 |
- _types.json_path(step.path), |
|
2123 |
- json.dumps(step.new_value), |
|
2793 |
+ _msg.TranslatedString( |
|
2794 |
+ _msg.WarnMsgTemplate.STEP_REPLACE_INVALID_VALUE, |
|
2795 |
+ old=json.dumps(step.old_value), |
|
2796 |
+ path=_types.json_path(step.path), |
|
2797 |
+ new=json.dumps(step.new_value), |
|
2798 |
+ ), |
|
2124 | 2799 |
) |
2125 | 2800 |
else: |
2126 | 2801 |
logger.warning( |
2127 |
- 'Removing ineffective setting %s = %s.', |
|
2128 |
- _types.json_path(step.path), |
|
2129 |
- json.dumps(step.old_value), |
|
2802 |
+ _msg.TranslatedString( |
|
2803 |
+ _msg.WarnMsgTemplate.STEP_REMOVE_INEFFECTIVE_VALUE, |
|
2804 |
+ path=_types.json_path(step.path), |
|
2805 |
+ old=json.dumps(step.old_value), |
|
2806 |
+ ), |
|
2130 | 2807 |
) |
2131 | 2808 |
if '' in maybe_config['services']: |
2132 | 2809 |
logger.warning( |
2133 |
- ( |
|
2134 |
- 'An empty SERVICE is not supported by vault(1), ' |
|
2135 |
- 'and the empty-string service settings will be ' |
|
2136 |
- 'inaccessible and ineffective. To ensure that ' |
|
2137 |
- 'vault(1) and %s see the settings, move them ' |
|
2138 |
- 'into the "global" section.' |
|
2810 |
+ _msg.TranslatedString( |
|
2811 |
+ _msg.WarnMsgTemplate.EMPTY_SERVICE_SETTINGS_INACCESSIBLE, |
|
2812 |
+ service_metavar=service_metavar, |
|
2813 |
+ PROG_NAME=PROG_NAME, |
|
2139 | 2814 |
), |
2140 |
- PROG_NAME, |
|
2141 | 2815 |
) |
2142 | 2816 |
try: |
2143 | 2817 |
_check_for_misleading_passphrase( |
... | ... |
@@ -2152,14 +2826,21 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2152 | 2826 |
main_config=user_config, |
2153 | 2827 |
) |
2154 | 2828 |
except AssertionError as exc: |
2155 |
- err('The configuration file is invalid. ' + str(exc)) |
|
2829 |
+ err( |
|
2830 |
+ _msg.TranslatedString( |
|
2831 |
+ _msg.ErrMsgTemplate.INVALID_USER_CONFIG, |
|
2832 |
+ error=exc, |
|
2833 |
+ filename=None, |
|
2834 |
+ ).maybe_without_filename(), |
|
2835 |
+ ) |
|
2156 | 2836 |
global_obj = maybe_config.get('global', {}) |
2157 | 2837 |
has_key = _types.js_truthiness(global_obj.get('key')) |
2158 | 2838 |
has_phrase = _types.js_truthiness(global_obj.get('phrase')) |
2159 | 2839 |
if has_key and has_phrase: |
2160 | 2840 |
logger.warning( |
2161 |
- 'Setting a global passphrase is ineffective ' |
|
2162 |
- 'because a key is also set.' |
|
2841 |
+ _msg.TranslatedString( |
|
2842 |
+ _msg.WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE, |
|
2843 |
+ ) |
|
2163 | 2844 |
) |
2164 | 2845 |
for service_name, service_obj in maybe_config['services'].items(): |
2165 | 2846 |
has_key = _types.js_truthiness( |
... | ... |
@@ -2170,11 +2851,10 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2170 | 2851 |
) or _types.js_truthiness(global_obj.get('phrase')) |
2171 | 2852 |
if has_key and has_phrase: |
2172 | 2853 |
logger.warning( |
2173 |
- ( |
|
2174 |
- 'Setting a service passphrase is ineffective ' |
|
2175 |
- 'because a key is also set: %s' |
|
2854 |
+ _msg.TranslatedString( |
|
2855 |
+ _msg.WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE, |
|
2856 |
+ service=json.dumps(service_name), |
|
2176 | 2857 |
), |
2177 |
- json.dumps(service_name), |
|
2178 | 2858 |
) |
2179 | 2859 |
if overwrite_config: |
2180 | 2860 |
put_config(maybe_config) |
... | ... |
@@ -2240,7 +2920,13 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2240 | 2920 |
else: |
2241 | 2921 |
json.dump(configuration, outfile) |
2242 | 2922 |
except OSError as exc: |
2243 |
- err('Cannot store config: %s: %r', exc.strerror, exc.filename) |
|
2923 |
+ err( |
|
2924 |
+ _msg.TranslatedString( |
|
2925 |
+ _msg.ErrMsgTemplate.CANNOT_EXPORT_VAULT_SETTINGS, |
|
2926 |
+ error=exc.strerror, |
|
2927 |
+ filename=exc.filename, |
|
2928 |
+ ).maybe_without_filename(), |
|
2929 |
+ ) |
|
2244 | 2930 |
else: |
2245 | 2931 |
configuration = get_config() |
2246 | 2932 |
# This block could be type checked more stringently, but this |
... | ... |
@@ -2278,26 +2964,56 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2278 | 2964 |
'ASCII' |
2279 | 2965 |
) |
2280 | 2966 |
except IndexError: |
2281 |
- err('No valid SSH key selected') |
|
2967 |
+ err( |
|
2968 |
+ _msg.TranslatedString( |
|
2969 |
+ _msg.ErrMsgTemplate.USER_ABORTED_SSH_KEY_SELECTION |
|
2970 |
+ ), |
|
2971 |
+ ) |
|
2282 | 2972 |
except KeyError: |
2283 |
- err('Cannot find running SSH agent; check SSH_AUTH_SOCK') |
|
2284 |
- except NotImplementedError: |
|
2285 | 2973 |
err( |
2286 |
- 'Cannot connect to SSH agent because ' |
|
2287 |
- 'this Python version does not support UNIX domain sockets' |
|
2974 |
+ _msg.TranslatedString( |
|
2975 |
+ _msg.ErrMsgTemplate.NO_SSH_AGENT_FOUND |
|
2976 |
+ ), |
|
2977 |
+ ) |
|
2978 |
+ except LookupError: |
|
2979 |
+ err( |
|
2980 |
+ _msg.TranslatedString( |
|
2981 |
+ _msg.ErrMsgTemplate.NO_SUITABLE_SSH_KEYS, |
|
2982 |
+ PROG_NAME=PROG_NAME, |
|
2288 | 2983 |
) |
2984 |
+ ) |
|
2985 |
+ except NotImplementedError: |
|
2986 |
+ err(_msg.TranslatedString(_msg.ErrMsgTemplate.NO_AF_UNIX)) |
|
2289 | 2987 |
except OSError as exc: |
2290 |
- err('Cannot connect to SSH agent: %s', exc.strerror) |
|
2291 |
- except ( |
|
2292 |
- LookupError, |
|
2293 |
- RuntimeError, |
|
2294 |
- ssh_agent.SSHAgentFailedError, |
|
2295 |
- ) as exc: |
|
2296 |
- err(str(exc)) |
|
2988 |
+ err( |
|
2989 |
+ _msg.TranslatedString( |
|
2990 |
+ _msg.ErrMsgTemplate.CANNOT_CONNECT_TO_AGENT, |
|
2991 |
+ error=exc.strerror, |
|
2992 |
+ filename=exc.filename, |
|
2993 |
+ ).maybe_without_filename(), |
|
2994 |
+ ) |
|
2995 |
+ except ssh_agent.SSHAgentFailedError as exc: |
|
2996 |
+ err( |
|
2997 |
+ _msg.TranslatedString( |
|
2998 |
+ _msg.ErrMsgTemplate.AGENT_REFUSED_LIST_KEYS |
|
2999 |
+ ), |
|
3000 |
+ exc_info=exc, |
|
3001 |
+ ) |
|
3002 |
+ except RuntimeError as exc: |
|
3003 |
+ err( |
|
3004 |
+ _msg.TranslatedString( |
|
3005 |
+ _msg.ErrMsgTemplate.CANNOT_UNDERSTAND_AGENT |
|
3006 |
+ ), |
|
3007 |
+ exc_info=exc, |
|
3008 |
+ ) |
|
2297 | 3009 |
elif use_phrase: |
2298 | 3010 |
maybe_phrase = _prompt_for_passphrase() |
2299 | 3011 |
if not maybe_phrase: |
2300 |
- err('No passphrase given') |
|
3012 |
+ err( |
|
3013 |
+ _msg.TranslatedString( |
|
3014 |
+ _msg.ErrMsgTemplate.USER_ABORTED_PASSPHRASE |
|
3015 |
+ ) |
|
3016 |
+ ) |
|
2301 | 3017 |
else: |
2302 | 3018 |
phrase = maybe_phrase |
2303 | 3019 |
if store_config_only: |
... | ... |
@@ -2319,35 +3035,41 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2319 | 3035 |
main_config=user_config, |
2320 | 3036 |
) |
2321 | 3037 |
except AssertionError as exc: |
2322 |
- err('The configuration file is invalid. ' + str(exc)) |
|
3038 |
+ err( |
|
3039 |
+ _msg.TranslatedString( |
|
3040 |
+ _msg.ErrMsgTemplate.INVALID_USER_CONFIG, |
|
3041 |
+ error=exc, |
|
3042 |
+ filename=None, |
|
3043 |
+ ).maybe_without_filename(), |
|
3044 |
+ ) |
|
2323 | 3045 |
if 'key' in settings: |
2324 | 3046 |
if service: |
2325 | 3047 |
logger.warning( |
2326 |
- ( |
|
2327 |
- 'Setting a service passphrase is ineffective ' |
|
2328 |
- 'because a key is also set: %s' |
|
2329 |
- ), |
|
2330 |
- json.dumps(service), |
|
3048 |
+ _msg.TranslatedString( |
|
3049 |
+ _msg.WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE, |
|
3050 |
+ service=json.dumps(service), |
|
3051 |
+ ) |
|
2331 | 3052 |
) |
2332 | 3053 |
else: |
2333 | 3054 |
logger.warning( |
2334 |
- 'Setting a global passphrase is ineffective ' |
|
2335 |
- 'because a key is also set.' |
|
3055 |
+ _msg.TranslatedString( |
|
3056 |
+ _msg.WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE |
|
3057 |
+ ) |
|
2336 | 3058 |
) |
2337 | 3059 |
if not view.maps[0] and not unset_settings: |
2338 | 3060 |
settings_type = 'service' if service else 'global' |
2339 |
- msg = ( |
|
2340 |
- f'Cannot update {settings_type} settings without ' |
|
2341 |
- f'actual settings' |
|
3061 |
+ err_msg = _msg.TranslatedString( |
|
3062 |
+ _msg.ErrMsgTemplate.CANNOT_UPDATE_SETTINGS_NO_SETTINGS, |
|
3063 |
+ settings_type=settings_type, |
|
2342 | 3064 |
) |
2343 |
- raise click.UsageError(msg) |
|
3065 |
+ raise click.UsageError(str(err_msg)) |
|
2344 | 3066 |
for setting in unset_settings: |
2345 | 3067 |
if setting in view.maps[0]: |
2346 |
- msg = ( |
|
2347 |
- f'Attempted to unset and set --{setting} ' |
|
2348 |
- f'at the same time.' |
|
3068 |
+ err_msg = _msg.TranslatedString( |
|
3069 |
+ _msg.ErrMsgTemplate.SET_AND_UNSET_SAME_SETTING, |
|
3070 |
+ setting=setting, |
|
2349 | 3071 |
) |
2350 |
- raise click.UsageError(msg) |
|
3072 |
+ raise click.UsageError(str(err_msg)) |
|
2351 | 3073 |
subtree: dict[str, Any] = ( |
2352 | 3074 |
configuration['services'].setdefault(service, {}) # type: ignore[assignment] |
2353 | 3075 |
if service |
... | ... |
@@ -2365,8 +3087,13 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2365 | 3087 |
put_config(configuration) |
2366 | 3088 |
else: |
2367 | 3089 |
if not service: |
2368 |
- msg = 'SERVICE is required' |
|
2369 |
- raise click.UsageError(msg) |
|
3090 |
+ err_msg = _msg.TranslatedString( |
|
3091 |
+ _msg.ErrMsgTemplate.SERVICE_REQUIRED, |
|
3092 |
+ service_metavar=_msg.TranslatedString( |
|
3093 |
+ _msg.Label.VAULT_METAVAR_SERVICE |
|
3094 |
+ ), |
|
3095 |
+ ) |
|
3096 |
+ raise click.UsageError(str(err_msg)) |
|
2370 | 3097 |
kwargs: dict[str, Any] = { |
2371 | 3098 |
k: v |
2372 | 3099 |
for k, v in settings.items() |
... | ... |
@@ -2381,7 +3108,13 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2381 | 3108 |
main_config=user_config, |
2382 | 3109 |
) |
2383 | 3110 |
except AssertionError as exc: |
2384 |
- err('The configuration file is invalid. ' + str(exc)) |
|
3111 |
+ err( |
|
3112 |
+ _msg.TranslatedString( |
|
3113 |
+ _msg.ErrMsgTemplate.INVALID_USER_CONFIG, |
|
3114 |
+ error=exc, |
|
3115 |
+ filename=None, |
|
3116 |
+ ).maybe_without_filename(), |
|
3117 |
+ ) |
|
2385 | 3118 |
|
2386 | 3119 |
# If either --key or --phrase are given, use that setting. |
2387 | 3120 |
# Otherwise, if both key and phrase are set in the config, |
... | ... |
@@ -2402,11 +3135,10 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
2402 | 3135 |
elif kwargs.get('phrase'): |
2403 | 3136 |
pass |
2404 | 3137 |
else: |
2405 |
- msg = ( |
|
2406 |
- 'No passphrase or key given on command-line ' |
|
2407 |
- 'or in configuration' |
|
3138 |
+ err_msg = _msg.TranslatedString( |
|
3139 |
+ _msg.ErrMsgTemplate.NO_KEY_OR_PHRASE |
|
2408 | 3140 |
) |
2409 |
- raise click.UsageError(msg) |
|
3141 |
+ raise click.UsageError(str(err_msg)) |
|
2410 | 3142 |
kwargs.pop('key', '') |
2411 | 3143 |
result = vault.Vault(**kwargs).generate(service) |
2412 | 3144 |
click.echo(result.decode('ASCII')) |
... | ... |
@@ -295,7 +295,7 @@ class TestCLI: |
295 | 295 |
empty_stderr=True, output='Passphrase generation:\n' |
296 | 296 |
), 'expected clean exit, and option groups in help text' |
297 | 297 |
assert result.clean_exit( |
298 |
- empty_stderr=True, output='Use NUMBER=0, e.g. "--symbol 0"' |
|
298 |
+ empty_stderr=True, output='Use $VISUAL or $EDITOR to configure' |
|
299 | 299 |
), 'expected clean exit, and option group epilog in help text' |
300 | 300 |
|
301 | 301 |
@pytest.mark.parametrize( |
... | ... |
@@ -971,7 +971,7 @@ class TestCLI: |
971 | 971 |
) |
972 | 972 |
result = tests.ReadableResult.parse(_result) |
973 | 973 |
assert result.error_exit( |
974 |
- error='Cannot load config' |
|
974 |
+ error='Cannot load vault settings:' |
|
975 | 975 |
), 'expected error exit and known error message' |
976 | 976 |
|
977 | 977 |
@pytest.mark.parametrize( |
... | ... |
@@ -999,7 +999,7 @@ class TestCLI: |
999 | 999 |
) |
1000 | 1000 |
result = tests.ReadableResult.parse(_result) |
1001 | 1001 |
assert result.error_exit( |
1002 |
- error='Cannot load config' |
|
1002 |
+ error='Cannot load vault settings:' |
|
1003 | 1003 |
), 'expected error exit and known error message' |
1004 | 1004 |
|
1005 | 1005 |
@pytest.mark.parametrize( |
... | ... |
@@ -1025,7 +1025,7 @@ class TestCLI: |
1025 | 1025 |
) |
1026 | 1026 |
result = tests.ReadableResult.parse(_result) |
1027 | 1027 |
assert result.error_exit( |
1028 |
- error='Cannot store config' |
|
1028 |
+ error='Cannot export vault settings:' |
|
1029 | 1029 |
), 'expected error exit and known error message' |
1030 | 1030 |
|
1031 | 1031 |
@pytest.mark.parametrize( |
... | ... |
@@ -1054,9 +1054,9 @@ class TestCLI: |
1054 | 1054 |
) |
1055 | 1055 |
result = tests.ReadableResult.parse(_result) |
1056 | 1056 |
assert result.error_exit( |
1057 |
- error='Cannot load config' |
|
1057 |
+ error='Cannot load vault settings:' |
|
1058 | 1058 |
) or result.error_exit( |
1059 |
- error='Cannot load user config' |
|
1059 |
+ error='Cannot load user config:' |
|
1060 | 1060 |
), 'expected error exit and known error message' |
1061 | 1061 |
|
1062 | 1062 |
def test_220_edit_notes_successfully( |
... | ... |
@@ -1156,7 +1156,7 @@ contents go here |
1156 | 1156 |
) |
1157 | 1157 |
result = tests.ReadableResult.parse(_result) |
1158 | 1158 |
assert result.error_exit( |
1159 |
- error='user aborted request' |
|
1159 |
+ error='the user aborted the request' |
|
1160 | 1160 |
), 'expected known error message' |
1161 | 1161 |
with open( |
1162 | 1162 |
cli._config_filename(subsystem='vault'), encoding='UTF-8' |
... | ... |
@@ -1241,14 +1241,18 @@ contents go here |
1241 | 1241 |
@pytest.mark.parametrize( |
1242 | 1242 |
['command_line', 'input', 'err_text'], |
1243 | 1243 |
[ |
1244 |
- ([], '', 'Cannot update global settings without actual settings'), |
|
1244 |
+ ( |
|
1245 |
+ [], |
|
1246 |
+ '', |
|
1247 |
+ 'Cannot update global settings without any given settings', |
|
1248 |
+ ), |
|
1245 | 1249 |
( |
1246 | 1250 |
['--', 'sv'], |
1247 | 1251 |
'', |
1248 |
- 'Cannot update service settings without actual settings', |
|
1252 |
+ 'Cannot update service settings without any given settings', |
|
1249 | 1253 |
), |
1250 |
- (['--phrase', '--', 'sv'], '', 'No passphrase given'), |
|
1251 |
- (['--key'], '', 'No valid SSH key selected'), |
|
1254 |
+ (['--phrase', '--', 'sv'], '', 'No passphrase was given'), |
|
1255 |
+ (['--key'], '', 'No SSH key was selected'), |
|
1252 | 1256 |
], |
1253 | 1257 |
) |
1254 | 1258 |
def test_225_store_config_fail( |
... | ... |
@@ -1324,7 +1328,7 @@ contents go here |
1324 | 1328 |
) |
1325 | 1329 |
result = tests.ReadableResult.parse(_result) |
1326 | 1330 |
assert result.error_exit( |
1327 |
- error='Cannot find running SSH agent' |
|
1331 |
+ error='Cannot find any running SSH agent' |
|
1328 | 1332 |
), 'expected error exit and known error message' |
1329 | 1333 |
|
1330 | 1334 |
def test_225c_store_config_fail_manual_bad_ssh_agent_connection( |
... | ... |
@@ -1345,7 +1349,7 @@ contents go here |
1345 | 1349 |
) |
1346 | 1350 |
result = tests.ReadableResult.parse(_result) |
1347 | 1351 |
assert result.error_exit( |
1348 |
- error='Cannot connect to SSH agent' |
|
1352 |
+ error='Cannot connect to the SSH agent' |
|
1349 | 1353 |
), 'expected error exit and known error message' |
1350 | 1354 |
|
1351 | 1355 |
@pytest.mark.parametrize('try_race_free_implementation', [True, False]) |
... | ... |
@@ -1371,7 +1375,7 @@ contents go here |
1371 | 1375 |
) |
1372 | 1376 |
result = tests.ReadableResult.parse(_result) |
1373 | 1377 |
assert result.error_exit( |
1374 |
- error='Cannot store config' |
|
1378 |
+ error='Cannot store vault settings:' |
|
1375 | 1379 |
), 'expected error exit and known error message' |
1376 | 1380 |
|
1377 | 1381 |
def test_225e_store_config_fail_manual_custom_error( |
... | ... |
@@ -1427,6 +1431,89 @@ contents go here |
1427 | 1431 |
error='Attempted to unset and set --length at the same time.' |
1428 | 1432 |
), 'expected error exit and known error message' |
1429 | 1433 |
|
1434 |
+ def test_225g_store_config_fail_manual_ssh_agent_no_keys_loaded( |
|
1435 |
+ self, |
|
1436 |
+ monkeypatch: pytest.MonkeyPatch, |
|
1437 |
+ running_ssh_agent: tests.RunningSSHAgentInfo, |
|
1438 |
+ ) -> None: |
|
1439 |
+ del running_ssh_agent |
|
1440 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
1441 |
+ with tests.isolated_vault_config( |
|
1442 |
+ monkeypatch=monkeypatch, |
|
1443 |
+ runner=runner, |
|
1444 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1445 |
+ ): |
|
1446 |
+ def func( |
|
1447 |
+ *_args: Any, |
|
1448 |
+ **_kwargs: Any, |
|
1449 |
+ ) -> list[_types.KeyCommentPair]: |
|
1450 |
+ return [] |
|
1451 |
+ |
|
1452 |
+ monkeypatch.setattr(ssh_agent.SSHAgentClient, 'list_keys', func) |
|
1453 |
+ _result = runner.invoke( |
|
1454 |
+ cli.derivepassphrase_vault, |
|
1455 |
+ ['--key', '--config'], |
|
1456 |
+ catch_exceptions=False, |
|
1457 |
+ ) |
|
1458 |
+ result = tests.ReadableResult.parse(_result) |
|
1459 |
+ assert result.error_exit( |
|
1460 |
+ error='no keys suitable' |
|
1461 |
+ ), 'expected error exit and known error message' |
|
1462 |
+ |
|
1463 |
+ def test_225h_store_config_fail_manual_ssh_agent_runtime_error( |
|
1464 |
+ self, |
|
1465 |
+ monkeypatch: pytest.MonkeyPatch, |
|
1466 |
+ running_ssh_agent: tests.RunningSSHAgentInfo, |
|
1467 |
+ ) -> None: |
|
1468 |
+ del running_ssh_agent |
|
1469 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
1470 |
+ with tests.isolated_vault_config( |
|
1471 |
+ monkeypatch=monkeypatch, |
|
1472 |
+ runner=runner, |
|
1473 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1474 |
+ ): |
|
1475 |
+ def raiser(*_args: Any, **_kwargs: Any) -> None: |
|
1476 |
+ raise ssh_agent.TrailingDataError() |
|
1477 |
+ |
|
1478 |
+ monkeypatch.setattr(ssh_agent.SSHAgentClient, 'list_keys', raiser) |
|
1479 |
+ _result = runner.invoke( |
|
1480 |
+ cli.derivepassphrase_vault, |
|
1481 |
+ ['--key', '--config'], |
|
1482 |
+ catch_exceptions=False, |
|
1483 |
+ ) |
|
1484 |
+ result = tests.ReadableResult.parse(_result) |
|
1485 |
+ assert result.error_exit( |
|
1486 |
+ error='violates the communications protocol.' |
|
1487 |
+ ), 'expected error exit and known error message' |
|
1488 |
+ |
|
1489 |
+ def test_225i_store_config_fail_manual_ssh_agent_refuses( |
|
1490 |
+ self, |
|
1491 |
+ monkeypatch: pytest.MonkeyPatch, |
|
1492 |
+ running_ssh_agent: tests.RunningSSHAgentInfo, |
|
1493 |
+ ) -> None: |
|
1494 |
+ del running_ssh_agent |
|
1495 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
1496 |
+ with tests.isolated_vault_config( |
|
1497 |
+ monkeypatch=monkeypatch, |
|
1498 |
+ runner=runner, |
|
1499 |
+ vault_config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
1500 |
+ ): |
|
1501 |
+ def func(*_args: Any, **_kwargs: Any) -> NoReturn: |
|
1502 |
+ raise ssh_agent.SSHAgentFailedError( |
|
1503 |
+ _types.SSH_AGENT.FAILURE, b'' |
|
1504 |
+ ) |
|
1505 |
+ |
|
1506 |
+ monkeypatch.setattr(ssh_agent.SSHAgentClient, 'list_keys', func) |
|
1507 |
+ _result = runner.invoke( |
|
1508 |
+ cli.derivepassphrase_vault, |
|
1509 |
+ ['--key', '--config'], |
|
1510 |
+ catch_exceptions=False, |
|
1511 |
+ ) |
|
1512 |
+ result = tests.ReadableResult.parse(_result) |
|
1513 |
+ assert result.error_exit( |
|
1514 |
+ error='refused to' |
|
1515 |
+ ), 'expected error exit and known error message' |
|
1516 |
+ |
|
1430 | 1517 |
def test_226_no_arguments(self, monkeypatch: pytest.MonkeyPatch) -> None: |
1431 | 1518 |
runner = click.testing.CliRunner(mix_stderr=False) |
1432 | 1519 |
with tests.isolated_config( |
... | ... |
@@ -1438,7 +1525,7 @@ contents go here |
1438 | 1525 |
) |
1439 | 1526 |
result = tests.ReadableResult.parse(_result) |
1440 | 1527 |
assert result.error_exit( |
1441 |
- error='SERVICE is required' |
|
1528 |
+ error='Deriving a passphrase requires a SERVICE' |
|
1442 | 1529 |
), 'expected error exit and known error message' |
1443 | 1530 |
|
1444 | 1531 |
def test_226a_no_passphrase_or_key( |
... | ... |
@@ -1456,7 +1543,7 @@ contents go here |
1456 | 1543 |
) |
1457 | 1544 |
result = tests.ReadableResult.parse(_result) |
1458 | 1545 |
assert result.error_exit( |
1459 |
- error='No passphrase or key given' |
|
1546 |
+ error='No passphrase or key was given' |
|
1460 | 1547 |
), 'expected error exit and known error message' |
1461 | 1548 |
|
1462 | 1549 |
def test_230_config_directory_nonexistant( |
... | ... |
@@ -1529,7 +1616,7 @@ contents go here |
1529 | 1616 |
) |
1530 | 1617 |
result = tests.ReadableResult.parse(_result) |
1531 | 1618 |
assert result.error_exit( |
1532 |
- error='Cannot store config' |
|
1619 |
+ error='Cannot store vault settings:' |
|
1533 | 1620 |
), 'expected error exit and known error message' |
1534 | 1621 |
|
1535 | 1622 |
def test_230b_store_config_custom_error( |
... | ... |
@@ -1568,7 +1655,7 @@ contents go here |
1568 | 1655 |
'global': {'phrase': 'Du\u0308sseldorf'}, |
1569 | 1656 |
'services': {}, |
1570 | 1657 |
}), |
1571 |
- 'the $.global passphrase is not NFC-normalized', |
|
1658 |
+ 'The $.global passphrase is not NFC-normalized', |
|
1572 | 1659 |
id='global-NFC', |
1573 | 1660 |
), |
1574 | 1661 |
pytest.param( |
... | ... |
@@ -1581,7 +1668,7 @@ contents go here |
1581 | 1668 |
} |
1582 | 1669 |
}), |
1583 | 1670 |
( |
1584 |
- 'the $.services["weird entry name"] passphrase ' |
|
1671 |
+ 'The $.services["weird entry name"] passphrase ' |
|
1585 | 1672 |
'is not NFC-normalized' |
1586 | 1673 |
), |
1587 | 1674 |
id='service-weird-name-NFC', |
... | ... |
@@ -1591,7 +1678,7 @@ contents go here |
1591 | 1678 |
['--config', '-p', '--', DUMMY_SERVICE], |
1592 | 1679 |
'Du\u0308sseldorf', |
1593 | 1680 |
( |
1594 |
- f'the $.services.{DUMMY_SERVICE} passphrase ' |
|
1681 |
+ f'The $.services.{DUMMY_SERVICE} passphrase ' |
|
1595 | 1682 |
f'is not NFC-normalized' |
1596 | 1683 |
), |
1597 | 1684 |
id='config-NFC', |
... | ... |
@@ -1600,7 +1687,7 @@ contents go here |
1600 | 1687 |
'', |
1601 | 1688 |
['-p', '--', DUMMY_SERVICE], |
1602 | 1689 |
'Du\u0308sseldorf', |
1603 |
- 'the interactive input passphrase is not NFC-normalized', |
|
1690 |
+ 'The interactive input passphrase is not NFC-normalized', |
|
1604 | 1691 |
id='direct-input-NFC', |
1605 | 1692 |
), |
1606 | 1693 |
pytest.param( |
... | ... |
@@ -1615,7 +1702,7 @@ contents go here |
1615 | 1702 |
}, |
1616 | 1703 |
'services': {}, |
1617 | 1704 |
}), |
1618 |
- 'the $.global passphrase is not NFD-normalized', |
|
1705 |
+ 'The $.global passphrase is not NFD-normalized', |
|
1619 | 1706 |
id='global-NFD', |
1620 | 1707 |
), |
1621 | 1708 |
pytest.param( |
... | ... |
@@ -1631,7 +1718,7 @@ contents go here |
1631 | 1718 |
}, |
1632 | 1719 |
}), |
1633 | 1720 |
( |
1634 |
- 'the $.services["weird entry name"] passphrase ' |
|
1721 |
+ 'The $.services["weird entry name"] passphrase ' |
|
1635 | 1722 |
'is not NFD-normalized' |
1636 | 1723 |
), |
1637 | 1724 |
id='service-weird-name-NFD', |
... | ... |
@@ -1650,7 +1737,7 @@ contents go here |
1650 | 1737 |
}, |
1651 | 1738 |
}), |
1652 | 1739 |
( |
1653 |
- 'the $.services["weird entry name 2"] passphrase ' |
|
1740 |
+ 'The $.services["weird entry name 2"] passphrase ' |
|
1654 | 1741 |
'is not NFKD-normalized' |
1655 | 1742 |
), |
1656 | 1743 |
id='service-weird-name-2-NFKD', |
... | ... |
@@ -1753,7 +1840,7 @@ contents go here |
1753 | 1840 |
) |
1754 | 1841 |
result = tests.ReadableResult.parse(_result) |
1755 | 1842 |
assert result.error_exit( |
1756 |
- error='The configuration file is invalid.' |
|
1843 |
+ error='The user configuration file is invalid.' |
|
1757 | 1844 |
), 'expected error exit and known error message' |
1758 | 1845 |
assert result.error_exit( |
1759 | 1846 |
error=error_message |
... | ... |
@@ -1797,7 +1884,7 @@ contents go here |
1797 | 1884 |
) |
1798 | 1885 |
result = tests.ReadableResult.parse(_result) |
1799 | 1886 |
assert result.error_exit( |
1800 |
- error='The configuration file is invalid.' |
|
1887 |
+ error='The user configuration file is invalid.' |
|
1801 | 1888 |
), 'expected error exit and known error message' |
1802 | 1889 |
assert result.error_exit( |
1803 | 1890 |
error=( |
... | ... |
@@ -2464,12 +2551,14 @@ Boo. |
2464 | 2551 |
skip_if_no_af_unix_support: None, |
2465 | 2552 |
ssh_agent_client_with_test_keys_loaded: ssh_agent.SSHAgentClient, |
2466 | 2553 |
) -> None: |
2467 |
- class CustomError(RuntimeError): |
|
2468 |
- pass |
|
2554 |
+ class ErrCallback(BaseException): |
|
2555 |
+ def __init__(self, *args: Any, **kwargs: Any) -> None: |
|
2556 |
+ super().__init__(*args[:1]) |
|
2557 |
+ self.args = args |
|
2558 |
+ self.kwargs = kwargs |
|
2469 | 2559 |
|
2470 | 2560 |
def err(*args: Any, **_kwargs: Any) -> NoReturn: |
2471 |
- args = args or ('custom error message',) |
|
2472 |
- raise CustomError(*args) |
|
2561 |
+ raise ErrCallback(*args, **_kwargs) |
|
2473 | 2562 |
|
2474 | 2563 |
def fail(*_args: Any, **_kwargs: Any) -> Any: |
2475 | 2564 |
raise ssh_agent.SSHAgentFailedError( |
... | ... |
@@ -2477,6 +2566,9 @@ Boo. |
2477 | 2566 |
b'', |
2478 | 2567 |
) |
2479 | 2568 |
|
2569 |
+ def fail_runtime(*_args: Any, **_kwargs: Any) -> Any: |
|
2570 |
+ raise ssh_agent.TrailingDataError() |
|
2571 |
+ |
|
2480 | 2572 |
del skip_if_no_af_unix_support |
2481 | 2573 |
monkeypatch.setattr(ssh_agent.SSHAgentClient, 'sign', fail) |
2482 | 2574 |
loaded_keys = list(ssh_agent_client_with_test_keys_loaded.list_keys()) |
... | ... |
@@ -2487,42 +2579,52 @@ Boo. |
2487 | 2579 |
'list_keys', |
2488 | 2580 |
lambda *_a, **_kw: [], |
2489 | 2581 |
) |
2490 |
- with pytest.raises(CustomError, match='not loaded into the agent'): |
|
2582 |
+ with pytest.raises(ErrCallback, match='not loaded into the agent'): |
|
2491 | 2583 |
cli._key_to_phrase(loaded_key, error_callback=err) |
2492 | 2584 |
with monkeypatch.context() as mp: |
2493 | 2585 |
mp.setattr(ssh_agent.SSHAgentClient, 'list_keys', fail) |
2494 | 2586 |
with pytest.raises( |
2495 |
- CustomError, match='SSH agent failed to complete' |
|
2587 |
+ ErrCallback, match='SSH agent failed to or refused to' |
|
2496 | 2588 |
): |
2497 | 2589 |
cli._key_to_phrase(loaded_key, error_callback=err) |
2498 | 2590 |
with monkeypatch.context() as mp: |
2499 |
- mp.setattr(ssh_agent.SSHAgentClient, 'list_keys', err) |
|
2591 |
+ mp.setattr(ssh_agent.SSHAgentClient, 'list_keys', fail_runtime) |
|
2500 | 2592 |
with pytest.raises( |
2501 |
- CustomError, match='SSH agent failed to complete' |
|
2593 |
+ ErrCallback, match='SSH agent failed to or refused to' |
|
2502 | 2594 |
) as excinfo: |
2503 | 2595 |
cli._key_to_phrase(loaded_key, error_callback=err) |
2504 |
- assert excinfo.value.args |
|
2596 |
+ assert excinfo.value.kwargs |
|
2597 |
+ assert isinstance( |
|
2598 |
+ excinfo.value.kwargs['exc_info'], |
|
2599 |
+ ssh_agent.SSHAgentFailedError, |
|
2600 |
+ ) |
|
2601 |
+ assert excinfo.value.kwargs['exc_info'].__context__ is not None |
|
2505 | 2602 |
assert isinstance( |
2506 |
- excinfo.value.args[0], ssh_agent.SSHAgentFailedError |
|
2603 |
+ excinfo.value.kwargs['exc_info'].__context__, |
|
2604 |
+ ssh_agent.TrailingDataError, |
|
2507 | 2605 |
) |
2508 |
- assert excinfo.value.args[0].__context__ is not None |
|
2509 |
- assert isinstance(excinfo.value.args[0].__context__, CustomError) |
|
2510 | 2606 |
with monkeypatch.context() as mp: |
2511 | 2607 |
mp.delenv('SSH_AUTH_SOCK', raising=True) |
2512 | 2608 |
with pytest.raises( |
2513 |
- CustomError, match='Cannot find running SSH agent' |
|
2609 |
+ ErrCallback, match='Cannot find any running SSH agent' |
|
2514 | 2610 |
): |
2515 | 2611 |
cli._key_to_phrase(loaded_key, error_callback=err) |
2516 | 2612 |
with monkeypatch.context() as mp: |
2517 | 2613 |
mp.setenv('SSH_AUTH_SOCK', os.environ['SSH_AUTH_SOCK'] + '~') |
2518 | 2614 |
with pytest.raises( |
2519 |
- CustomError, match='Cannot connect to SSH agent' |
|
2615 |
+ ErrCallback, match='Cannot connect to the SSH agent' |
|
2520 | 2616 |
): |
2521 | 2617 |
cli._key_to_phrase(loaded_key, error_callback=err) |
2522 | 2618 |
with monkeypatch.context() as mp: |
2523 | 2619 |
mp.delattr(socket, 'AF_UNIX', raising=True) |
2524 | 2620 |
with pytest.raises( |
2525 |
- CustomError, match='does not support UNIX domain sockets' |
|
2621 |
+ ErrCallback, match='does not support UNIX domain sockets' |
|
2622 |
+ ): |
|
2623 |
+ cli._key_to_phrase(loaded_key, error_callback=err) |
|
2624 |
+ with monkeypatch.context() as mp: |
|
2625 |
+ mp.setattr(ssh_agent.SSHAgentClient, 'sign', fail_runtime) |
|
2626 |
+ with pytest.raises( |
|
2627 |
+ ErrCallback, match='violates the communications protocol' |
|
2526 | 2628 |
): |
2527 | 2629 |
cli._key_to_phrase(loaded_key, error_callback=err) |
2528 | 2630 |
|
... | ... |
@@ -2575,7 +2677,7 @@ class TestCLITransition: |
2575 | 2677 |
) |
2576 | 2678 |
result = tests.ReadableResult.parse(_result) |
2577 | 2679 |
assert result.clean_exit( |
2578 |
- empty_stderr=True, output='Read the vault-native configuration' |
|
2680 |
+ empty_stderr=True, output='Export a vault-native configuration' |
|
2579 | 2681 |
), 'expected clean exit, and known help text' |
2580 | 2682 |
|
2581 | 2683 |
def test_103_help_output_vault( |
... | ... |
@@ -2596,7 +2698,7 @@ class TestCLITransition: |
2596 | 2698 |
empty_stderr=True, output='Passphrase generation:\n' |
2597 | 2699 |
), 'expected clean exit, and option groups in help text' |
2598 | 2700 |
assert result.clean_exit( |
2599 |
- empty_stderr=True, output='Use NUMBER=0, e.g. "--symbol 0"' |
|
2701 |
+ empty_stderr=True, output='Use $VISUAL or $EDITOR to configure' |
|
2600 | 2702 |
), 'expected clean exit, and option group epilog in help text' |
2601 | 2703 |
|
2602 | 2704 |
@pytest.mark.parametrize( |
... | ... |
@@ -2749,9 +2851,9 @@ class TestCLITransition: |
2749 | 2851 |
result = tests.ReadableResult.parse(_result) |
2750 | 2852 |
assert result.clean_exit(empty_stderr=False), 'expected clean exit' |
2751 | 2853 |
assert tests.deprecation_warning_emitted( |
2752 |
- 'A subcommand will be required in v1.0', caplog.record_tuples |
|
2854 |
+ 'A subcommand will be required here in v1.0', caplog.record_tuples |
|
2753 | 2855 |
) |
2754 |
- assert tests.warning_emitted( |
|
2856 |
+ assert tests.deprecation_warning_emitted( |
|
2755 | 2857 |
'Defaulting to subcommand "vault"', caplog.record_tuples |
2756 | 2858 |
) |
2757 | 2859 |
assert json.loads(result.output) == tests.VAULT_V03_CONFIG_DATA |
... | ... |
@@ -2773,9 +2875,9 @@ class TestCLITransition: |
2773 | 2875 |
) |
2774 | 2876 |
result = tests.ReadableResult.parse(_result) |
2775 | 2877 |
assert tests.deprecation_warning_emitted( |
2776 |
- 'A subcommand will be required in v1.0', caplog.record_tuples |
|
2878 |
+ 'A subcommand will be required here in v1.0', caplog.record_tuples |
|
2777 | 2879 |
) |
2778 |
- assert tests.warning_emitted( |
|
2880 |
+ assert tests.deprecation_warning_emitted( |
|
2779 | 2881 |
'Defaulting to subcommand "vault"', caplog.record_tuples |
2780 | 2882 |
) |
2781 | 2883 |
assert result.error_exit( |
... | ... |
@@ -2808,9 +2910,9 @@ class TestCLITransition: |
2808 | 2910 |
result = tests.ReadableResult.parse(_result) |
2809 | 2911 |
assert result.clean_exit(empty_stderr=False), 'expected clean exit' |
2810 | 2912 |
assert tests.deprecation_warning_emitted( |
2811 |
- 'A subcommand will be required in v1.0', caplog.record_tuples |
|
2913 |
+ 'A subcommand will be required here in v1.0', caplog.record_tuples |
|
2812 | 2914 |
) |
2813 |
- assert tests.warning_emitted( |
|
2915 |
+ assert tests.deprecation_warning_emitted( |
|
2814 | 2916 |
'Defaulting to subcommand "vault"', caplog.record_tuples |
2815 | 2917 |
) |
2816 | 2918 |
for c in charset: |
... | ... |
@@ -2836,13 +2938,13 @@ class TestCLITransition: |
2836 | 2938 |
) |
2837 | 2939 |
result = tests.ReadableResult.parse(_result) |
2838 | 2940 |
assert tests.deprecation_warning_emitted( |
2839 |
- 'A subcommand will be required in v1.0', caplog.record_tuples |
|
2941 |
+ 'A subcommand will be required here in v1.0', caplog.record_tuples |
|
2840 | 2942 |
) |
2841 |
- assert tests.warning_emitted( |
|
2943 |
+ assert tests.deprecation_warning_emitted( |
|
2842 | 2944 |
'Defaulting to subcommand "vault"', caplog.record_tuples |
2843 | 2945 |
) |
2844 | 2946 |
assert result.error_exit( |
2845 |
- error='SERVICE is required' |
|
2947 |
+ error='Deriving a passphrase requires a SERVICE.' |
|
2846 | 2948 |
), 'expected error exit and known error type' |
2847 | 2949 |
|
2848 | 2950 |
def test_300_export_using_old_config_file( |
2849 | 2951 |