Skip to content

Commit

Permalink
deprecate commands replaced by B2 URI using equivalents
Browse files Browse the repository at this point in the history
  • Loading branch information
mjurbanski-reef committed Nov 21, 2023
1 parent 34f204c commit fde190d
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 98 deletions.
20 changes: 14 additions & 6 deletions b2/arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# License https://www.backblaze.com/using_b2_code.html
#
######################################################################
from __future__ import annotations

import argparse
import functools
Expand All @@ -22,11 +23,11 @@
_arrow_version = tuple(int(p) for p in arrow.__version__.split("."))


class RawTextHelpFormatter(argparse.RawTextHelpFormatter):
class B2RawTextHelpFormatter(argparse.RawTextHelpFormatter):
"""
CLI custom formatter.
It removes default "usage: " text and prints usage for all subcommands.
It removes default "usage: " text and prints usage for all (non-hidden) subcommands.
"""

def add_usage(self, usage, actions, groups, prefix=None):
Expand All @@ -38,7 +39,8 @@ 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):
usages.append(choice.format_usage())
if not getattr(choice, 'hidden', False):
usages.append(choice.format_usage())
self.add_text(''.join(usages))
else:
super().add_argument(action)
Expand All @@ -52,19 +54,25 @@ def _unique_choice_values(cls, action):
yield value


class ArgumentParser(argparse.ArgumentParser):
class B2ArgumentParser(argparse.ArgumentParser):
"""
CLI custom parser.
It fixes indentation of the description, set the custom formatter as a default
and use help message in case of error.
"""

def __init__(self, *args, for_docs=False, **kwargs):
def __init__(self, *args, for_docs: bool = False, hidden: bool = False, **kwargs):
"""
:param for_docs: is this parser used for generating docs
:param hidden: should this parser be hidden from `--help`
"""
self._raw_description = None
self._description = None
self._for_docs = for_docs
kwargs.setdefault('formatter_class', RawTextHelpFormatter)
self.hidden = hidden
kwargs.setdefault('formatter_class', B2RawTextHelpFormatter)
super().__init__(*args, **kwargs)

@property
Expand Down
90 changes: 66 additions & 24 deletions b2/console_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
from b2._cli.shell import detect_shell
from b2._utils.uri import B2URI, B2FileIdURI, B2URIAdapter, B2URIBase
from b2.arg_parser import (
ArgumentParser,
B2ArgumentParser,
parse_comma_separated_list,
parse_default_retention_period,
parse_millis_from_float_timestamp,
Expand Down Expand Up @@ -623,6 +623,8 @@ class Command(Described):
# Set to True for commands that receive sensitive information in arguments
FORBID_LOGGING_ARGUMENTS = False

hide_from_help = False

# The registry for the subcommands, should be reinitialized in subclass
subcommands_registry = None

Expand Down Expand Up @@ -652,28 +654,38 @@ def register_subcommand(cls, command_class):
return decorator

@classmethod
def get_parser(cls, subparsers=None, parents=None, for_docs=False):
def create_parser(
cls, subparsers: "argparse._SubParsersAction | None" = None, parents=None, for_docs=False
) -> argparse.ArgumentParser:
"""
Creates a parser for the command.
:param subparsers: subparsers object to which add new parser
:param parents: created ArgumentParser `parents`, see `argparse.ArgumentParser`
:param for_docs: if parser is to be used for documentation generation
:return: created parser
"""
if parents is None:
parents = []

description = cls._get_description()

name, alias = cls.name_and_alias()
parser_kwargs = dict(
prog=name,
description=description,
parents=parents,
for_docs=for_docs,
hidden=cls.hide_from_help,
)

if subparsers is None:
name, _ = cls.name_and_alias()
parser = ArgumentParser(
prog=name,
description=description,
parents=parents,
for_docs=for_docs,
)
parser = B2ArgumentParser(**parser_kwargs,)
else:
name, alias = cls.name_and_alias()
parser = subparsers.add_parser(
name,
description=description,
parents=parents,
parser_kwargs.pop('prog'),
**parser_kwargs,
aliases=[alias] if alias is not None and not for_docs else (),
for_docs=for_docs,
)
# Register class that will handle this particular command, for both name and alias.
parser.set_defaults(command_class=cls)
Expand All @@ -682,7 +694,7 @@ def get_parser(cls, subparsers=None, parents=None, for_docs=False):

if cls.subcommands_registry:
if not parents:
common_parser = ArgumentParser(add_help=False)
common_parser = B2ArgumentParser(add_help=False)
common_parser.add_argument(
'--debugLogs', action='store_true', help=argparse.SUPPRESS
)
Expand All @@ -694,10 +706,15 @@ def get_parser(cls, subparsers=None, parents=None, for_docs=False):
)
parents = [common_parser]

subparsers = parser.add_subparsers(prog=parser.prog, title='usages', dest='command')
subparsers = parser.add_subparsers(
prog=parser.prog,
title='usages',
dest='command',
parser_class=B2ArgumentParser,
)
subparsers.required = True
for subcommand in cls.subcommands_registry.values():
subcommand.get_parser(subparsers=subparsers, parents=parents, for_docs=for_docs)
subcommand.create_parser(subparsers=subparsers, parents=parents, for_docs=for_docs)

return parser

Expand Down Expand Up @@ -781,6 +798,26 @@ def __str__(self):
return f'{self.__class__.__module__}.{self.__class__.__name__}'


class CmdReplacedByMixin:
hide_from_help = True
replaced_by_cmd: "type[Command]"

def run(self, args):
self._print_stderr(
f'WARNING: {self.__class__.name_and_alias()[0]} command is deprecated. '
f'Use {self.replaced_by_cmd.name_and_alias()[0]} instead.'
)
return super().run(args)

@classmethod
def _get_description(cls):
return (
f'{super()._get_description()}\n\n'
f'.. warning::\n'
f' This command is deprecated. Use ``{cls.replaced_by_cmd.name_and_alias()[0]}`` instead.\n'
)


class B2(Command):
"""
This program provides command-line access to the B2 service.
Expand Down Expand Up @@ -1509,8 +1546,9 @@ def get_b2_uri_from_arg(self, args: argparse.Namespace) -> B2URIBase:


@B2.register_subcommand
class DownloadFileById(B2URIFileIDArgMixin, DownloadFileBase):
class DownloadFileById(CmdReplacedByMixin, B2URIFileIDArgMixin, DownloadFileBase):
__doc__ = DownloadFileBase.__doc__
replaced_by_cmd = DownloadFile

@classmethod
def _setup_parser(cls, parser):
Expand All @@ -1519,8 +1557,9 @@ def _setup_parser(cls, parser):


@B2.register_subcommand
class DownloadFileByName(B2URIBucketNFilenameArgMixin, DownloadFileBase):
class DownloadFileByName(CmdReplacedByMixin, B2URIBucketNFilenameArgMixin, DownloadFileBase):
__doc__ = DownloadFileBase.__doc__
replaced_by_cmd = DownloadFile

@classmethod
def _setup_parser(cls, parser):
Expand Down Expand Up @@ -1671,8 +1710,9 @@ class FileInfo(B2URIFileArgMixin, FileInfoBase):


@B2.register_subcommand
class GetFileInfo(B2URIFileIDArgMixin, FileInfoBase):
class GetFileInfo(CmdReplacedByMixin, B2URIFileIDArgMixin, FileInfoBase):
__doc__ = FileInfoBase.__doc__
replaced_by_cmd = FileInfo


@B2.register_subcommand
Expand Down Expand Up @@ -2347,13 +2387,15 @@ class GetUrl(B2URIFileArgMixin, GetUrlBase):


@B2.register_subcommand
class MakeUrl(B2URIFileIDArgMixin, GetUrlBase):
class MakeUrl(CmdReplacedByMixin, B2URIFileIDArgMixin, GetUrlBase):
__doc__ = GetUrlBase.__doc__
replaced_by_cmd = GetUrl


@B2.register_subcommand
class MakeFriendlyUrl(B2URIBucketNFilenameArgMixin, GetUrlBase):
class MakeFriendlyUrl(CmdReplacedByMixin, B2URIBucketNFilenameArgMixin, GetUrlBase):
__doc__ = GetUrlBase.__doc__
replaced_by_cmd = GetUrl


@B2.register_subcommand
Expand Down Expand Up @@ -3821,7 +3863,7 @@ def __init__(self, b2_api: Optional[B2Api], stdout, stderr):

def run_command(self, argv):
signal.signal(signal.SIGINT, keyboard_interrupt_handler)
parser = B2.get_parser()
parser = B2.create_parser()
argcomplete.autocomplete(parser, default_completer=None)
args = parser.parse_args(argv[1:])
self._setup_logging(args, argv)
Expand Down Expand Up @@ -3959,7 +4001,7 @@ def _setup_logging(cls, args, argv):


# used by Sphinx
get_parser = functools.partial(B2.get_parser, for_docs=True)
get_parser = functools.partial(B2.create_parser, for_docs=True)


# TODO: import from b2sdk as soon as we rely on 1.0.0
Expand Down
3 changes: 3 additions & 0 deletions changelog.d/+b2_uri_cmds.deprecated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Deprecated `download-file-by-id` and `download-file-by-name`, use `download-file` instead.
Deprecated `get-file-info`, use `file-info` instead.
Deprecated `make-url` and `make-friendly-url`, use `get-url` instead.
21 changes: 3 additions & 18 deletions test/integration/test_autocomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def test_autocomplete_b2_commands(autocomplete_installed, is_running_on_docker,
if is_running_on_docker:
pytest.skip('Not supported on Docker')
shell.send('b2 \t\t')
shell.expect_exact(["authorize-account", "download-file-by-id", "get-bucket"], timeout=TIMEOUT)
shell.expect_exact(["authorize-account", "download-file", "get-bucket"], timeout=TIMEOUT)


@skip_on_windows
Expand All @@ -69,28 +69,13 @@ def test_autocomplete_b2_only_matching_commands(
):
if is_running_on_docker:
pytest.skip('Not supported on Docker')
shell.send('b2 download-\t\t')
shell.send('b2 delete-\t\t')

shell.expect_exact(
"file-by-", timeout=TIMEOUT
) # common part of remaining cmds is autocompleted
shell.expect_exact("file", timeout=TIMEOUT) # common part of remaining cmds is autocompleted
with pytest.raises(pexpect.exceptions.TIMEOUT): # no other commands are suggested
shell.expect_exact("get-bucket", timeout=0.5)


@skip_on_windows
def test_autocomplete_b2_bucket_n_file_name(
autocomplete_installed, shell, b2_tool, bucket_name, file_name, is_running_on_docker
):
"""Test that autocomplete suggests bucket names and file names."""
if is_running_on_docker:
pytest.skip('Not supported on Docker')
shell.send('b2 download_file_by_name \t\t')
shell.expect_exact(bucket_name, timeout=TIMEOUT)
shell.send(f'{bucket_name} \t\t')
shell.expect_exact(file_name, timeout=TIMEOUT)


@skip_on_windows
def test_autocomplete_b2__download_file__b2uri(
autocomplete_installed, shell, b2_tool, bucket_name, file_name, is_running_on_docker
Expand Down
Loading

0 comments on commit fde190d

Please sign in to comment.