From 5104f3e7f00746b69745f2a6f4065f07edb28a9f Mon Sep 17 00:00:00 2001 From: Olzhas Arystanov Date: Tue, 7 May 2024 21:15:41 +0500 Subject: [PATCH 1/8] Use short description from docstring as usage help --- b2/_internal/arg_parser.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/b2/_internal/arg_parser.py b/b2/_internal/arg_parser.py index 364e6fcd..e9caab8f 100644 --- a/b2/_internal/arg_parser.py +++ b/b2/_internal/arg_parser.py @@ -46,7 +46,7 @@ def add_argument(self, action): if self.show_all: usages.append(f'(DEPRECATED) {choice.format_usage()}') else: - usages.append(choice.format_usage()) + usages.append(choice.format_usage(use_short_description=not self.show_all)) self.add_text(''.join(usages)) else: super().add_argument(action) @@ -112,6 +112,10 @@ def description(self): def description(self, value): self._raw_description = value + @property + def short_description(self): + return self.usage or self.description.split('\n', 1)[0] + def error(self, message): self.print_help() @@ -175,6 +179,14 @@ def _hide_duplicated_action_choices(self, action): yield action.choices = original_choices + def format_usage(self, use_short_description: bool = False): + if not use_short_description or not self.short_description: + return super().format_usage() + + formatter = self._get_formatter() + formatter.add_text(f"{self.prog} {self.short_description}") + return formatter.format_help() + SUPPORT_CAMEL_CASE_ARGUMENTS = False From 5a8d90be7c94266dc68280aea75db33fd7dbae24 Mon Sep 17 00:00:00 2001 From: Olzhas Arystanov Date: Wed, 8 May 2024 00:17:58 +0500 Subject: [PATCH 2/8] Properly align subcommands in help message --- b2/_internal/arg_parser.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/b2/_internal/arg_parser.py b/b2/_internal/arg_parser.py index e9caab8f..a601d128 100644 --- a/b2/_internal/arg_parser.py +++ b/b2/_internal/arg_parser.py @@ -40,13 +40,15 @@ def add_usage(self, usage, actions, groups, prefix=None): def add_argument(self, action): if isinstance(action, argparse._SubParsersAction) and action.help is not argparse.SUPPRESS: usages = [] + col_length = max(len(choice.prog) for choice in action.choices.values()) + for choice in action.choices.values(): deprecated = getattr(choice, 'deprecated', False) if deprecated: if self.show_all: usages.append(f'(DEPRECATED) {choice.format_usage()}') else: - usages.append(choice.format_usage(use_short_description=not self.show_all)) + usages.append(choice.format_usage(use_short_description=not self.show_all, col_length=col_length)) self.add_text(''.join(usages)) else: super().add_argument(action) @@ -179,12 +181,12 @@ def _hide_duplicated_action_choices(self, action): yield action.choices = original_choices - def format_usage(self, use_short_description: bool = False): + def format_usage(self, use_short_description: bool = False, col_length: int = 16): if not use_short_description or not self.short_description: return super().format_usage() formatter = self._get_formatter() - formatter.add_text(f"{self.prog} {self.short_description}") + formatter.add_text(f"{self.prog:{col_length + 2}} {self.short_description}") return formatter.format_help() From d3505138847a8e4f1c09ade681f0a0924b9da7f7 Mon Sep 17 00:00:00 2001 From: Olzhas Arystanov Date: Wed, 8 May 2024 00:49:24 +0500 Subject: [PATCH 3/8] Update docstrings for cleaner help messages --- b2/_internal/console_tool.py | 79 +++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index 0ecc0601..b077bfdc 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -1250,6 +1250,8 @@ def _run(self, args): class AccountAuthorizeBase(Command): """ + Authorize an account with credentials. + Prompts for Backblaze ``applicationKeyId`` and ``applicationKey`` (unless they are given on the command line). @@ -1411,7 +1413,7 @@ def _run(self, args): class AccountClearBase(Command): """ - Erases everything in local cache. + Erase everything in local cache. See @@ -1435,6 +1437,7 @@ class FileCopyByIdBase( ): """ Copy a file version to the given bucket (server-side, **not** via download+upload). + Copies the contents of the source B2 file to destination bucket and assigns the given name to the new B2 file, possibly setting options like server-side encryption and retention. @@ -1570,8 +1573,9 @@ def _determine_source_metadata( class BucketCreateBase(DefaultSseMixin, LifecycleRulesMixin, Command): """ - Creates a new bucket. Prints the ID of the bucket created. + Create a new bucket. + Prints the ID of the bucket created. Optionally stores bucket info, CORS rules and lifecycle rules with the bucket. These can be given as JSON on the command line. @@ -1627,7 +1631,9 @@ def _run(self, args): class KeyCreateBase(Command): """ - Creates a new application key. Prints the application key information. This is the only + Create a new application key. + + Prints the application key information. This is the only time the application key itself will be returned. Listing application keys will show their IDs, but not the secret keys. @@ -1694,7 +1700,7 @@ def _run(self, args): class BucketDeleteBase(Command): """ - Deletes the bucket with the given name. + Delete the bucket with the given name. Requires capability: @@ -1714,7 +1720,7 @@ def _run(self, args): class DeleteFileVersionBase(FileIdAndOptionalFileNameMixin, Command): """ - Permanently and irrevocably deletes one version of a file. + Permanently and irrevocably delete one version of a file. {FileIdAndOptionalFileNameMixin} @@ -1745,7 +1751,7 @@ def _run(self, args): class KeyDeleteBase(Command): """ - Deletes the specified application key by its ID. + Delete the specified application key by ID. Requires capability: @@ -1907,7 +1913,7 @@ class FileDownloadBase( DownloadCommand, ): """ - Downloads the given file-like object, and stores it in the given local file. + Download the given file-like object, and store it in the given local file. {ProgressMixin} {ThreadsMixin} @@ -1978,7 +1984,9 @@ def _run(self, args): class AccountGetBase(Command): """ - Shows the account ID, key, auth token, URLs, and what capabilities + Show current account info + + Prints account ID, key, auth token, URLs, and what capabilities the current application keys has. """ @@ -1990,6 +1998,8 @@ def _run(self, args): class BucketGetBase(Command): """ + Display bucket info + Prints all of the information about the bucket, including bucket info, CORS rules and lifecycle rules. @@ -2050,6 +2060,8 @@ def _run(self, args): class FileInfoBase(Command): """ + Print file info + Prints all of the information about the object, but not its contents. Requires capability: @@ -2066,6 +2078,8 @@ def _run(self, args): class BucketGetDownloadAuthBase(Command): """ + Display authorization token for downloading files + Prints an authorization token that is valid only for downloading files from the given bucket. @@ -2099,7 +2113,9 @@ def _run(self, args): class GetDownloadUrlWithAuthBase(Command): """ - Prints a URL to download the given file. The URL includes an authorization + Print a URL to download the given file. + + The URL includes an authorization token that allows downloads from the given bucket for files whose names start with the given file name. @@ -2135,7 +2151,7 @@ def _run(self, args): class FileHideBase(Command): """ - Uploads a new, hidden, version of the given file. + Upload a new, hidden, version of the given file. Requires capability: @@ -2157,7 +2173,7 @@ def _run(self, args): class BucketListBase(Command): """ - Lists all of the buckets in the current account. + List all of the buckets in the current account. Output lines list the bucket ID, bucket type, and bucket name, and look like this: @@ -2196,7 +2212,7 @@ def run_list_buckets(cls, command: Command, *, json_: bool) -> int: class KeyListBase(Command): """ - Lists the application keys for the current account. + List the application keys for the current account. The columns in the output are: @@ -2385,6 +2401,8 @@ def get_b2_uri_from_arg(self, args: argparse.Namespace) -> B2URI: class BaseLs(AbstractLsCommand, metaclass=ABCMeta): """ + List files in a given folder. + Using the file naming convention that ``/`` separates folder names from their contents, returns a list of the files and folders in a given folder. If no folder name is given, @@ -2536,7 +2554,9 @@ class Ls(B2IDOrB2URIMixin, BaseLs): class BaseRm(ThreadsMixin, AbstractLsCommand, metaclass=ABCMeta): """ - Removes a "folder" or a set of files matching a pattern. Use with caution. + Remove a "folder" or a set of files matching a pattern. + + Use with caution! .. note:: @@ -2757,6 +2777,8 @@ class Rm(B2IDOrB2URIMixin, BaseRm): class FileUrlBase(Command): """ + Display download URL for a file + Prints an URL that can be used to download the given file, if it is public. @@ -2807,8 +2829,9 @@ class Sync( Command, ): """ - Copies multiple files from source to destination. Optionally - deletes or hides destination files that the source does not have. + Copy multiple files from source to destination. + + Optionally deletes or hides destination files that the source does not have. The synchronizer can copy files: @@ -3154,9 +3177,9 @@ def get_synchronizer_from_args( class BucketUpdateBase(DefaultSseMixin, LifecycleRulesMixin, Command): """ - Updates the ``bucketType`` of an existing bucket. Prints the ID - of the bucket updated. + Updates the ``bucketType`` of an existing bucket. + Prints the ID of the bucket updated. Optionally stores bucket info, CORS rules and lifecycle rules with the bucket. These can be given as JSON on the command line. @@ -3431,7 +3454,7 @@ class NotAnInputStream(Exception): class FileUploadBase(UploadFileMixin, UploadModeMixin, Command): """ - Uploads one file to the given bucket. + Upload single file to the given bucket. Uploads the contents of the local file, and assigns the given name to the B2 file, possibly setting options like server-side encryption and retention. @@ -3572,6 +3595,8 @@ def execute_operation(self, local_file, bucket, threads, **kwargs): class FileUpdateBase(B2URIFileArgMixin, LegalHoldMixin, Command): """ + Update file settings. + Setting legal holds only works in bucket with fileLockEnabled=true. Retention: @@ -3726,6 +3751,8 @@ def _run(self, args): class ReplicationSetupBase(Command): """ + Set up replication between two buckets. + Sets up replication between two buckets (potentially from different accounts), creating and replacing keys if necessary. Requires capabilities on both profiles: @@ -3852,7 +3879,7 @@ def alter_one_rule(cls, rule: ReplicationRule) -> ReplicationRule | None: class ReplicationDeleteBase(ReplicationRuleChanger): """ - Deletes a replication rule + Delete a replication rule Requires capabilities: @@ -3868,7 +3895,7 @@ def alter_one_rule(cls, rule: ReplicationRule) -> ReplicationRule | None: class ReplicationPauseBase(ReplicationRuleChanger): """ - Pauses a replication rule + Pause a replication rule Requires capabilities: @@ -3885,7 +3912,7 @@ def alter_one_rule(cls, rule: ReplicationRule) -> ReplicationRule | None: class ReplicationUnpauseBase(ReplicationRuleChanger): """ - Unpauses a replication rule + Unpause a replication rule Requires capabilities: @@ -3902,6 +3929,8 @@ def alter_one_rule(cls, rule: ReplicationRule) -> ReplicationRule | None: class ReplicationStatusBase(Command): """ + Display detailed replication statistics + Inspects files in only source or both source and destination buckets (potentially from different accounts) and provides detailed replication statistics. @@ -4069,7 +4098,7 @@ def output_csv(self, results: dict[str, list[dict]]) -> None: class Version(Command): """ - Prints the version number of this tool. + Print the version number of this tool. """ REQUIRES_AUTH = False @@ -4089,7 +4118,9 @@ def _run(self, args): class License(Command): # pragma: no cover """ - Prints the license of B2 Command line tool and all libraries shipped with it. + Print the license information for this tool. + + Displays the license of B2 Command line tool and all libraries shipped with it. """ LICENSE_OUTPUT_FILE = pathlib.Path(__file__).parent.parent / 'licenses_output.txt' @@ -4279,7 +4310,7 @@ def _get_single_license(self, module_dict: dict): class InstallAutocomplete(Command): """ - Installs autocomplete for supported shells. + Install autocomplete for supported shells. Autocomplete is installed for the current user only and will become available after shell reload. Any existing autocomplete configuration for same executable name will be overwritten. From 0cee10a82938235196ab4c3995dde09458b820b8 Mon Sep 17 00:00:00 2001 From: Olzhas Arystanov Date: Wed, 8 May 2024 00:55:58 +0500 Subject: [PATCH 4/8] Add changelog item --- changelog.d/+help_usage_desc.doc.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/+help_usage_desc.doc.md diff --git a/changelog.d/+help_usage_desc.doc.md b/changelog.d/+help_usage_desc.doc.md new file mode 100644 index 00000000..98dac25d --- /dev/null +++ b/changelog.d/+help_usage_desc.doc.md @@ -0,0 +1 @@ +Display short descriptions instead of arguments in subcommands help messages \ No newline at end of file From 3154fb7b244d1a802d427e599271db2740187e56 Mon Sep 17 00:00:00 2001 From: Olzhas Arystanov Date: Wed, 8 May 2024 13:24:59 +0500 Subject: [PATCH 5/8] Format with yapf --- b2/_internal/arg_parser.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/b2/_internal/arg_parser.py b/b2/_internal/arg_parser.py index a601d128..86c7a064 100644 --- a/b2/_internal/arg_parser.py +++ b/b2/_internal/arg_parser.py @@ -48,7 +48,11 @@ def add_argument(self, action): if self.show_all: usages.append(f'(DEPRECATED) {choice.format_usage()}') else: - usages.append(choice.format_usage(use_short_description=not self.show_all, col_length=col_length)) + usages.append( + choice.format_usage( + use_short_description=not self.show_all, col_length=col_length + ) + ) self.add_text(''.join(usages)) else: super().add_argument(action) From e35eb8665d43d8bab10c2573e212e3b3b06ffb41 Mon Sep 17 00:00:00 2001 From: Olzhas Arystanov Date: Wed, 8 May 2024 16:09:04 +0500 Subject: [PATCH 6/8] Fix changelog entry --- changelog.d/+help_usage_desc.doc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/+help_usage_desc.doc.md b/changelog.d/+help_usage_desc.doc.md index 98dac25d..770360f1 100644 --- a/changelog.d/+help_usage_desc.doc.md +++ b/changelog.d/+help_usage_desc.doc.md @@ -1 +1 @@ -Display short descriptions instead of arguments in subcommands help messages \ No newline at end of file +Display short descriptions instead of arguments in subcommands help messages. From 6000fe77cf0238d8b8533464f158667198c8922e Mon Sep 17 00:00:00 2001 From: Olzhas Arystanov Date: Wed, 8 May 2024 21:59:34 +0500 Subject: [PATCH 7/8] Cache short_description in parser for help message --- b2/_internal/arg_parser.py | 39 +++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/b2/_internal/arg_parser.py b/b2/_internal/arg_parser.py index 86c7a064..95154fde 100644 --- a/b2/_internal/arg_parser.py +++ b/b2/_internal/arg_parser.py @@ -91,8 +91,10 @@ def __init__( self._description = None self._for_docs = for_docs self.deprecated = deprecated + self._short_description = self._make_short_description(kwargs.get('usage', ''), kwargs.get('description', '')) kwargs.setdefault('formatter_class', B2RawTextHelpFormatter) super().__init__(*args, **kwargs) + if add_help_all: self.register('action', 'help_all', _HelpAllAction) self.add_argument( @@ -104,23 +106,34 @@ def __init__( @property def description(self): if self._description is None and self._raw_description is not None: - if self._for_docs: - self._description = textwrap.dedent(self._raw_description) - else: - encoding = self._get_encoding() - self._description = rst2ansi( - self._raw_description.encode(encoding), output_encoding=encoding - ) - + self._description = self._encode_description(self._raw_description) return self._description @description.setter def description(self, value): self._raw_description = value - @property - def short_description(self): - return self.usage or self.description.split('\n', 1)[0] + def _encode_description(self, value: str): + if self._for_docs: + return textwrap.dedent(value) + else: + encoding = self._get_encoding() + return rst2ansi( + value.encode(encoding), output_encoding=encoding + ) + + def _make_short_description(self, usage: str, raw_description: str) -> str: + if usage: + return usage + + if not raw_description: + return "" + + for line in raw_description.splitlines(): + if line.strip(): + return self._encode_description(line.strip()) + + return "" def error(self, message): self.print_help() @@ -186,11 +199,11 @@ def _hide_duplicated_action_choices(self, action): action.choices = original_choices def format_usage(self, use_short_description: bool = False, col_length: int = 16): - if not use_short_description or not self.short_description: + if not use_short_description or not self._short_description: return super().format_usage() formatter = self._get_formatter() - formatter.add_text(f"{self.prog:{col_length + 2}} {self.short_description}") + formatter.add_text(f"{self.prog:{col_length + 2}} {self._short_description}") return formatter.format_help() From 6b0448e7f72ae338682df2c1371caca5d889c164 Mon Sep 17 00:00:00 2001 From: Olzhas Arystanov Date: Wed, 8 May 2024 22:00:48 +0500 Subject: [PATCH 8/8] Format with yapf --- b2/_internal/arg_parser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/b2/_internal/arg_parser.py b/b2/_internal/arg_parser.py index 95154fde..ddfe9d3f 100644 --- a/b2/_internal/arg_parser.py +++ b/b2/_internal/arg_parser.py @@ -91,7 +91,9 @@ def __init__( self._description = None self._for_docs = for_docs self.deprecated = deprecated - self._short_description = self._make_short_description(kwargs.get('usage', ''), kwargs.get('description', '')) + self._short_description = self._make_short_description( + kwargs.get('usage', ''), kwargs.get('description', '') + ) kwargs.setdefault('formatter_class', B2RawTextHelpFormatter) super().__init__(*args, **kwargs) @@ -118,9 +120,7 @@ def _encode_description(self, value: str): return textwrap.dedent(value) else: encoding = self._get_encoding() - return rst2ansi( - value.encode(encoding), output_encoding=encoding - ) + return rst2ansi(value.encode(encoding), output_encoding=encoding) def _make_short_description(self, usage: str, raw_description: str) -> str: if usage: