From 853ba16dbc4a57b25dbdd5bf9e56b257859b4175 Mon Sep 17 00:00:00 2001 From: Maciej Urbanski Date: Thu, 23 Nov 2023 21:01:08 +0100 Subject: [PATCH] add --help-all --- b2/arg_parser.py | 48 ++++++++++++++++++++++++++--- b2/console_tool.py | 26 ++++++++++------ test/unit/console_tool/test_help.py | 43 ++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 test/unit/console_tool/test_help.py diff --git a/b2/arg_parser.py b/b2/arg_parser.py index 9420ac51e..480a251fb 100644 --- a/b2/arg_parser.py +++ b/b2/arg_parser.py @@ -15,6 +15,7 @@ import re import sys import textwrap +import unittest.mock import arrow from b2sdk.v2 import RetentionPeriod @@ -30,6 +31,10 @@ class B2RawTextHelpFormatter(argparse.RawTextHelpFormatter): It removes default "usage: " text and prints usage for all (non-hidden) subcommands. """ + def __init__(self, *args, show_all: bool = False, **kwargs): + super().__init__(*args, **kwargs) + self.show_all = show_all + def add_usage(self, usage, actions, groups, prefix=None): if prefix is None: prefix = '' @@ -39,7 +44,11 @@ def add_argument(self, action): if isinstance(action, argparse._SubParsersAction) and action.help is not argparse.SUPPRESS: usages = [] for choice in self._unique_choice_values(action): - if not getattr(choice, 'hidden', False): + deprecated = getattr(choice, 'deprecated', False) + if deprecated: + if self.show_all: + usages.append(f'(DEPRECATED) {choice.format_usage()}') + else: usages.append(choice.format_usage()) self.add_text(''.join(usages)) else: @@ -54,6 +63,14 @@ def _unique_choice_values(cls, action): yield value +class _HelpAllAction(argparse._HelpAction): + """Like argparse._HelpAction but prints help for all subcommands (even deprecated ones).""" + + def __call__(self, parser, namespace, values, option_string=None): + parser.print_help(show_all=True) + parser.exit() + + class B2ArgumentParser(argparse.ArgumentParser): """ CLI custom parser. @@ -62,18 +79,32 @@ class B2ArgumentParser(argparse.ArgumentParser): and use help message in case of error. """ - def __init__(self, *args, for_docs: bool = False, hidden: bool = False, **kwargs): + def __init__( + self, + *args, + add_help_all: bool = True, + for_docs: bool = False, + deprecated: bool = False, + **kwargs + ): """ :param for_docs: is this parser used for generating docs - :param hidden: should this parser be hidden from `--help` + :param deprecated: is this option deprecated """ self._raw_description = None self._description = None self._for_docs = for_docs - self.hidden = hidden + self.deprecated = deprecated kwargs.setdefault('formatter_class', B2RawTextHelpFormatter) super().__init__(*args, **kwargs) + if add_help_all: + self.register('action', 'help_all', _HelpAllAction) + self.add_argument( + '--help-all', + help='show help for all options, including deprecated ones', + action='help_all', + ) @property def description(self): @@ -113,6 +144,15 @@ def _get_encoding(cls): # locales are improperly configured return 'ascii' + def print_help(self, *args, show_all: bool = False, **kwargs): + """ + Print help message. + """ + with unittest.mock.patch.object( + self, 'formatter_class', functools.partial(B2RawTextHelpFormatter, show_all=show_all) + ): + super().print_help(*args, **kwargs) + def parse_comma_separated_list(s): """ diff --git a/b2/console_tool.py b/b2/console_tool.py index 2865a56c7..37564fe1a 100644 --- a/b2/console_tool.py +++ b/b2/console_tool.py @@ -687,7 +687,7 @@ class Command(Described): # Set to True for commands that receive sensitive information in arguments FORBID_LOGGING_ARGUMENTS = False - hide_from_help = False + deprecated = False # The registry for the subcommands, should be reinitialized in subclass subcommands_registry = None @@ -719,7 +719,11 @@ def register_subcommand(cls, command_class): @classmethod def create_parser( - cls, subparsers: "argparse._SubParsersAction | None" = None, parents=None, for_docs=False + cls, + subparsers: argparse._SubParsersAction | None = None, + parents=None, + for_docs=False, + name: str | None = None ) -> argparse.ArgumentParser: """ Creates a parser for the command. @@ -734,22 +738,26 @@ def create_parser( description = cls._get_description() - name, alias = cls.name_and_alias() + if name: + alias = None + else: + name, alias = cls.name_and_alias() parser_kwargs = dict( prog=name, description=description, parents=parents, for_docs=for_docs, - hidden=cls.hide_from_help, + deprecated=cls.deprecated, ) if subparsers is None: - parser = B2ArgumentParser(**parser_kwargs,) + parser = B2ArgumentParser(**parser_kwargs) else: parser = subparsers.add_parser( parser_kwargs.pop('prog'), **parser_kwargs, aliases=[alias] if alias is not None and not for_docs else (), + add_help_all=False, ) # Register class that will handle this particular command, for both name and alias. parser.set_defaults(command_class=cls) @@ -758,7 +766,7 @@ def create_parser( if cls.subcommands_registry: if not parents: - common_parser = B2ArgumentParser(add_help=False) + common_parser = B2ArgumentParser(add_help=False, add_help_all=False) common_parser.add_argument( '--debugLogs', action='store_true', help=argparse.SUPPRESS ) @@ -863,8 +871,8 @@ def __str__(self): class CmdReplacedByMixin: - hide_from_help = True - replaced_by_cmd: "type[Command]" + deprecated = True + replaced_by_cmd: type[Command] def run(self, args): self._print_stderr( @@ -3929,7 +3937,7 @@ def __init__(self, b2_api: B2Api | None, stdout, stderr): def run_command(self, argv): signal.signal(signal.SIGINT, keyboard_interrupt_handler) - parser = B2.create_parser() + parser = B2.create_parser(name=argv[0]) argcomplete.autocomplete(parser, default_completer=None) args = parser.parse_args(argv[1:]) self._setup_logging(args, argv) diff --git a/test/unit/console_tool/test_help.py b/test/unit/console_tool/test_help.py new file mode 100644 index 000000000..4dfca6c61 --- /dev/null +++ b/test/unit/console_tool/test_help.py @@ -0,0 +1,43 @@ +###################################################################### +# +# File: test/unit/console_tool/test_help.py +# +# Copyright 2023 Backblaze Inc. All Rights Reserved. +# +# License https://www.backblaze.com/using_b2_code.html +# +###################################################################### +import pytest + + +@pytest.mark.parametrize( + "flag, included, excluded", + [ + # --help shouldn't show deprecated commands + ( + "--help", + [" b2 download-file ", "-h", "--help-all"], + [" download-file-by-name ", "(DEPRECATED)"], + ), + # --help-all should show deprecated commands, but marked as deprecated + ( + "--help-all", + ["(DEPRECATED) b2 download-file-by-name ", "-h", "--help-all"], + [], + ), + ], +) +def test_help(b2_cli, flag, included, excluded, capsys): + b2_cli.run([flag], expected_stdout=None) + + out = capsys.readouterr().out + + found = set() + for i in included: + if i in out: + found.add(i) + for e in excluded: + if e in out: + found.add(e) + assert found.issuperset(included), f"expected {included!r} in {out!r}" + assert found.isdisjoint(excluded), f"expected {excluded!r} not in {out!r}"