Reimplement `--help` and `--version` options in a translatable way
Marco Ricci

Marco Ricci commited on 2024-12-31 22:30:36
Zeige 2 geänderte Dateien mit 137 Einfügungen und 8 Löschungen.


Reimplement the machinery for displaying help texts and version
information, supporting translatable strings.  Besides the one-line help
texts for the `--help` and `--version` options, the version information
text is a translatable string itself, in case the name and version
number should be rearranged, or prefixed or suffixed, or whatever.
... ...
@@ -401,6 +401,11 @@ class Label(enum.Enum):
401 401
         """,
402 402
         context='help text (option one-line description)',
403 403
     )
404
+    HELP_OPTION_HELP_TEXT = _prepare_translatable(
405
+        'show this help text, then exit',
406
+        comments='',
407
+        context='help text (option one-line description)',
408
+    )
404 409
     QUIET_OPTION_HELP_TEXT = _prepare_translatable(
405 410
         'suppress even warnings, emit only errors',
406 411
         comments='',
... ...
@@ -411,6 +416,11 @@ class Label(enum.Enum):
411 416
         comments='',
412 417
         context='help text (option one-line description)',
413 418
     )
419
+    VERSION_OPTION_HELP_TEXT = _prepare_translatable(
420
+        'show applicable version information, then exit',
421
+        comments='',
422
+        context='help text (option one-line description)',
423
+    )
414 424
 
415 425
     DERIVEPASSPHRASE_VAULT_PHRASE_HELP_TEXT = _prepare_translatable(
416 426
         msg='prompt for a master passphrase',
... ...
@@ -675,6 +685,14 @@ class Label(enum.Enum):
675 685
         comments='',
676 686
         context='help text, option group name',
677 687
     )
688
+    VERSION_INFO_TEXT = _prepare_translatable(
689
+        msg=r"""
690
+        {PROG_NAME!s} {__version__}
691
+        """,  # noqa: RUF027
692
+        comments='',
693
+        context='help text, version info text',
694
+        flags='python-brace-format',
695
+    )
678 696
     CONFIRM_THIS_CHOICE_PROMPT_TEXT = _prepare_translatable(
679 697
         comments=r"""
680 698
         TRANSLATORS: There is no support for "yes" or "no" in other
... ...
@@ -423,6 +423,10 @@ class OptionGroupOption(click.Option):
423 423
             self.help = help
424 424
 
425 425
 
426
+class StandardOption(OptionGroupOption):
427
+    pass
428
+
429
+
426 430
 class CommandWithHelpGroups(click.Command):
427 431
     """A [`click.Command`][] with support for some help text customizations.
428 432
 
... ...
@@ -498,6 +502,83 @@ class CommandWithHelpGroups(click.Command):
498 502
             rv.extend(str(x) for x in param.get_usage_pieces(ctx))
499 503
         return rv
500 504
 
505
+    def get_help_option(
506
+        self,
507
+        ctx: click.Context,
508
+    ) -> click.Option | None:
509
+        """Return a standard help option object.
510
+
511
+        Based on code from click 8.1.  Subject to the following license
512
+        (3-clause BSD license):
513
+
514
+            Copyright 2024 Pallets
515
+
516
+            Redistribution and use in source and binary forms, with or
517
+            without modification, are permitted provided that the
518
+            following conditions are met:
519
+
520
+             1. Redistributions of source code must retain the above
521
+                copyright notice, this list of conditions and the
522
+                following disclaimer.
523
+
524
+             2. Redistributions in binary form must reproduce the above
525
+                copyright notice, this list of conditions and the
526
+                following disclaimer in the documentation and/or other
527
+                materials provided with the distribution.
528
+
529
+             3. Neither the name of the copyright holder nor the names
530
+                of its contributors may be used to endorse or promote
531
+                products derived from this software without specific
532
+                prior written permission.
533
+
534
+            THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
535
+            CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES,
536
+            INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
537
+            MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
538
+            DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
539
+            CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
540
+            SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
541
+            NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
542
+            LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
543
+            HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
544
+            CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
545
+            OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
546
+            SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
547
+
548
+        Modifications are marked with respective comments.  They too are
549
+        released under the same license above.  The original code did
550
+        not contain any "noqa" or "pragma" comments.
551
+
552
+        Args:
553
+            ctx:
554
+                The click context.
555
+
556
+        """
557
+        help_options = self.get_help_option_names(ctx)
558
+
559
+        if not help_options or not self.add_help_option:  # pragma: no cover
560
+            return None
561
+
562
+        def show_help(
563
+            ctx: click.Context,
564
+            param: click.Parameter,  # noqa: ARG001
565
+            value: str,
566
+        ) -> None:
567
+            if value and not ctx.resilient_parsing:
568
+                click.echo(ctx.get_help(), color=ctx.color)
569
+                ctx.exit()
570
+
571
+        # Modified from click 8.1: We use StandardOption and a non-str
572
+        # object as the help string.
573
+        return StandardOption(
574
+            help_options,
575
+            is_flag=True,
576
+            is_eager=True,
577
+            expose_value=False,
578
+            callback=show_help,
579
+            help=_msg.TranslatedString(_msg.Label.HELP_OPTION_HELP_TEXT),
580
+        )
581
+
501 582
     def get_short_help_str(
502 583
         self,
503 584
         limit: int = 45,
... ...
@@ -904,6 +985,37 @@ class CommandWithHelpGroups(click.Command):
904 985
                 formatter.write_text(epilog)
905 986
 
906 987
 
988
+def version_option_callback(
989
+    ctx: click.Context,
990
+    param: click.Parameter,
991
+    value: bool,  # noqa: FBT001
992
+) -> None:
993
+    del param
994
+    if value and not ctx.resilient_parsing:
995
+        click.echo(
996
+            str(
997
+                _msg.TranslatedString(
998
+                    _msg.Label.VERSION_INFO_TEXT,
999
+                    PROG_NAME=PROG_NAME,
1000
+                    __version__=__version__,
1001
+                )
1002
+            ),
1003
+        )
1004
+        ctx.exit()
1005
+
1006
+
1007
+def version_option(f: Callable[P, R]) -> Callable[P, R]:
1008
+    return click.option(
1009
+        '--version',
1010
+        is_flag=True,
1011
+        is_eager=True,
1012
+        expose_value=False,
1013
+        callback=version_option_callback,
1014
+        cls=StandardOption,
1015
+        help=_msg.TranslatedString(_msg.Label.VERSION_OPTION_HELP_TEXT),
1016
+    )(f)
1017
+
1018
+
907 1019
 class LoggingOption(OptionGroupOption):
908 1020
     """Logging options for the CLI."""
909 1021
 
... ...
@@ -1110,7 +1222,7 @@ class _TopLevelCLIEntryPoint(_DefaultToVaultGroup):
1110 1222
         _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_03),
1111 1223
     ),
1112 1224
 )
1113
-@click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
1225
+@version_option
1114 1226
 @standard_logging_options
1115 1227
 @click.pass_context
1116 1228
 def derivepassphrase(ctx: click.Context, /) -> None:
... ...
@@ -1159,7 +1271,7 @@ def derivepassphrase(ctx: click.Context, /) -> None:
1159 1271
         _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_03),
1160 1272
     ),
1161 1273
 )
1162
-@click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
1274
+@version_option
1163 1275
 @standard_logging_options
1164 1276
 @click.pass_context
1165 1277
 def derivepassphrase_export(ctx: click.Context, /) -> None:
... ...
@@ -1231,10 +1343,6 @@ def _load_data(
1231 1343
         assert_never(fmt)
1232 1344
 
1233 1345
 
1234
-class StandardOption(OptionGroupOption):
1235
-    pass
1236
-
1237
-
1238 1346
 @derivepassphrase_export.command(
1239 1347
     'vault',
1240 1348
     context_settings={'help_option_names': ['-h', '--help']},
... ...
@@ -1255,7 +1363,6 @@ class StandardOption(OptionGroupOption):
1255 1363
         ),
1256 1364
     ),
1257 1365
 )
1258
-@standard_logging_options
1259 1366
 @click.option(
1260 1367
     '-f',
1261 1368
     '--format',
... ...
@@ -1288,6 +1395,8 @@ class StandardOption(OptionGroupOption):
1288 1395
     ),
1289 1396
     cls=StandardOption,
1290 1397
 )
1398
+@version_option
1399
+@standard_logging_options
1291 1400
 @click.argument(
1292 1401
     'path',
1293 1402
     metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_METAVAR_PATH),
... ...
@@ -2362,7 +2471,7 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
2362 2471
     ),
2363 2472
     cls=CompatibilityOption,
2364 2473
 )
2365
-@click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
2474
+@version_option
2366 2475
 @standard_logging_options
2367 2476
 @click.argument(
2368 2477
     'service',
... ...
@@ -2508,6 +2617,8 @@ def derivepassphrase_vault(  # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915
2508 2617
                 group = LoggingOption
2509 2618
             elif isinstance(param, CompatibilityOption):
2510 2619
                 group = CompatibilityOption
2620
+            elif isinstance(param, StandardOption):
2621
+                group = StandardOption
2511 2622
             elif isinstance(param, OptionGroupOption):
2512 2623
                 raise AssertionError(  # noqa: DOC501,TRY003,TRY004
2513 2624
                     f'Unknown option group for {param!r}'  # noqa: EM102
2514 2625