From b291298c7b8cfc872f5ea6de597c8743821cf00d Mon Sep 17 00:00:00 2001 From: Krzysztof Kalinowski Date: Tue, 12 Dec 2023 14:06:28 +0400 Subject: [PATCH] ApiVer introduced into CLI - `b2`, `b2v3` and unstable `_b2v4` scripts and binaries are now built. - Tests are ran against all available versions. - Both unit and integration tests have the ability to limit versions on which given tests are running. - `doc` is built only for the latest stable version. - Implementation moved to `_internal` to discourage people from importing it. --- .github/workflows/ci.yml | 4 +- .gitignore | 1 + README.md | 20 ++ b2.spec => b2.spec.template | 4 +- b2/__init__.py | 4 +- b2/{_utils => _internal}/__init__.py | 2 +- b2/_internal/_b2v4/__init__.py | 11 ++ b2/_internal/_b2v4/__main__.py | 13 ++ b2/_internal/_b2v4/registry.py | 57 ++++++ b2/{ => _internal}/_cli/__init__.py | 4 +- b2/{ => _internal}/_cli/arg_parser_types.py | 4 +- b2/{ => _internal}/_cli/argcompleters.py | 12 +- b2/{ => _internal}/_cli/autocomplete_cache.py | 4 +- .../_cli/autocomplete_install.py | 4 +- b2/{ => _internal}/_cli/b2api.py | 4 +- b2/{ => _internal}/_cli/b2args.py | 8 +- b2/{ => _internal}/_cli/const.py | 2 +- b2/{ => _internal}/_cli/obj_loads.py | 2 +- b2/{ => _internal}/_cli/shell.py | 2 +- .../_utils/__init__.py} | 8 +- b2/{ => _internal}/_utils/python_compat.py | 2 +- b2/{ => _internal}/_utils/uri.py | 4 +- b2/{ => _internal}/arg_parser.py | 2 +- b2/_internal/b2v3/__init__.py | 11 ++ b2/_internal/b2v3/__main__.py | 13 ++ b2/_internal/b2v3/registry.py | 57 ++++++ b2/{ => _internal}/console_tool.py | 82 +++----- b2/{ => _internal}/json_encoder.py | 2 +- b2/{ => _internal}/version.py | 2 +- b2/_internal/version_listing.py | 32 ++++ changelog.d/+apiver_for_cli.added.md | 1 + changelog.d/+internal_directory.changed.md | 1 + doc/source/conf.py | 10 +- doc/source/subcommands/authorize_account.rst | 2 +- .../cancel_all_unfinished_large_files.rst | 2 +- doc/source/subcommands/cancel_large_file.rst | 2 +- doc/source/subcommands/cat.rst | 2 +- doc/source/subcommands/clear_account.rst | 2 +- doc/source/subcommands/copy_file_by_id.rst | 2 +- doc/source/subcommands/create_bucket.rst | 2 +- doc/source/subcommands/create_key.rst | 2 +- doc/source/subcommands/delete_bucket.rst | 2 +- .../subcommands/delete_file_version.rst | 2 +- doc/source/subcommands/delete_key.rst | 2 +- doc/source/subcommands/download_file.rst | 2 +- .../subcommands/download_file_by_id.rst | 2 +- .../subcommands/download_file_by_name.rst | 2 +- doc/source/subcommands/file_info.rst | 2 +- doc/source/subcommands/get_account_info.rst | 2 +- doc/source/subcommands/get_bucket.rst | 2 +- doc/source/subcommands/get_download_auth.rst | 2 +- .../get_download_url_with_auth.rst | 2 +- doc/source/subcommands/get_file_info.rst | 2 +- doc/source/subcommands/get_url.rst | 2 +- doc/source/subcommands/hide_file.rst | 2 +- .../subcommands/install_autocomplete.rst | 2 +- doc/source/subcommands/list_buckets.rst | 2 +- doc/source/subcommands/list_keys.rst | 2 +- doc/source/subcommands/list_parts.rst | 2 +- .../list_unfinished_large_files.rst | 2 +- doc/source/subcommands/ls.rst | 2 +- doc/source/subcommands/make_friendly_url.rst | 2 +- doc/source/subcommands/make_url.rst | 2 +- doc/source/subcommands/replication-setup.rst | 2 +- doc/source/subcommands/rm.rst | 2 +- doc/source/subcommands/sync.rst | 2 +- doc/source/subcommands/update_bucket.rst | 2 +- .../subcommands/update_file_legal_hold.rst | 2 +- .../subcommands/update_file_retention.rst | 2 +- doc/source/subcommands/upload_file.rst | 2 +- doc/source/subcommands/version.rst | 2 +- noxfile.py | 177 +++++++++++++----- pyinstaller-hooks/hook-b2.py | 18 +- pyproject.toml | 4 +- setup.py | 7 +- test/conftest.py | 75 ++++++++ test/integration/conftest.py | 66 ++++++- test/integration/helpers.py | 2 +- test/integration/test_autocomplete.py | 32 +++- test/integration/test_b2_command_line.py | 20 +- test/unit/_cli/test_autocomplete_cache.py | 14 +- test/unit/_cli/test_autocomplete_install.py | 2 +- test/unit/_cli/test_shell.py | 2 +- test/unit/_utils/test_uri.py | 2 +- test/unit/conftest.py | 57 +++++- test/unit/console_tool/conftest.py | 6 +- .../console_tool/test_authorize_account.py | 2 +- test/unit/test_apiver.py | 67 +++++++ test/unit/test_arg_parser.py | 6 +- test/unit/test_base.py | 6 + test/unit/test_console_tool.py | 19 +- test/unit/test_copy.py | 2 +- test/unit/test_represent_file_metadata.py | 2 +- 93 files changed, 807 insertions(+), 246 deletions(-) rename b2.spec => b2.spec.template (92%) rename b2/{_utils => _internal}/__init__.py (88%) create mode 100644 b2/_internal/_b2v4/__init__.py create mode 100644 b2/_internal/_b2v4/__main__.py create mode 100644 b2/_internal/_b2v4/registry.py rename b2/{ => _internal}/_cli/__init__.py (90%) rename b2/{ => _internal}/_cli/arg_parser_types.py (96%) rename b2/{ => _internal}/_cli/argcompleters.py (88%) rename b2/{ => _internal}/_cli/autocomplete_cache.py (98%) rename b2/{ => _internal}/_cli/autocomplete_install.py (98%) rename b2/{ => _internal}/_cli/b2api.py (92%) rename b2/{ => _internal}/_cli/b2args.py (82%) rename b2/{ => _internal}/_cli/const.py (96%) rename b2/{ => _internal}/_cli/obj_loads.py (97%) rename b2/{ => _internal}/_cli/shell.py (93%) rename b2/{__main__.py => _internal/_utils/__init__.py} (63%) rename b2/{ => _internal}/_utils/python_compat.py (96%) rename b2/{ => _internal}/_utils/uri.py (97%) rename b2/{ => _internal}/arg_parser.py (99%) create mode 100644 b2/_internal/b2v3/__init__.py create mode 100644 b2/_internal/b2v3/__main__.py create mode 100644 b2/_internal/b2v3/registry.py rename b2/{ => _internal}/console_tool.py (98%) rename b2/{ => _internal}/json_encoder.py (96%) rename b2/{ => _internal}/version.py (92%) create mode 100644 b2/_internal/version_listing.py create mode 100644 changelog.d/+apiver_for_cli.added.md create mode 100644 changelog.d/+internal_directory.changed.md create mode 100644 test/conftest.py create mode 100644 test/unit/test_apiver.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a29fe0494..acbd3d3b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,7 +167,7 @@ jobs: id: hashes run: nox -vs make_dist_digest - name: Run integration tests (without secrets) - run: nox -vs integration -- -m "not require_secrets" + run: nox -vs integration -- --sut=${{ steps.bundle.outputs.sut_path }} -m "not require_secrets" - name: Run integration tests (with secrets) if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' }} run: nox -vs integration -- --sut=${{ steps.bundle.outputs.sut_path }} -m "require_secrets" --cleanup @@ -206,7 +206,7 @@ jobs: id: hashes run: nox -vs make_dist_digest - name: Run integration tests (without secrets) - run: nox -vs integration -- -m "not require_secrets" + run: nox -vs integration -- --sut=${{ steps.bundle.outputs.sut_path }} -m "not require_secrets" - name: Run integration tests (with secrets) if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' }} run: nox -vs integration -- --sut=${{ steps.bundle.outputs.sut_path }} -m "require_secrets" --cleanup diff --git a/.gitignore b/.gitignore index c0cd70cdf..ee2ed042e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ venv doc/source/main_help.rst Dockerfile b2/licenses_output.txt +*.spec diff --git a/README.md b/README.md index 2672c9fd1..21fddd259 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,26 @@ or by mounting local files in the docker container: docker run --rm -v b2:/root -v /home/user/path/to/data:/data backblazeit/b2:latest upload-file bucket_name /data/source_file.txt target_file_name ``` +## Versions + +When you start working with `b2`, you might notice that more than one script is available to you. +This is by design - we use the `ApiVer` methodology to provide all the commands to all the versions +while also providing all the bugfixes to all the old versions. + +If you use the `b2` command, you're working with the latest stable version. +It provides all the bells and whistles, latest features, and the best performance. +While it's a great version to work with, if you're willing to write a reliable, long-running script, +you might find out that after some time it will break. +New commands will appear, older will deprecate and be removed, parameters will change. +Backblaze service evolves and the `b2` CLI evolves with it. + +However, now you have a way around this problem. +Instead of using the `b2` command, you can use a version-bound interface e.g.: `b2v3`. +This command will always provide the same interface that the `ApiVer` version `3` provided. +Even if the `b2` command goes into the `ApiVer` version `4`, `6` or even `10` with some major changes, +`b2v3` will still provide the same interface, same commands, and same parameters. +Over time, it might get slower as we may need to emulate some older behaviors, but we'll ensure that it won't break. + ## Contrib ### Detailed logs diff --git a/b2.spec b/b2.spec.template similarity index 92% rename from b2.spec rename to b2.spec.template index 10d8700b2..021bc6830 100644 --- a/b2.spec +++ b/b2.spec.template @@ -8,7 +8,7 @@ block_cipher = None # https://github.com/Backblaze/B2_Command_Line_Tool/issues/689 datas = copy_metadata('b2') + collect_data_files('dateutil') -a = Analysis(['b2/console_tool.py'], +a = Analysis(['b2/_internal/${VERSION}/__main__.py'], pathex=['.'], binaries=[], datas=datas, @@ -30,7 +30,7 @@ exe = EXE(pyz, a.zipfiles, a.datas, [], - name='b2', + name='${NAME}', debug=False, bootloader_ignore_signals=False, strip=False, diff --git a/b2/__init__.py b/b2/__init__.py index 43a144eff..988bae414 100644 --- a/b2/__init__.py +++ b/b2/__init__.py @@ -13,7 +13,7 @@ logging.getLogger(__name__).addHandler(logging.NullHandler()) -import b2.version # noqa: E402 +import b2._internal.version # noqa: E402 -__version__ = b2.version.VERSION +__version__ = b2._internal.version.VERSION assert __version__ # PEP-0396 diff --git a/b2/_utils/__init__.py b/b2/_internal/__init__.py similarity index 88% rename from b2/_utils/__init__.py rename to b2/_internal/__init__.py index 1bc414dca..f5ea8fb90 100644 --- a/b2/_utils/__init__.py +++ b/b2/_internal/__init__.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/_utils/__init__.py +# File: b2/_internal/__init__.py # # Copyright 2023 Backblaze Inc. All Rights Reserved. # diff --git a/b2/_internal/_b2v4/__init__.py b/b2/_internal/_b2v4/__init__.py new file mode 100644 index 000000000..b60833325 --- /dev/null +++ b/b2/_internal/_b2v4/__init__.py @@ -0,0 +1,11 @@ +###################################################################### +# +# File: b2/_internal/_b2v4/__init__.py +# +# Copyright 2023 Backblaze Inc. All Rights Reserved. +# +# License https://www.backblaze.com/using_b2_code.html +# +###################################################################### + +# Note: importing console_tool in any shape or form in here will break sys.argv. diff --git a/b2/_internal/_b2v4/__main__.py b/b2/_internal/_b2v4/__main__.py new file mode 100644 index 000000000..d381bc03d --- /dev/null +++ b/b2/_internal/_b2v4/__main__.py @@ -0,0 +1,13 @@ +###################################################################### +# +# File: b2/_internal/_b2v4/__main__.py +# +# Copyright 2023 Backblaze Inc. All Rights Reserved. +# +# License https://www.backblaze.com/using_b2_code.html +# +###################################################################### + +from b2._internal._b2v4.registry import main + +main() diff --git a/b2/_internal/_b2v4/registry.py b/b2/_internal/_b2v4/registry.py new file mode 100644 index 000000000..d6deb14e3 --- /dev/null +++ b/b2/_internal/_b2v4/registry.py @@ -0,0 +1,57 @@ +###################################################################### +# +# File: b2/_internal/_b2v4/registry.py +# +# Copyright 2023 Backblaze Inc. All Rights Reserved. +# +# License https://www.backblaze.com/using_b2_code.html +# +###################################################################### + +# ruff: noqa: F405 +from b2._internal.console_tool import * # noqa + +B2.register_subcommand(AuthorizeAccount) +B2.register_subcommand(CancelAllUnfinishedLargeFiles) +B2.register_subcommand(CancelLargeFile) +B2.register_subcommand(ClearAccount) +B2.register_subcommand(CopyFileById) +B2.register_subcommand(CreateBucket) +B2.register_subcommand(CreateKey) +B2.register_subcommand(DeleteBucket) +B2.register_subcommand(DeleteFileVersion) +B2.register_subcommand(DeleteKey) +B2.register_subcommand(DownloadFile) +B2.register_subcommand(DownloadFileById) +B2.register_subcommand(DownloadFileByName) +B2.register_subcommand(Cat) +B2.register_subcommand(GetAccountInfo) +B2.register_subcommand(GetBucket) +B2.register_subcommand(FileInfo) +B2.register_subcommand(GetFileInfo) +B2.register_subcommand(GetDownloadAuth) +B2.register_subcommand(GetDownloadUrlWithAuth) +B2.register_subcommand(HideFile) +B2.register_subcommand(ListBuckets) +B2.register_subcommand(ListKeys) +B2.register_subcommand(ListParts) +B2.register_subcommand(ListUnfinishedLargeFiles) +B2.register_subcommand(Ls) +B2.register_subcommand(Rm) +B2.register_subcommand(GetUrl) +B2.register_subcommand(MakeUrl) +B2.register_subcommand(MakeFriendlyUrl) +B2.register_subcommand(Sync) +B2.register_subcommand(UpdateBucket) +B2.register_subcommand(UploadFile) +B2.register_subcommand(UploadUnboundStream) +B2.register_subcommand(UpdateFileLegalHold) +B2.register_subcommand(UpdateFileRetention) +B2.register_subcommand(ReplicationSetup) +B2.register_subcommand(ReplicationDelete) +B2.register_subcommand(ReplicationPause) +B2.register_subcommand(ReplicationUnpause) +B2.register_subcommand(ReplicationStatus) +B2.register_subcommand(Version) +B2.register_subcommand(License) +B2.register_subcommand(InstallAutocomplete) diff --git a/b2/_cli/__init__.py b/b2/_internal/_cli/__init__.py similarity index 90% rename from b2/_cli/__init__.py rename to b2/_internal/_cli/__init__.py index c978f6ff0..3e5c4ef7a 100644 --- a/b2/_cli/__init__.py +++ b/b2/_internal/_cli/__init__.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/_cli/__init__.py +# File: b2/_internal/_cli/__init__.py # # Copyright 2023 Backblaze Inc. All Rights Reserved. # @@ -11,4 +11,4 @@ _cli package contains internals of the command-line interface to the B2. It is not intended to be used as a library. -""" \ No newline at end of file +""" diff --git a/b2/_cli/arg_parser_types.py b/b2/_internal/_cli/arg_parser_types.py similarity index 96% rename from b2/_cli/arg_parser_types.py rename to b2/_internal/_cli/arg_parser_types.py index 93d0357ff..43b2de85a 100644 --- a/b2/_cli/arg_parser_types.py +++ b/b2/_internal/_cli/arg_parser_types.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/_cli/arg_parser_types.py +# File: b2/_internal/_cli/arg_parser_types.py # # Copyright 2020 Backblaze Inc. All Rights Reserved. # @@ -75,4 +75,4 @@ def wrapper(*args, **kwargs): except exc_type as e: raise argparse.ArgumentTypeError(translator(e)) - return wrapper \ No newline at end of file + return wrapper diff --git a/b2/_cli/argcompleters.py b/b2/_internal/_cli/argcompleters.py similarity index 88% rename from b2/_cli/argcompleters.py rename to b2/_internal/_cli/argcompleters.py index dc070eebe..e3c4cfdb5 100644 --- a/b2/_cli/argcompleters.py +++ b/b2/_internal/_cli/argcompleters.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/_cli/argcompleters.py +# File: b2/_internal/_cli/argcompleters.py # # Copyright 2023 Backblaze Inc. All Rights Reserved. # @@ -16,7 +16,7 @@ def bucket_name_completer(prefix, parsed_args, **kwargs): - from b2._cli.b2api import _get_b2api_for_profile + from b2._internal._cli.b2api import _get_b2api_for_profile api = _get_b2api_for_profile(getattr(parsed_args, 'profile', None)) res = [bucket.name for bucket in api.list_buckets(use_cache=True)] return res @@ -30,7 +30,7 @@ def file_name_completer(prefix, parsed_args, **kwargs): """ from b2sdk.v2 import LIST_FILE_NAMES_MAX_LIMIT - from b2._cli.b2api import _get_b2api_for_profile + from b2._internal._cli.b2api import _get_b2api_for_profile api = _get_b2api_for_profile(parsed_args.profile) bucket = api.get_bucket_by_name(parsed_args.bucketName) @@ -52,9 +52,9 @@ def b2uri_file_completer(prefix: str, parsed_args, **kwargs): """ from b2sdk.v2 import LIST_FILE_NAMES_MAX_LIMIT - from b2._cli.b2api import _get_b2api_for_profile - from b2._utils.python_compat import removeprefix - from b2._utils.uri import parse_b2_uri + from b2._internal._cli.b2api import _get_b2api_for_profile + from b2._internal._utils.python_compat import removeprefix + from b2._internal._utils.uri import parse_b2_uri api = _get_b2api_for_profile(getattr(parsed_args, 'profile', None)) if prefix.startswith('b2://'): diff --git a/b2/_cli/autocomplete_cache.py b/b2/_internal/_cli/autocomplete_cache.py similarity index 98% rename from b2/_cli/autocomplete_cache.py rename to b2/_internal/_cli/autocomplete_cache.py index cdd5dfef1..443f759a8 100644 --- a/b2/_cli/autocomplete_cache.py +++ b/b2/_internal/_cli/autocomplete_cache.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/_cli/autocomplete_cache.py +# File: b2/_internal/_cli/autocomplete_cache.py # # Copyright 2020 Backblaze Inc. All Rights Reserved. # @@ -19,7 +19,7 @@ import argcomplete import platformdirs -from b2.version import VERSION +from b2._internal.version import VERSION def identity(x): diff --git a/b2/_cli/autocomplete_install.py b/b2/_internal/_cli/autocomplete_install.py similarity index 98% rename from b2/_cli/autocomplete_install.py rename to b2/_internal/_cli/autocomplete_install.py index 4761e75e8..4d8cc68e7 100644 --- a/b2/_cli/autocomplete_install.py +++ b/b2/_internal/_cli/autocomplete_install.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/_cli/autocomplete_install.py +# File: b2/_internal/_cli/autocomplete_install.py # # Copyright 2023 Backblaze Inc. All Rights Reserved. # @@ -157,4 +157,4 @@ class AutocompleteInstallError(Exception): """Exception raised when autocomplete installation fails.""" -SUPPORTED_SHELLS = sorted(SHELL_REGISTRY.keys()) \ No newline at end of file +SUPPORTED_SHELLS = sorted(SHELL_REGISTRY.keys()) diff --git a/b2/_cli/b2api.py b/b2/_internal/_cli/b2api.py similarity index 92% rename from b2/_cli/b2api.py rename to b2/_internal/_cli/b2api.py index 5cda2b6d3..adfd6a797 100644 --- a/b2/_cli/b2api.py +++ b/b2/_internal/_cli/b2api.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/_cli/b2api.py +# File: b2/_internal/_cli/b2api.py # # Copyright 2023 Backblaze Inc. All Rights Reserved. # @@ -18,7 +18,7 @@ SqliteAccountInfo, ) -from b2._cli.const import B2_USER_AGENT_APPEND_ENV_VAR +from b2._internal._cli.const import B2_USER_AGENT_APPEND_ENV_VAR def _get_b2api_for_profile(profile: Optional[str] = None, **kwargs) -> B2Api: diff --git a/b2/_cli/b2args.py b/b2/_internal/_cli/b2args.py similarity index 82% rename from b2/_cli/b2args.py rename to b2/_internal/_cli/b2args.py index d85379f32..178d81d8d 100644 --- a/b2/_cli/b2args.py +++ b/b2/_internal/_cli/b2args.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/_cli/b2args.py +# File: b2/_internal/_cli/b2args.py # # Copyright 2023 Backblaze Inc. All Rights Reserved. # @@ -12,9 +12,9 @@ """ import argparse -from b2._cli.arg_parser_types import wrap_with_argument_type_error -from b2._cli.argcompleters import b2uri_file_completer -from b2._utils.uri import B2URI, B2URIBase, parse_b2_uri +from b2._internal._cli.arg_parser_types import wrap_with_argument_type_error +from b2._internal._cli.argcompleters import b2uri_file_completer +from b2._internal._utils.uri import B2URI, B2URIBase, parse_b2_uri def b2_file_uri(value: str) -> B2URIBase: diff --git a/b2/_cli/const.py b/b2/_internal/_cli/const.py similarity index 96% rename from b2/_cli/const.py rename to b2/_internal/_cli/const.py index 64deb23e8..d78e66427 100644 --- a/b2/_cli/const.py +++ b/b2/_internal/_cli/const.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/_cli/const.py +# File: b2/_internal/_cli/const.py # # Copyright 2023 Backblaze Inc. All Rights Reserved. # diff --git a/b2/_cli/obj_loads.py b/b2/_internal/_cli/obj_loads.py similarity index 97% rename from b2/_cli/obj_loads.py rename to b2/_internal/_cli/obj_loads.py index dfd12b0ec..5ad2b48ef 100644 --- a/b2/_cli/obj_loads.py +++ b/b2/_internal/_cli/obj_loads.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/_cli/obj_loads.py +# File: b2/_internal/_cli/obj_loads.py # # Copyright 2023 Backblaze Inc. All Rights Reserved. # diff --git a/b2/_cli/shell.py b/b2/_internal/_cli/shell.py similarity index 93% rename from b2/_cli/shell.py rename to b2/_internal/_cli/shell.py index f43fdf0eb..b175c7650 100644 --- a/b2/_cli/shell.py +++ b/b2/_internal/_cli/shell.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/_cli/shell.py +# File: b2/_internal/_cli/shell.py # # Copyright 2023 Backblaze Inc. All Rights Reserved. # diff --git a/b2/__main__.py b/b2/_internal/_utils/__init__.py similarity index 63% rename from b2/__main__.py rename to b2/_internal/_utils/__init__.py index eca5f4e1a..9707dcc5a 100644 --- a/b2/__main__.py +++ b/b2/_internal/_utils/__init__.py @@ -1,13 +1,9 @@ ###################################################################### # -# File: b2/__main__.py +# File: b2/_internal/_utils/__init__.py # -# Copyright 2019 Backblaze Inc. All Rights Reserved. +# Copyright 2023 Backblaze Inc. All Rights Reserved. # # License https://www.backblaze.com/using_b2_code.html # ###################################################################### - -from .console_tool import main - -main() diff --git a/b2/_utils/python_compat.py b/b2/_internal/_utils/python_compat.py similarity index 96% rename from b2/_utils/python_compat.py rename to b2/_internal/_utils/python_compat.py index e7945e33a..54b82143d 100644 --- a/b2/_utils/python_compat.py +++ b/b2/_internal/_utils/python_compat.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/_utils/python_compat.py +# File: b2/_internal/_utils/python_compat.py # # Copyright 2023 Backblaze Inc. All Rights Reserved. # diff --git a/b2/_utils/uri.py b/b2/_internal/_utils/uri.py similarity index 97% rename from b2/_utils/uri.py rename to b2/_internal/_utils/uri.py index b82daaa4c..91da61df4 100644 --- a/b2/_utils/uri.py +++ b/b2/_internal/_utils/uri.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/_utils/uri.py +# File: b2/_internal/_utils/uri.py # # Copyright 2023 Backblaze Inc. All Rights Reserved. # @@ -20,7 +20,7 @@ FileVersion, ) -from b2._utils.python_compat import removeprefix, singledispatchmethod +from b2._internal._utils.python_compat import removeprefix, singledispatchmethod class B2URIBase: diff --git a/b2/arg_parser.py b/b2/_internal/arg_parser.py similarity index 99% rename from b2/arg_parser.py rename to b2/_internal/arg_parser.py index 689cdacf7..6df339490 100644 --- a/b2/arg_parser.py +++ b/b2/_internal/arg_parser.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/arg_parser.py +# File: b2/_internal/arg_parser.py # # Copyright 2020 Backblaze Inc. All Rights Reserved. # diff --git a/b2/_internal/b2v3/__init__.py b/b2/_internal/b2v3/__init__.py new file mode 100644 index 000000000..31696aa09 --- /dev/null +++ b/b2/_internal/b2v3/__init__.py @@ -0,0 +1,11 @@ +###################################################################### +# +# File: b2/_internal/b2v3/__init__.py +# +# Copyright 2023 Backblaze Inc. All Rights Reserved. +# +# License https://www.backblaze.com/using_b2_code.html +# +###################################################################### + +# Note: importing console_tool in any shape or form in here will break sys.argv. diff --git a/b2/_internal/b2v3/__main__.py b/b2/_internal/b2v3/__main__.py new file mode 100644 index 000000000..d1deb60bf --- /dev/null +++ b/b2/_internal/b2v3/__main__.py @@ -0,0 +1,13 @@ +###################################################################### +# +# File: b2/_internal/b2v3/__main__.py +# +# Copyright 2023 Backblaze Inc. All Rights Reserved. +# +# License https://www.backblaze.com/using_b2_code.html +# +###################################################################### + +from b2._internal.b2v3.registry import main + +main() diff --git a/b2/_internal/b2v3/registry.py b/b2/_internal/b2v3/registry.py new file mode 100644 index 000000000..7bf0091ab --- /dev/null +++ b/b2/_internal/b2v3/registry.py @@ -0,0 +1,57 @@ +###################################################################### +# +# File: b2/_internal/b2v3/registry.py +# +# Copyright 2023 Backblaze Inc. All Rights Reserved. +# +# License https://www.backblaze.com/using_b2_code.html +# +###################################################################### + +# ruff: noqa: F405 +from b2._internal._b2v4.registry import * # noqa + +B2.register_subcommand(AuthorizeAccount) +B2.register_subcommand(CancelAllUnfinishedLargeFiles) +B2.register_subcommand(CancelLargeFile) +B2.register_subcommand(ClearAccount) +B2.register_subcommand(CopyFileById) +B2.register_subcommand(CreateBucket) +B2.register_subcommand(CreateKey) +B2.register_subcommand(DeleteBucket) +B2.register_subcommand(DeleteFileVersion) +B2.register_subcommand(DeleteKey) +B2.register_subcommand(DownloadFile) +B2.register_subcommand(DownloadFileById) +B2.register_subcommand(DownloadFileByName) +B2.register_subcommand(Cat) +B2.register_subcommand(GetAccountInfo) +B2.register_subcommand(GetBucket) +B2.register_subcommand(FileInfo) +B2.register_subcommand(GetFileInfo) +B2.register_subcommand(GetDownloadAuth) +B2.register_subcommand(GetDownloadUrlWithAuth) +B2.register_subcommand(HideFile) +B2.register_subcommand(ListBuckets) +B2.register_subcommand(ListKeys) +B2.register_subcommand(ListParts) +B2.register_subcommand(ListUnfinishedLargeFiles) +B2.register_subcommand(Ls) +B2.register_subcommand(Rm) +B2.register_subcommand(GetUrl) +B2.register_subcommand(MakeUrl) +B2.register_subcommand(MakeFriendlyUrl) +B2.register_subcommand(Sync) +B2.register_subcommand(UpdateBucket) +B2.register_subcommand(UploadFile) +B2.register_subcommand(UploadUnboundStream) +B2.register_subcommand(UpdateFileLegalHold) +B2.register_subcommand(UpdateFileRetention) +B2.register_subcommand(ReplicationSetup) +B2.register_subcommand(ReplicationDelete) +B2.register_subcommand(ReplicationPause) +B2.register_subcommand(ReplicationUnpause) +B2.register_subcommand(ReplicationStatus) +B2.register_subcommand(Version) +B2.register_subcommand(License) +B2.register_subcommand(InstallAutocomplete) diff --git a/b2/console_tool.py b/b2/_internal/console_tool.py similarity index 98% rename from b2/console_tool.py rename to b2/_internal/console_tool.py index 2f99f56c7..5667a8c78 100644 --- a/b2/console_tool.py +++ b/b2/_internal/console_tool.py @@ -2,18 +2,19 @@ # PYTHON_ARGCOMPLETE_OK ###################################################################### # -# File: b2/console_tool.py +# File: b2/_internal/console_tool.py # # Copyright 2019 Backblaze Inc. All Rights Reserved. # # License https://www.backblaze.com/using_b2_code.html # ###################################################################### +# ruff: noqa: E402 from __future__ import annotations import tempfile -from b2._cli.autocomplete_cache import AUTOCOMPLETE # noqa +from b2._internal._cli.autocomplete_cache import AUTOCOMPLETE # noqa AUTOCOMPLETE.autocomplete_from_cache() @@ -111,21 +112,21 @@ from class_registry import ClassRegistry from tabulate import tabulate -from b2._cli.arg_parser_types import ( +from b2._internal._cli.arg_parser_types import ( parse_comma_separated_list, parse_default_retention_period, parse_millis_from_float_timestamp, parse_range, ) -from b2._cli.argcompleters import bucket_name_completer, file_name_completer -from b2._cli.autocomplete_install import ( +from b2._internal._cli.argcompleters import bucket_name_completer, file_name_completer +from b2._internal._cli.autocomplete_install import ( SUPPORTED_SHELLS, AutocompleteInstallError, autocomplete_install, ) -from b2._cli.b2api import _get_b2api_for_profile -from b2._cli.b2args import add_b2_file_argument -from b2._cli.const import ( +from b2._internal._cli.b2api import _get_b2api_for_profile +from b2._internal._cli.b2args import add_b2_file_argument +from b2._internal._cli.const import ( B2_APPLICATION_KEY_ENV_VAR, B2_APPLICATION_KEY_ID_ENV_VAR, B2_DESTINATION_SSE_C_KEY_B64_ENV_VAR, @@ -136,12 +137,12 @@ CREATE_BUCKET_TYPES, DEFAULT_THREADS, ) -from b2._cli.obj_loads import validated_loads -from b2._cli.shell import detect_shell -from b2._utils.uri import B2URI, B2FileIdURI, B2URIAdapter, B2URIBase -from b2.arg_parser import B2ArgumentParser -from b2.json_encoder import B2CliJsonEncoder -from b2.version import VERSION +from b2._internal._cli.obj_loads import validated_loads +from b2._internal._cli.shell import detect_shell +from b2._internal._utils.uri import B2URI, B2FileIdURI, B2URIAdapter, B2URIBase +from b2._internal.arg_parser import B2ArgumentParser +from b2._internal.json_encoder import B2CliJsonEncoder +from b2._internal.version import VERSION piplicenses = None prettytable = None @@ -160,7 +161,10 @@ # The name of an executable entry point NAME = os.path.basename(sys.argv[0]) if NAME.endswith('.py'): - NAME = 'b2' + version_name = re.search( + r'[\\/]b2[\\/]_internal[\\/](_{0,1}b2v\d+)[\\/]__main__.py', sys.argv[0] + ) + NAME = version_name.group(1) if version_name else 'b2' FILE_RETENTION_COMPATIBILITY_WARNING = """ .. warning:: @@ -997,7 +1001,6 @@ def _run(self, args): return args.command_class -@B2.register_subcommand class AuthorizeAccount(Command): """ Prompts for Backblaze ``applicationKeyId`` and ``applicationKey`` (unless they are given @@ -1122,7 +1125,6 @@ def _get_user_requested_realm(cls, args) -> str | None: return os.environ.get(B2_ENVIRONMENT_ENV_VAR) -@B2.register_subcommand class CancelAllUnfinishedLargeFiles(Command): """ Lists all large files that have been started but not @@ -1148,7 +1150,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class CancelLargeFile(Command): """ Cancels a large file upload. Used to undo a ``start-large-file``. @@ -1172,7 +1173,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class ClearAccount(Command): """ Erases everything in local cache. @@ -1193,7 +1193,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class CopyFileById( HeaderFlagsMixin, DestinationSseMixin, SourceSseMixin, FileRetentionSettingMixin, LegalHoldMixin, Command @@ -1331,7 +1330,6 @@ def _determine_source_metadata( return source_file_version.file_info, source_file_version.content_type -@B2.register_subcommand class CreateBucket(DefaultSseMixin, LifecycleRulesMixin, Command): """ Creates a new bucket. Prints the ID of the bucket created. @@ -1387,7 +1385,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class CreateKey(Command): """ Creates a new application key. Prints the application key information. This is the only @@ -1448,7 +1445,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class DeleteBucket(Command): """ Deletes the bucket with the given name. @@ -1469,7 +1465,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class DeleteFileVersion(FileIdAndOptionalFileNameMixin, Command): """ Permanently and irrevocably deletes one version of a file. @@ -1501,7 +1496,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class DeleteKey(Command): """ Deletes the specified application key by its ID. @@ -1694,7 +1688,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class DownloadFile(B2URIFileArgMixin, DownloadFileBase): __doc__ = DownloadFileBase.__doc__ @@ -1707,7 +1700,6 @@ def get_b2_uri_from_arg(self, args: argparse.Namespace) -> B2URIBase: return args.B2_URI -@B2.register_subcommand class DownloadFileById(CmdReplacedByMixin, B2URIFileIDArgMixin, DownloadFileBase): __doc__ = DownloadFileBase.__doc__ replaced_by_cmd = DownloadFile @@ -1718,7 +1710,6 @@ def _setup_parser(cls, parser): parser.add_argument('localFileName') -@B2.register_subcommand class DownloadFileByName(CmdReplacedByMixin, B2URIBucketNFilenameArgMixin, DownloadFileBase): __doc__ = DownloadFileBase.__doc__ replaced_by_cmd = DownloadFile @@ -1729,7 +1720,6 @@ def _setup_parser(cls, parser): parser.add_argument('localFileName') -@B2.register_subcommand class Cat(B2URIFileArgMixin, DownloadCommand): """ Download content of a file-like object identified by B2 URI directly to stdout. @@ -1758,7 +1748,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class GetAccountInfo(Command): """ Shows the account ID, key, auth token, URLs, and what capabilities @@ -1787,7 +1776,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class GetBucket(Command): """ Prints all of the information about the bucket, including @@ -1864,18 +1852,15 @@ def _run(self, args): return 0 -@B2.register_subcommand class FileInfo(B2URIFileArgMixin, FileInfoBase): __doc__ = FileInfoBase.__doc__ -@B2.register_subcommand class GetFileInfo(CmdReplacedByMixin, B2URIFileIDArgMixin, FileInfoBase): __doc__ = FileInfoBase.__doc__ replaced_by_cmd = FileInfo -@B2.register_subcommand class GetDownloadAuth(Command): """ Prints an authorization token that is valid only for downloading @@ -1909,7 +1894,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class GetDownloadUrlWithAuth(Command): """ Prints a URL to download the given file. The URL includes an authorization @@ -1946,7 +1930,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class HideFile(Command): """ Uploads a new, hidden, version of the given file. @@ -1969,7 +1952,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class ListBuckets(Command): """ Lists all of the buckets in the current account. @@ -2005,7 +1987,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class ListKeys(Command): """ Lists the application keys for the current account. @@ -2087,7 +2068,6 @@ def timestamp_display(self, timestamp_or_none): return dt.strftime('%Y-%m-%d'), dt.strftime('%H:%M:%S') -@B2.register_subcommand class ListParts(Command): """ Lists all of the parts that have been uploaded for the given @@ -2110,7 +2090,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class ListUnfinishedLargeFiles(Command): """ Lists all of the large files in the bucket that were started, @@ -2193,7 +2172,6 @@ def _get_ls_generator(self, args): raise B2Error(error.args[0]) -@B2.register_subcommand class Ls(AbstractLsCommand): """ Using the file naming convention that ``/`` separates folder @@ -2313,7 +2291,6 @@ def format_ls_entry(self, file_version: FileVersion, replication: bool): return template % tuple(parameters) -@B2.register_subcommand class Rm(ThreadsMixin, AbstractLsCommand): """ Removes a "folder" or a set of files matching a pattern. Use with caution. @@ -2533,24 +2510,20 @@ def _run(self, args): return 0 -@B2.register_subcommand class GetUrl(B2URIFileArgMixin, GetUrlBase): __doc__ = GetUrlBase.__doc__ -@B2.register_subcommand class MakeUrl(CmdReplacedByMixin, B2URIFileIDArgMixin, GetUrlBase): __doc__ = GetUrlBase.__doc__ replaced_by_cmd = GetUrl -@B2.register_subcommand class MakeFriendlyUrl(CmdReplacedByMixin, B2URIBucketNFilenameArgMixin, GetUrlBase): __doc__ = GetUrlBase.__doc__ replaced_by_cmd = GetUrl -@B2.register_subcommand class Sync( ThreadsMixin, DestinationSseMixin, @@ -2896,7 +2869,6 @@ def get_synchronizer_from_args( ) -@B2.register_subcommand class UpdateBucket(DefaultSseMixin, LifecycleRulesMixin, Command): """ Updates the ``bucketType`` of an existing bucket. Prints the ID @@ -3170,7 +3142,6 @@ class NotAnInputStream(Exception): pass -@B2.register_subcommand class UploadFile(UploadFileMixin, UploadModeMixin, Command): """ Uploads one file to the given bucket. @@ -3230,7 +3201,6 @@ def execute_operation(self, local_file, bucket, threads, **kwargs): return file_version -@B2.register_subcommand class UploadUnboundStream(UploadFileMixin, Command): """ Uploads an unbound stream to the given bucket. @@ -3311,7 +3281,6 @@ def execute_operation(self, local_file, bucket, threads, **kwargs): return file_version -@B2.register_subcommand class UpdateFileLegalHold(FileIdAndOptionalFileNameMixin, Command): """ Only works in buckets with fileLockEnabled=true. @@ -3337,7 +3306,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class UpdateFileRetention(FileIdAndOptionalFileNameMixin, Command): """ Only works in buckets with fileLockEnabled=true. Providing a ``retentionMode`` other than ``none`` requires @@ -3396,7 +3364,6 @@ def _run(self, args): return 0 -@B2.register_subcommand class ReplicationSetup(Command): """ Sets up replication between two buckets (potentially from different accounts), creating and replacing keys if necessary. @@ -3518,7 +3485,6 @@ def alter_one_rule(cls, rule: ReplicationRule) -> ReplicationRule | None: pass -@B2.register_subcommand class ReplicationDelete(ReplicationRuleChanger): """ Deletes a replication rule @@ -3535,7 +3501,6 @@ def alter_one_rule(cls, rule: ReplicationRule) -> ReplicationRule | None: return None -@B2.register_subcommand class ReplicationPause(ReplicationRuleChanger): """ Pauses a replication rule @@ -3553,7 +3518,6 @@ def alter_one_rule(cls, rule: ReplicationRule) -> ReplicationRule | None: return rule -@B2.register_subcommand class ReplicationUnpause(ReplicationRuleChanger): """ Unpauses a replication rule @@ -3571,7 +3535,6 @@ def alter_one_rule(cls, rule: ReplicationRule) -> ReplicationRule | None: return rule -@B2.register_subcommand class ReplicationStatus(Command): """ Inspects files in only source or both source and destination buckets @@ -3738,7 +3701,6 @@ def output_csv(self, results: dict[str, list[dict]]) -> None: writer.writerows(rows) -@B2.register_subcommand class Version(Command): """ Prints the version number of this tool. @@ -3759,12 +3721,11 @@ def _run(self, args): return 0 -@B2.register_subcommand class License(Command): # pragma: no cover """ Prints the license of B2 Command line tool and all libraries shipped with it. """ - LICENSE_OUTPUT_FILE = pathlib.Path(__file__).parent / 'licenses_output.txt' + LICENSE_OUTPUT_FILE = pathlib.Path(__file__).parent.parent / 'licenses_output.txt' REQUIRES_AUTH = False IGNORE_MODULES = {'b2', 'distlib', 'patchelf-wrapper', 'platformdirs'} @@ -3851,7 +3812,7 @@ def _put_license_text(self, stream: io.StringIO, with_packages: bool = False): files_table.add_row([file_name, file_content]) stream.write(str(files_table)) stream.write(f'\n\n{NAME} license:\n') - b2_license_file_text = (pathlib.Path(__file__).parent / + b2_license_file_text = (pathlib.Path(__file__).parent.parent / 'LICENSE').read_text(encoding='utf8') stream.write(b2_license_file_text) @@ -3944,7 +3905,6 @@ def _get_single_license(self, module_dict: dict): return license_ -@B2.register_subcommand class InstallAutocomplete(Command): """ Installs autocomplete for supported shells. diff --git a/b2/json_encoder.py b/b2/_internal/json_encoder.py similarity index 96% rename from b2/json_encoder.py rename to b2/_internal/json_encoder.py index 81c182d92..51d3db560 100644 --- a/b2/json_encoder.py +++ b/b2/_internal/json_encoder.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/json_encoder.py +# File: b2/_internal/json_encoder.py # # Copyright 2020 Backblaze Inc. All Rights Reserved. # diff --git a/b2/version.py b/b2/_internal/version.py similarity index 92% rename from b2/version.py rename to b2/_internal/version.py index e88e8a260..627f3220f 100644 --- a/b2/version.py +++ b/b2/_internal/version.py @@ -1,6 +1,6 @@ ###################################################################### # -# File: b2/version.py +# File: b2/_internal/version.py # # Copyright 2019 Backblaze Inc. All Rights Reserved. # diff --git a/b2/_internal/version_listing.py b/b2/_internal/version_listing.py new file mode 100644 index 000000000..779b23d48 --- /dev/null +++ b/b2/_internal/version_listing.py @@ -0,0 +1,32 @@ +###################################################################### +# +# File: b2/_internal/version_listing.py +# +# Copyright 2023 Backblaze Inc. All Rights Reserved. +# +# License https://www.backblaze.com/using_b2_code.html +# +###################################################################### + +import pathlib +import re +from typing import List + +RE_VERSION = re.compile(r'[_]*b2v(\d+)') + + +def get_versions() -> List[str]: + return [path.name for path in sorted(pathlib.Path(__file__).parent.glob('*b2v*'))] + + +def get_int_version(version: str) -> int: + match = RE_VERSION.match(version) + assert match, f'Version {version} does not match pattern {RE_VERSION.pattern}' + return int(match.group(1)) + + +CLI_VERSIONS = get_versions() +UNSTABLE_CLI_VERSION = max(CLI_VERSIONS, key=get_int_version) +LATEST_STABLE_VERSION = max( + [elem for elem in CLI_VERSIONS if not elem.startswith('_')], key=get_int_version +) diff --git a/changelog.d/+apiver_for_cli.added.md b/changelog.d/+apiver_for_cli.added.md new file mode 100644 index 000000000..e93f63655 --- /dev/null +++ b/changelog.d/+apiver_for_cli.added.md @@ -0,0 +1 @@ +Client binary is now handled with ApiVer methodology in mind. `b2` executable points to the latest stable version, while other versions can be called directly. diff --git a/changelog.d/+internal_directory.changed.md b/changelog.d/+internal_directory.changed.md new file mode 100644 index 000000000..760f705d5 --- /dev/null +++ b/changelog.d/+internal_directory.changed.md @@ -0,0 +1 @@ +All the Python libraries were moved to the `_internal` directory to discourage users from importing them. diff --git a/doc/source/conf.py b/doc/source/conf.py index 210bab311..5136a85ba 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -28,16 +28,20 @@ # import datetime +import importlib import os -from os import path import re import sys import textwrap +from os import path sys.path.insert(0, os.path.abspath('../..')) -from b2.console_tool import B2 -from b2.version import VERSION +from b2._internal.version import VERSION +from b2._internal.version_listing import LATEST_STABLE_VERSION + +B2 = importlib.import_module(f'b2._internal.{LATEST_STABLE_VERSION}.registry').B2 + # -- General configuration ------------------------------------------------ diff --git a/doc/source/subcommands/authorize_account.rst b/doc/source/subcommands/authorize_account.rst index 642213bcc..ebf642d6c 100644 --- a/doc/source/subcommands/authorize_account.rst +++ b/doc/source/subcommands/authorize_account.rst @@ -2,7 +2,7 @@ Authorize-account command ************************* .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: authorize-account diff --git a/doc/source/subcommands/cancel_all_unfinished_large_files.rst b/doc/source/subcommands/cancel_all_unfinished_large_files.rst index 362f0d995..b097dd2cd 100644 --- a/doc/source/subcommands/cancel_all_unfinished_large_files.rst +++ b/doc/source/subcommands/cancel_all_unfinished_large_files.rst @@ -2,7 +2,7 @@ Cancel-all-unfinished-large-files command ***************************************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: cancel-all-unfinished-large-files diff --git a/doc/source/subcommands/cancel_large_file.rst b/doc/source/subcommands/cancel_large_file.rst index 7e0581f2d..ec12bc025 100644 --- a/doc/source/subcommands/cancel_large_file.rst +++ b/doc/source/subcommands/cancel_large_file.rst @@ -2,7 +2,7 @@ Cancel-large-file command ************************* .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: cancel-large-file diff --git a/doc/source/subcommands/cat.rst b/doc/source/subcommands/cat.rst index 5f866985f..1712fa6c7 100644 --- a/doc/source/subcommands/cat.rst +++ b/doc/source/subcommands/cat.rst @@ -2,7 +2,7 @@ Cat command **************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: cat diff --git a/doc/source/subcommands/clear_account.rst b/doc/source/subcommands/clear_account.rst index 5ccff8788..f9758b5f0 100644 --- a/doc/source/subcommands/clear_account.rst +++ b/doc/source/subcommands/clear_account.rst @@ -2,7 +2,7 @@ Clear-account command ********************* .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: clear-account diff --git a/doc/source/subcommands/copy_file_by_id.rst b/doc/source/subcommands/copy_file_by_id.rst index d1c16586d..681774feb 100644 --- a/doc/source/subcommands/copy_file_by_id.rst +++ b/doc/source/subcommands/copy_file_by_id.rst @@ -2,7 +2,7 @@ Copy-file-by-id command *********************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: copy-file-by-id diff --git a/doc/source/subcommands/create_bucket.rst b/doc/source/subcommands/create_bucket.rst index 1e04897a3..fc8128fde 100644 --- a/doc/source/subcommands/create_bucket.rst +++ b/doc/source/subcommands/create_bucket.rst @@ -2,7 +2,7 @@ Create-bucket command ********************* .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: create-bucket diff --git a/doc/source/subcommands/create_key.rst b/doc/source/subcommands/create_key.rst index 83fb93031..b0d38ec70 100644 --- a/doc/source/subcommands/create_key.rst +++ b/doc/source/subcommands/create_key.rst @@ -2,7 +2,7 @@ Create-key command ****************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: create-key diff --git a/doc/source/subcommands/delete_bucket.rst b/doc/source/subcommands/delete_bucket.rst index b80a6fa3d..7338d05bb 100644 --- a/doc/source/subcommands/delete_bucket.rst +++ b/doc/source/subcommands/delete_bucket.rst @@ -2,7 +2,7 @@ Delete-bucket command ********************* .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: delete-bucket diff --git a/doc/source/subcommands/delete_file_version.rst b/doc/source/subcommands/delete_file_version.rst index acf0af36c..a8b6ef8bc 100644 --- a/doc/source/subcommands/delete_file_version.rst +++ b/doc/source/subcommands/delete_file_version.rst @@ -2,7 +2,7 @@ Delete-file-version command *************************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: delete-file-version diff --git a/doc/source/subcommands/delete_key.rst b/doc/source/subcommands/delete_key.rst index 99a3f0f12..71d124ffd 100644 --- a/doc/source/subcommands/delete_key.rst +++ b/doc/source/subcommands/delete_key.rst @@ -2,7 +2,7 @@ Delete-key command ****************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: delete-key diff --git a/doc/source/subcommands/download_file.rst b/doc/source/subcommands/download_file.rst index 6c80386b7..656aa87e2 100644 --- a/doc/source/subcommands/download_file.rst +++ b/doc/source/subcommands/download_file.rst @@ -2,7 +2,7 @@ Download-file command *************************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: download-file diff --git a/doc/source/subcommands/download_file_by_id.rst b/doc/source/subcommands/download_file_by_id.rst index 2feff77f9..eaf42f01c 100644 --- a/doc/source/subcommands/download_file_by_id.rst +++ b/doc/source/subcommands/download_file_by_id.rst @@ -2,7 +2,7 @@ Download-file-by-id command *************************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: download-file-by-id diff --git a/doc/source/subcommands/download_file_by_name.rst b/doc/source/subcommands/download_file_by_name.rst index ceeb86c18..4210473d2 100644 --- a/doc/source/subcommands/download_file_by_name.rst +++ b/doc/source/subcommands/download_file_by_name.rst @@ -2,7 +2,7 @@ Download-file-by-name command ***************************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: download-file-by-name diff --git a/doc/source/subcommands/file_info.rst b/doc/source/subcommands/file_info.rst index 863d52265..262f9bebe 100644 --- a/doc/source/subcommands/file_info.rst +++ b/doc/source/subcommands/file_info.rst @@ -2,7 +2,7 @@ File-info command ********************* .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: file-info diff --git a/doc/source/subcommands/get_account_info.rst b/doc/source/subcommands/get_account_info.rst index aad5e2488..fd156793b 100644 --- a/doc/source/subcommands/get_account_info.rst +++ b/doc/source/subcommands/get_account_info.rst @@ -2,7 +2,7 @@ Get-account-info command ************************ .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: get-account-info diff --git a/doc/source/subcommands/get_bucket.rst b/doc/source/subcommands/get_bucket.rst index 3bf4c9709..39930b17b 100644 --- a/doc/source/subcommands/get_bucket.rst +++ b/doc/source/subcommands/get_bucket.rst @@ -2,7 +2,7 @@ Get-bucket command ****************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: get-bucket diff --git a/doc/source/subcommands/get_download_auth.rst b/doc/source/subcommands/get_download_auth.rst index 1c2d214c2..06677d1cf 100644 --- a/doc/source/subcommands/get_download_auth.rst +++ b/doc/source/subcommands/get_download_auth.rst @@ -2,7 +2,7 @@ Get-download-auth command ************************* .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: get-download-auth diff --git a/doc/source/subcommands/get_download_url_with_auth.rst b/doc/source/subcommands/get_download_url_with_auth.rst index 7fef96080..4d0e5632f 100644 --- a/doc/source/subcommands/get_download_url_with_auth.rst +++ b/doc/source/subcommands/get_download_url_with_auth.rst @@ -2,7 +2,7 @@ Get-download-url-with-auth command ********************************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: get-download-url-with-auth diff --git a/doc/source/subcommands/get_file_info.rst b/doc/source/subcommands/get_file_info.rst index 0c192a044..abeb969c5 100644 --- a/doc/source/subcommands/get_file_info.rst +++ b/doc/source/subcommands/get_file_info.rst @@ -2,7 +2,7 @@ Get-file-info command ********************* .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: get-file-info diff --git a/doc/source/subcommands/get_url.rst b/doc/source/subcommands/get_url.rst index 537e4f481..4a71b2d46 100644 --- a/doc/source/subcommands/get_url.rst +++ b/doc/source/subcommands/get_url.rst @@ -2,7 +2,7 @@ Get-url command **************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: get-url diff --git a/doc/source/subcommands/hide_file.rst b/doc/source/subcommands/hide_file.rst index 02fe08647..bf258a2b1 100644 --- a/doc/source/subcommands/hide_file.rst +++ b/doc/source/subcommands/hide_file.rst @@ -2,7 +2,7 @@ Hide-file command ***************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: hide-file diff --git a/doc/source/subcommands/install_autocomplete.rst b/doc/source/subcommands/install_autocomplete.rst index a84e4d1ee..48dac2e0c 100644 --- a/doc/source/subcommands/install_autocomplete.rst +++ b/doc/source/subcommands/install_autocomplete.rst @@ -2,7 +2,7 @@ install-autocomplete command **************************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: install-autocomplete diff --git a/doc/source/subcommands/list_buckets.rst b/doc/source/subcommands/list_buckets.rst index ed63dc004..1ec1118fc 100644 --- a/doc/source/subcommands/list_buckets.rst +++ b/doc/source/subcommands/list_buckets.rst @@ -2,7 +2,7 @@ List-buckets command ******************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: list-buckets diff --git a/doc/source/subcommands/list_keys.rst b/doc/source/subcommands/list_keys.rst index 22a84aced..c6d9026bb 100644 --- a/doc/source/subcommands/list_keys.rst +++ b/doc/source/subcommands/list_keys.rst @@ -2,7 +2,7 @@ List-keys command ***************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: list-keys diff --git a/doc/source/subcommands/list_parts.rst b/doc/source/subcommands/list_parts.rst index 253f5c62e..688bc25e8 100644 --- a/doc/source/subcommands/list_parts.rst +++ b/doc/source/subcommands/list_parts.rst @@ -2,7 +2,7 @@ List-parts command ****************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: list-parts diff --git a/doc/source/subcommands/list_unfinished_large_files.rst b/doc/source/subcommands/list_unfinished_large_files.rst index 9517f31bf..2fd6d700b 100644 --- a/doc/source/subcommands/list_unfinished_large_files.rst +++ b/doc/source/subcommands/list_unfinished_large_files.rst @@ -2,7 +2,7 @@ List-unfinished-large-files command *********************************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: list-unfinished-large-files diff --git a/doc/source/subcommands/ls.rst b/doc/source/subcommands/ls.rst index b8ff08fff..51f11fcb0 100644 --- a/doc/source/subcommands/ls.rst +++ b/doc/source/subcommands/ls.rst @@ -2,7 +2,7 @@ Ls command ********** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: ls diff --git a/doc/source/subcommands/make_friendly_url.rst b/doc/source/subcommands/make_friendly_url.rst index 79da5e527..dc38ffd92 100644 --- a/doc/source/subcommands/make_friendly_url.rst +++ b/doc/source/subcommands/make_friendly_url.rst @@ -2,7 +2,7 @@ Make-friendly-url command ************************* .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: make-friendly-url diff --git a/doc/source/subcommands/make_url.rst b/doc/source/subcommands/make_url.rst index f3edeada6..055d67caf 100644 --- a/doc/source/subcommands/make_url.rst +++ b/doc/source/subcommands/make_url.rst @@ -2,7 +2,7 @@ Make-url command **************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: make-url diff --git a/doc/source/subcommands/replication-setup.rst b/doc/source/subcommands/replication-setup.rst index 77eb06f81..960a0ce1e 100644 --- a/doc/source/subcommands/replication-setup.rst +++ b/doc/source/subcommands/replication-setup.rst @@ -4,7 +4,7 @@ replication-setup command ************************* .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: replication-setup diff --git a/doc/source/subcommands/rm.rst b/doc/source/subcommands/rm.rst index 68ef3516a..39705a850 100644 --- a/doc/source/subcommands/rm.rst +++ b/doc/source/subcommands/rm.rst @@ -2,7 +2,7 @@ Rm command ********** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: rm diff --git a/doc/source/subcommands/sync.rst b/doc/source/subcommands/sync.rst index 6ac09a2ad..61241b06f 100644 --- a/doc/source/subcommands/sync.rst +++ b/doc/source/subcommands/sync.rst @@ -2,7 +2,7 @@ Sync command ************ .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: sync diff --git a/doc/source/subcommands/update_bucket.rst b/doc/source/subcommands/update_bucket.rst index 7624a84d3..a1a726c7f 100644 --- a/doc/source/subcommands/update_bucket.rst +++ b/doc/source/subcommands/update_bucket.rst @@ -2,7 +2,7 @@ Update-bucket command ********************* .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: update-bucket diff --git a/doc/source/subcommands/update_file_legal_hold.rst b/doc/source/subcommands/update_file_legal_hold.rst index ddefbba06..42615aa60 100644 --- a/doc/source/subcommands/update_file_legal_hold.rst +++ b/doc/source/subcommands/update_file_legal_hold.rst @@ -2,7 +2,7 @@ Update-file-legal-hold command ****************************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: update-file-legal-hold diff --git a/doc/source/subcommands/update_file_retention.rst b/doc/source/subcommands/update_file_retention.rst index 51cafcb73..05811bdf2 100644 --- a/doc/source/subcommands/update_file_retention.rst +++ b/doc/source/subcommands/update_file_retention.rst @@ -2,7 +2,7 @@ Update-file-retention command ***************************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: update-file-retention diff --git a/doc/source/subcommands/upload_file.rst b/doc/source/subcommands/upload_file.rst index f7d67cdae..0b3315126 100644 --- a/doc/source/subcommands/upload_file.rst +++ b/doc/source/subcommands/upload_file.rst @@ -2,7 +2,7 @@ Upload-file command ******************* .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: upload-file diff --git a/doc/source/subcommands/version.rst b/doc/source/subcommands/version.rst index 080d7b04a..20a8d5263 100644 --- a/doc/source/subcommands/version.rst +++ b/doc/source/subcommands/version.rst @@ -2,7 +2,7 @@ Version command *************** .. argparse:: - :module: b2.console_tool + :module: b2._internal.console_tool :func: get_parser :prog: b2 :path: version diff --git a/noxfile.py b/noxfile.py index f789fc758..d95fe7bee 100644 --- a/noxfile.py +++ b/noxfile.py @@ -15,7 +15,6 @@ import re import string import subprocess -from glob import glob from typing import List, Set, Tuple import nox @@ -91,6 +90,34 @@ ) +def get_version_key(path: pathlib.Path) -> int: + version_name = path.name + # There is no version 0, thus we can provide it to the element starting with an underscore. + if version_name.startswith('_'): + return 0 + + version_match = re.match(r'[_]*b2v(\d+)', version_name) + assert version_match, f'Version {version_name} does not match pattern B2Cli version pattern.' + version_number = int(version_match.group(1)) + return version_number + + +def get_versions() -> List[str]: + """ + "Almost" a copy of b2/_internalg/version_listing.py:get_versions(), because importing + the file directly seems impossible from the noxfile. + """ + # This sorting ensures that: + # - the first element is the latest unstable version (starts with an underscore) + # - the last element is the latest stable version (highest version number) + return [ + path.name for path in sorted( + (pathlib.Path(__file__).parent / 'b2' / '_internal').glob('*b2v*'), + key=get_version_key, + ) + ] + + @nox.session(venv_backend='none') def install(session): install_myself(session) @@ -178,7 +205,8 @@ def unit(session): """Run unit tests.""" install_myself(session, ['license']) session.run('pip', 'install', *REQUIREMENTS_TEST) - session.run( + + command = [ 'pytest', '-n', 'auto', @@ -188,16 +216,24 @@ def unit(session): '--doctest-modules', *session.posargs, 'test/unit', - ) + ] + + versions = get_versions() + session.run(*command, '--cli', versions[0]) + command.append('--cov-append') if not session.posargs: session.notify('cover') + for cli_version in versions[1:]: + session.run(*command, '--cli', cli_version) + def run_integration_test(session, pytest_posargs): """Run integration tests.""" install_myself(session, ['license']) session.run('pip', 'install', *REQUIREMENTS_TEST) - session.run( + + command = [ 'pytest', 'test/integration', '-s', @@ -208,7 +244,19 @@ def run_integration_test(session, pytest_posargs): '-W', 'ignore::DeprecationWarning:rst2ansi.visitor:', *pytest_posargs, - ) + ] + + # sut can be provided explicitly (like in docker) or like `"--sut=path/b2"`. + provided_sut = any('--sut' in elem for elem in pytest_posargs) + + # If `sut` was provided, we just run this one. + # If not, we're running the test on all known versions. + if provided_sut: + session.run(*command) + else: + versions = get_versions() + for cli_version in versions: + session.run(*command, '--sut', f'python -m b2._internal.{cli_version}') @nox.session(python=PYTHON_VERSIONS) @@ -278,14 +326,32 @@ def bundle(session: nox.Session): install_myself(session, ['license', 'full']) session.run('b2', 'license', '--dump', '--with-packages', **run_kwargs) - session.run('pyinstaller', *session.posargs, 'b2.spec', **run_kwargs) + template_spec = string.Template(pathlib.Path('b2.spec.template').read_text()) + versions = get_versions() - if SYSTEM == 'linux' and not NO_STATICX: - session.run( - 'staticx', '--no-compress', '--strip', '--loglevel', 'INFO', 'dist/b2', - 'dist/b2-static', **run_kwargs - ) - session.run('mv', '-f', 'dist/b2-static', 'dist/b2', external=True, **run_kwargs) + # It is assumed that the last element will be the "latest stable". + for binary_name, version in [('b2', versions[-1])] + list(zip(versions, versions)): + spec = template_spec.safe_substitute({ + 'VERSION': version, + 'NAME': binary_name, + }) + pathlib.Path(f'{binary_name}.spec').write_text(spec) + + session.run('pyinstaller', *session.posargs, f'{binary_name}.spec', **run_kwargs) + + if SYSTEM == 'linux' and not NO_STATICX: + session.run( + 'staticx', '--no-compress', '--strip', '--loglevel', 'INFO', f'dist/{binary_name}', + f'dist/{binary_name}-static', **run_kwargs + ) + session.run( + 'mv', + '-f', + f'dist/{binary_name}-static', + f'dist/{binary_name}', + external=True, + **run_kwargs + ) # Set outputs for GitHub Actions if CI: @@ -293,7 +359,14 @@ def bundle(session: nox.Session): # otherwise glob won't find files on windows in action-gh-release. print('asset_path=dist/*') - executable = str(next(pathlib.Path('dist').glob('*'))) + # Note: this should pick the shortest named executable from the directory. + # But, for yet unknown reason, the `./dist/b2` doesn't play well with `--sut` and the autocomplete. + # For this reason, we're returning here the "latest, stable version" instead. + # This current implementation works fine up until version 10, when it will break. + # By that time, we should have come back to picking the shortest named binary (`b2`) up. + executable = max( + str(path) for path in pathlib.Path('dist').glob('*') if not path.name.startswith('_') + ) print(f'sut_path={executable}') @@ -303,32 +376,33 @@ def sign(session): def sign_windows(cert_file, cert_password): session.run('certutil', '-f', '-p', cert_password, '-importpfx', cert_file, **run_kwargs) - session.run( - WINDOWS_SIGNTOOL_PATH, - 'sign', - '/f', - cert_file, - '/p', - cert_password, - '/tr', - WINDOWS_TIMESTAMP_SERVER, - '/td', - 'sha256', - '/fd', - 'sha256', - 'dist/b2.exe', - external=True, - **run_kwargs - ) - session.run( - WINDOWS_SIGNTOOL_PATH, - 'verify', - '/pa', - '/all', - 'dist/b2.exe', - external=True, - **run_kwargs - ) + for binary_name in ['b2'] + get_versions(): + session.run( + WINDOWS_SIGNTOOL_PATH, + 'sign', + '/f', + cert_file, + '/p', + cert_password, + '/tr', + WINDOWS_TIMESTAMP_SERVER, + '/td', + 'sha256', + '/fd', + 'sha256', + f'dist/{binary_name}.exe', + external=True, + **run_kwargs + ) + session.run( + WINDOWS_SIGNTOOL_PATH, + 'verify', + '/pa', + '/all', + f'dist/{binary_name}.exe', + external=True, + **run_kwargs + ) if SYSTEM == 'windows': try: @@ -343,12 +417,12 @@ def sign_windows(cert_file, cert_password): else: session.error(f'unrecognized platform: {SYSTEM}') - # Append OS name to the binary - asset_old_path = glob('dist/*')[0] - name, ext = os.path.splitext(os.path.basename(asset_old_path)) - asset_path = f'dist/{name}-{SYSTEM}{ext}' - - session.run('mv', '-f', asset_old_path, asset_path, external=True, **run_kwargs) + # Append OS name to all the binaries. + for asset in pathlib.Path('dist').glob('*'): + name = asset.stem + ext = asset.suffix + asset_path = f'dist/{name}-{SYSTEM}{ext}' + session.run('mv', '-f', asset, asset_path, external=True, **run_kwargs) # Set outputs for GitHub Actions if CI: @@ -538,12 +612,23 @@ def run_docker_tests(session, image_tag): run_integration_test( session, [ "--sut", - f"docker run -i -v b2:/root -v /tmp:/tmp:rw " + "docker run -i -v b2:/root -v /tmp:/tmp:rw " f"--env-file ENVFILE {image_tag}", "--env-file-cmd-placeholder", "ENVFILE", ] ) + for binary_name in get_versions(): + run_integration_test( + session, [ + "--sut", + "docker run -i -v b2:/root -v /tmp:/tmp:rw " + f"--entrypoint {binary_name} " + f"--env-file ENVFILE {image_tag}", + "--env-file-cmd-placeholder", + "ENVFILE", + ] + ) @nox.session(python=PYTHON_DEFAULT_VERSION) diff --git a/pyinstaller-hooks/hook-b2.py b/pyinstaller-hooks/hook-b2.py index 7e9e6fafc..110efaa0e 100644 --- a/pyinstaller-hooks/hook-b2.py +++ b/pyinstaller-hooks/hook-b2.py @@ -13,5 +13,21 @@ license_file = Path('b2/licenses_output.txt') assert license_file.exists() datas = [ - (str(license_file), '.'), + # When '.' was provided here, the license file was copied to the root of the executable. + # Before ApiVer, it pasted the file to the `b2/` directory. + # I have no idea why it worked before or how it works now. + # If you mean to debug it in the future, know that `pyinstaller` provides a special + # attribute in the `sys` module whenever it runs. + # + # Example: + # import sys + # if hasattr(sys, '_MEIPASS'): + # self._print(f'{NAME}') + # self._print(f'{sys._MEIPASS}') + # elems = [elem for elem in pathlib.Path(sys._MEIPASS).glob('**/*')] + # self._print(f'{elems}') + # + # If used at the very start of the `_run` of `Licenses` command, it will print + # all the files that were unpacked from the executable. + (str(license_file), 'b2/'), ] diff --git a/pyproject.toml b/pyproject.toml index 412f05368..a9d3061ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,9 @@ license = { file = ["requirements-license.txt"] } Homepage = "https://github.com/Backblaze/B2_Command_Line_Tool" [project.scripts] -b2 = "b2.console_tool:main" +b2 = "b2._internal.b2v3.__main__:main" +b2v3 = "b2._internal.b2v3.__main__:main" +_b2v4 = "b2._internal._b2v4.__main__:main" [tool.ruff] target-version = "py37" # to be replaced by project:requires-python when we will have that section in here diff --git a/setup.py b/setup.py index e022ad179..56e86b43c 100644 --- a/setup.py +++ b/setup.py @@ -134,6 +134,11 @@ def read_requirements(extra=None): # "scripts" keyword. Entry points provide cross-platform support and allow # pip to create the appropriate form of executable for the target platform. entry_points={ - 'console_scripts': ['b2=b2.console_tool:main'], + 'console_scripts': + [ + 'b2=b2._internal.b2v3.__main__:main', + 'b2v3=b2._internal.b2v3.__main__:main', + '_b2v4=b2._internal._b2v4.__main__:main', + ], }, ) diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 000000000..5cfb332a5 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,75 @@ +###################################################################### +# +# File: test/conftest.py +# +# Copyright 2023 Backblaze Inc. All Rights Reserved. +# +# License https://www.backblaze.com/using_b2_code.html +# +###################################################################### + +import sys + +import pytest + + +@pytest.hookimpl +def pytest_configure(config): + config.addinivalue_line( + "markers", + "cli_version(from_version, to_version): run tests only on certain versions", + ) + + +@pytest.fixture(scope='session') +def cli_int_version() -> int: + """ + This should never be called, only provides a placeholder for tests + not belonging to neither units nor integrations. + """ + return -1 + + +@pytest.fixture(autouse=True) +def run_on_cli_version_handler(request, cli_int_version): + """ + Auto-fixture that allows skipping tests based on the CLI version. + + Usage: + @pytest.mark.cli_version(1, 3) + def test_foo(): + # Test is run only for versions 1 and 3 + ... + + @pytest.mark.cli_version(from_version=2, to_version=5) + def test_bar(): + # Test is run only for versions 2, 3, 4 and 5 + ... + + Note that it requires the `cli_int_version` fixture to be defined. + Both unit tests and integration tests handle it a little bit different, thus + two different fixtures are provided. + """ + node = request.node.get_closest_marker('cli_version') + if not node: + return + + if not node.args and not node.kwargs: + return + + assert cli_int_version >= 0, 'cli_int_version fixture is not defined' + + if node.args: + if cli_int_version in node.args: + # Run the test. + return + + if node.kwargs: + from_version = node.kwargs.get('from_version', 0) + to_version = node.kwargs.get('to_version', sys.maxsize) + + if from_version <= cli_int_version <= to_version: + # Run the test. + return + + pytest.skip('Not supported on this CLI version') diff --git a/test/integration/conftest.py b/test/integration/conftest.py index c4b37b060..f54bb78d0 100755 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -12,6 +12,7 @@ import logging import os import pathlib +import re import subprocess import sys import tempfile @@ -21,6 +22,13 @@ import pytest from b2sdk.v2 import B2_ACCOUNT_INFO_ENV_VAR, XDG_CONFIG_HOME_ENV_VAR, Bucket +from b2._internal.version_listing import ( + CLI_VERSIONS, + LATEST_STABLE_VERSION, + UNSTABLE_CLI_VERSION, + get_int_version, +) + from .helpers import NODE_DESCRIPTION, RNG_SEED, Api, CommandLine, bucket_name_part, random_token logger = logging.getLogger(__name__) @@ -54,20 +62,72 @@ def node_stats(summary_notes): @pytest.hookimpl def pytest_addoption(parser): parser.addoption( - '--sut', default='%s -m b2' % sys.executable, help='Path to the System Under Test' + '--sut', + default=f'{sys.executable} -m b2._internal.{UNSTABLE_CLI_VERSION}', + help='Path to the System Under Test', ) parser.addoption( '--env-file-cmd-placeholder', default=None, help=( - 'If specified, all occurrences of this string in `--sut` will be substituted with a' - 'path to a tmp file containing env vars to be used when running commands in tests. Useful' + 'If specified, all occurrences of this string in `--sut` will be substituted with a ' + 'path to a tmp file containing env vars to be used when running commands in tests. Useful ' 'for docker.' ) ) + parser.addoption( + '--as_version', + default=None, + help='Force running tests as a particular version of the CLI, ' + 'useful if version cannot be determined easily from the executable', + ) parser.addoption('--cleanup', action='store_true', help='Perform full cleanup at exit') +def get_raw_cli_int_version(config) -> int | None: + forced_version = config.getoption('--as_version') + if forced_version: + return int(forced_version) + + executable = config.getoption('--sut') + # If the executable contains anything that looks like a proper version, we can try to pick it up. + versions_list = '|'.join(CLI_VERSIONS) + versions_match = re.search(rf'({versions_list})', executable) + if versions_match: + return get_int_version(versions_match.group(1)) + + return None + + +def get_cli_int_version(config) -> int: + return get_raw_cli_int_version(config) or get_int_version(LATEST_STABLE_VERSION) + + +@pytest.hookimpl +def pytest_report_header(config): + cli_version = get_cli_int_version(config) + return f'b2cli version: {cli_version}' + + +@pytest.fixture(scope='session') +def cli_int_version(request) -> int: + return get_cli_int_version(request.config) + + +@pytest.fixture(scope='session') +def cli_version(request) -> str: + # The default stable version could be provided directly as e.g.: b2v3, but also indirectly as b2. + # In case there is no direct version, we return the default binary name instead. + raw_cli_version = get_raw_cli_int_version(request.config) + if raw_cli_version is None: + return 'b2' + + for version in CLI_VERSIONS: + if get_int_version(version) == raw_cli_version: + return version + raise pytest.UsageError(f'Unknown CLI version: {raw_cli_version}') + + @pytest.fixture(scope='session') def application_key() -> str: key = environ.get('B2_TEST_APPLICATION_KEY') diff --git a/test/integration/helpers.py b/test/integration/helpers.py index a8777072e..ca6aee32d 100755 --- a/test/integration/helpers.py +++ b/test/integration/helpers.py @@ -58,7 +58,7 @@ v3BucketIdNotFound, ) -from b2.console_tool import Command, current_time_millis +from b2._internal.console_tool import Command, current_time_millis logger = logging.getLogger(__name__) diff --git a/test/integration/test_autocomplete.py b/test/integration/test_autocomplete.py index 6e5b0bd38..c877943e3 100644 --- a/test/integration/test_autocomplete.py +++ b/test/integration/test_autocomplete.py @@ -34,16 +34,24 @@ def bashrc(homedir): @pytest.fixture(scope="module") -def autocomplete_installed(env, homedir, bashrc): +def cli_command(request) -> str: + return request.config.getoption('--sut') + + +@pytest.fixture(scope="module") +def autocomplete_installed(env, homedir, bashrc, cli_version, cli_command, is_running_on_docker): + if is_running_on_docker: + return + shell = pexpect.spawn( - 'bash -i -c "b2 install-autocomplete"', env=env, logfile=sys.stderr.buffer + f'bash -i -c "{cli_command} install-autocomplete"', env=env, logfile=sys.stderr.buffer ) try: shell.expect_exact('Autocomplete successfully installed for bash', timeout=TIMEOUT) finally: shell.close() shell.wait() - assert (homedir / '.bash_completion.d' / 'b2').is_file() + assert (homedir / '.bash_completion.d' / cli_version).is_file() assert bashrc.read_text().startswith(BASHRC_CONTENT) @@ -56,20 +64,20 @@ def shell(env): @skip_on_windows -def test_autocomplete_b2_commands(autocomplete_installed, is_running_on_docker, shell): +def test_autocomplete_b2_commands(autocomplete_installed, is_running_on_docker, shell, cli_version): if is_running_on_docker: pytest.skip('Not supported on Docker') - shell.send('b2 \t\t') + shell.send(f'{cli_version} \t\t') shell.expect_exact(["authorize-account", "download-file", "get-bucket"], timeout=TIMEOUT) @skip_on_windows def test_autocomplete_b2_only_matching_commands( - autocomplete_installed, is_running_on_docker, shell + autocomplete_installed, is_running_on_docker, shell, cli_version ): if is_running_on_docker: pytest.skip('Not supported on Docker') - shell.send('b2 delete-\t\t') + shell.send(f'{cli_version} delete-\t\t') shell.expect_exact("file", timeout=TIMEOUT) # common part of remaining cmds is autocompleted with pytest.raises(pexpect.exceptions.TIMEOUT): # no other commands are suggested @@ -78,12 +86,18 @@ def test_autocomplete_b2_only_matching_commands( @skip_on_windows def test_autocomplete_b2__download_file__b2uri( - autocomplete_installed, shell, b2_tool, bucket_name, file_name, is_running_on_docker + autocomplete_installed, + shell, + b2_tool, + bucket_name, + file_name, + is_running_on_docker, + cli_version, ): """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 \t\t') + shell.send(f'{cli_version} download_file \t\t') shell.expect_exact("b2://", timeout=TIMEOUT) shell.send('b2://\t\t') shell.expect_exact(bucket_name, timeout=TIMEOUT) diff --git a/test/integration/test_b2_command_line.py b/test/integration/test_b2_command_line.py index 88050ad55..34f74c597 100755 --- a/test/integration/test_b2_command_line.py +++ b/test/integration/test_b2_command_line.py @@ -36,7 +36,7 @@ fix_windows_path_limit, ) -from b2.console_tool import current_time_millis +from b2._internal.console_tool import current_time_millis from ..helpers import skip_on_windows from .helpers import ( @@ -361,7 +361,7 @@ def test_rapid_bucket_operations(b2_tool): b2_tool.should_succeed(['delete-bucket', new_bucket_name]) -def test_account(b2_tool): +def test_account(b2_tool, cli_version): with b2_tool.env_var_test_context: b2_tool.should_succeed(['clear-account']) bad_application_key = random_hex(len(b2_tool.application_key)) @@ -388,7 +388,7 @@ def test_account(b2_tool): b2_tool.should_fail( ['create-bucket', bucket_name, 'allPrivate'], r'ERROR: Missing account data: \'NoneType\' object is not subscriptable (\(key 0\) )? ' - r'Use: b2(\.exe)? authorize-account or provide auth data with "B2_APPLICATION_KEY_ID" and ' + fr'Use: {cli_version}(\.exe)? authorize-account or provide auth data with "B2_APPLICATION_KEY_ID" and ' r'"B2_APPLICATION_KEY" environment variables' ) os.remove(new_creds) @@ -1509,14 +1509,18 @@ def test_sse_c(b2_tool, bucket_name, is_running_on_docker, sample_file, tmp_path "data in bundled and built packages." ) @pytest.mark.parametrize('with_packages', [True, False]) -def test_license(b2_tool, with_packages): +def test_license(b2_tool, with_packages, cli_version): license_text = b2_tool.should_succeed( ['license'] + (['--with-packages'] if with_packages else []) ) if with_packages: + # In the case of e.g.: docker image, it has a license built-in with a `b2`. + # It also is unable to generate this license because it lacks required packages. + # Thus, I'm allowing here for the test of licenses to pass whenever + # the binary is named `b2` or with the proper cli version string (e.g. `_b2v4` or `b2v3`). full_license_re = re.compile( - r'Licenses of all modules used by b2(\.EXE)?, shipped with it in binary form:\r?\n' + fr'Licenses of all modules used by ({cli_version}|b2)(\.EXE)?, shipped with it in binary form:\r?\n' r'\+-*\+-*\+\r?\n' r'\|\s*Module name\s*\|\s*License text\s*\|\r?\n' r'.*' @@ -1531,8 +1535,9 @@ def test_license(b2_tool, with_packages): # 'colorlog', 'virtualenv', 'nox', 'packaging', 'argcomplete', 'filelock' # that sum up to around 50k characters. Tests ran from docker image are unaffected. + # See the explanation above for why both `b2` and `cli_version` are allowed here. license_summary_re = re.compile( - r'Summary of all modules used by b2(\.EXE)?, shipped with it in binary form:\r?\n' + fr'Summary of all modules used by ({cli_version}|b2)(\.EXE)?, shipped with it in binary form:\r?\n' r'\+-*\+-*\+-*\+-*\+-*\+\r?\n' r'\|\s*Module name\s*\|\s*Version\s*\|\s*License\s*\|\s*Author\s*\|\s*URL\s*\|\r?\n' r'.*' @@ -2666,8 +2671,9 @@ def test_upload_unbound_stream__redirect_operator( if is_running_on_docker: pytest.skip('Not supported on Docker') content = request.node.name + command = request.config.getoption('--sut') run = bash_runner( - f'b2 upload-unbound-stream {bucket_name} <(echo -n {content}) {request.node.name}.txt' + f'{command} upload-unbound-stream {bucket_name} <(echo -n {content}) {request.node.name}.txt' ) assert hashlib.sha1(content.encode()).hexdigest() in run.stdout diff --git a/test/unit/_cli/test_autocomplete_cache.py b/test/unit/_cli/test_autocomplete_cache.py index 5fb397233..1e67ed488 100644 --- a/test/unit/_cli/test_autocomplete_cache.py +++ b/test/unit/_cli/test_autocomplete_cache.py @@ -25,10 +25,10 @@ import argcomplete import pytest -import b2._cli.argcompleters -import b2.arg_parser -import b2.console_tool -from b2._cli import autocomplete_cache +import b2._internal._cli.argcompleters +import b2._internal.arg_parser +import b2._internal.console_tool +from b2._internal._cli import autocomplete_cache # We can't use pytest.mark.skipif to skip forked tests because with pytest-forked, # there is an attempt to fork even if the test is marked as skipped. @@ -81,14 +81,14 @@ def runner(command: str): def _get_b2api_for_profile(profile: str): return b2_cli.b2_api - m.setattr('b2._cli.b2api._get_b2api_for_profile', _get_b2api_for_profile) + m.setattr('b2._internal._cli.b2api._get_b2api_for_profile', _get_b2api_for_profile) yield return runner def argcomplete_result(): - parser = b2.console_tool.B2.create_parser() + parser = b2._internal.console_tool.B2.create_parser() exit, output = Exit(), io.StringIO() argcomplete.autocomplete(parser, exit_method=exit, output_stream=output) return exit.code, output.getvalue() @@ -102,7 +102,7 @@ def cached_complete_result(cache: autocomplete_cache.AutocompleteCache): def uncached_complete_result(cache: autocomplete_cache.AutocompleteCache): exit, output = Exit(), io.StringIO() - parser = b2.console_tool.B2.create_parser() + parser = b2._internal.console_tool.B2.create_parser() cache.cache_and_autocomplete( parser, uncached_args={ 'exit_method': exit, diff --git a/test/unit/_cli/test_autocomplete_install.py b/test/unit/_cli/test_autocomplete_install.py index bf76e76ba..f6dfdd64b 100644 --- a/test/unit/_cli/test_autocomplete_install.py +++ b/test/unit/_cli/test_autocomplete_install.py @@ -9,7 +9,7 @@ ###################################################################### import pytest -from b2._cli.autocomplete_install import add_or_update_shell_section +from b2._internal._cli.autocomplete_install import add_or_update_shell_section section = "test_section" managed_by = "pytest" diff --git a/test/unit/_cli/test_shell.py b/test/unit/_cli/test_shell.py index 3271c68bd..3a5199fff 100644 --- a/test/unit/_cli/test_shell.py +++ b/test/unit/_cli/test_shell.py @@ -11,7 +11,7 @@ import os from unittest import mock -from b2._cli import shell +from b2._internal._cli import shell @mock.patch.dict(os.environ, {"SHELL": "/bin/bash"}) diff --git a/test/unit/_utils/test_uri.py b/test/unit/_utils/test_uri.py index ee2834abe..a2443b9cf 100644 --- a/test/unit/_utils/test_uri.py +++ b/test/unit/_utils/test_uri.py @@ -11,7 +11,7 @@ import pytest -from b2._utils.uri import B2URI, B2FileIdURI, parse_uri +from b2._internal._utils.uri import B2URI, B2FileIdURI, parse_uri class TestB2URI: diff --git a/test/unit/conftest.py b/test/unit/conftest.py index e3aa0b673..ce5cfd5a1 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -7,15 +7,57 @@ # License https://www.backblaze.com/using_b2_code.html # ###################################################################### +import importlib import os -from test.unit.helpers import RunOrDieExecutor -from test.unit.test_console_tool import BaseConsoleToolTest from unittest import mock import pytest from b2sdk.raw_api import REALM_URLS -from b2.console_tool import _TqdmCloser +from b2._internal.console_tool import _TqdmCloser +from b2._internal.version_listing import CLI_VERSIONS, UNSTABLE_CLI_VERSION, get_int_version + +from .helpers import RunOrDieExecutor +from .test_console_tool import BaseConsoleToolTest + + +@pytest.hookimpl +def pytest_addoption(parser): + parser.addoption( + '--cli', + default=UNSTABLE_CLI_VERSION, + choices=CLI_VERSIONS, + help='version of the CLI', + ) + + +@pytest.hookimpl +def pytest_report_header(config): + int_version = get_int_version(config.getoption('--cli')) + return f'b2cli version: {int_version}' + + +@pytest.fixture(scope='session') +def cli_version(request) -> str: + return request.config.getoption('--cli') + + +@pytest.fixture(scope='session') +def cli_int_version(cli_version) -> int: + return get_int_version(cli_version) + + +@pytest.fixture(scope='session') +def console_tool_class(cli_version): + # Ensures import of the correct library to handle all the tests. + module = importlib.import_module(f'b2._internal.{cli_version}.registry') + return module.ConsoleTool + + +@pytest.fixture(scope='class') +def unit_test_console_tool_class(request, console_tool_class): + # Ensures that the unittest class uses the correct console tool version. + request.cls.console_tool_class = console_tool_class @pytest.fixture(autouse=True, scope='session') @@ -46,8 +88,13 @@ def run(self, *args, **kwargs): @pytest.fixture -def b2_cli(): +def b2_cli(console_tool_class): cli_tester = ConsoleToolTester() + # Because of the magic the pytest does on importing and collecting fixtures, + # ConsoleToolTester is not injected with the `unit_test_console_tool_class` + # despite having it as a parent. + # Thus, we inject it manually here. + cli_tester.console_tool_class = console_tool_class cli_tester.setUp() yield cli_tester cli_tester.tearDown() @@ -99,4 +146,4 @@ def uploaded_file(b2_cli, bucket_info, local_file): 'fileName': filename, 'fileId': '9999', 'content': local_file.read_text(), - } \ No newline at end of file + } diff --git a/test/unit/console_tool/conftest.py b/test/unit/console_tool/conftest.py index aff147d3c..d0ab76dbc 100644 --- a/test/unit/console_tool/conftest.py +++ b/test/unit/console_tool/conftest.py @@ -12,7 +12,7 @@ import pytest -import b2.console_tool +import b2._internal.console_tool @pytest.fixture @@ -27,7 +27,7 @@ def cwd_path(tmp_path): @pytest.fixture def b2_cli_log_fix(caplog): caplog.set_level(0) # prevent pytest from blocking logs - b2.console_tool.logger.setLevel(0) # reset logger level to default + b2._internal.console_tool.logger.setLevel(0) # reset logger level to default @pytest.fixture @@ -36,4 +36,4 @@ def mock_stdin(monkeypatch): monkeypatch.setattr(sys, 'stdin', os.fdopen(out_)) in_f = open(in_, 'w') yield in_f - in_f.close() \ No newline at end of file + in_f.close() diff --git a/test/unit/console_tool/test_authorize_account.py b/test/unit/console_tool/test_authorize_account.py index 85201b047..1da42af35 100644 --- a/test/unit/console_tool/test_authorize_account.py +++ b/test/unit/console_tool/test_authorize_account.py @@ -11,7 +11,7 @@ import pytest -from b2._cli.const import ( +from b2._internal._cli.const import ( B2_APPLICATION_KEY_ENV_VAR, B2_APPLICATION_KEY_ID_ENV_VAR, B2_ENVIRONMENT_ENV_VAR, diff --git a/test/unit/test_apiver.py b/test/unit/test_apiver.py new file mode 100644 index 000000000..bf1176772 --- /dev/null +++ b/test/unit/test_apiver.py @@ -0,0 +1,67 @@ +###################################################################### +# +# File: test/unit/test_apiver.py +# +# Copyright 2023 Backblaze Inc. All Rights Reserved. +# +# License https://www.backblaze.com/using_b2_code.html +# +###################################################################### +import unittest + +import pytest + + +@pytest.fixture +def inject_cli_int_version(request, cli_int_version): + request.cls.cli_int_version = cli_int_version + + +@pytest.mark.usefixtures('inject_cli_int_version') +class UnitTestClass(unittest.TestCase): + cli_int_version: int + + @pytest.mark.cli_version(to_version=3) + def test_passes_below_and_on_v3(self): + assert self.cli_int_version <= 3 + + @pytest.mark.cli_version(from_version=4) + def test_passes_above_and_on_v4(self): + assert self.cli_int_version >= 4 + + @pytest.mark.cli_version(3) + def test_passes_only_on_v3(self): + assert self.cli_int_version == 3 + + @pytest.mark.cli_version(4) + def test_passes_only_on_v4(self): + assert self.cli_int_version == 4 + + @pytest.mark.cli_version(3, 4) + def test_passes_on_both_v3_and_v4(self): + assert self.cli_int_version in {3, 4} + + +@pytest.mark.cli_version(to_version=3) +def test_passes_below_and_on_v3(cli_int_version): + assert cli_int_version <= 3 + + +@pytest.mark.cli_version(from_version=4) +def test_passes_above_and_on_v4(cli_int_version): + assert cli_int_version >= 4 + + +@pytest.mark.cli_version(3) +def test_passes_only_on_v3(cli_int_version): + assert cli_int_version == 3 + + +@pytest.mark.cli_version(4) +def test_passes_only_on_v4(cli_int_version): + assert cli_int_version == 4 + + +@pytest.mark.cli_version(3, 4) +def test_passes_on_both_v3_and_v4(cli_int_version): + assert cli_int_version in {3, 4} diff --git a/test/unit/test_arg_parser.py b/test/unit/test_arg_parser.py index 53195f16e..669c578fc 100644 --- a/test/unit/test_arg_parser.py +++ b/test/unit/test_arg_parser.py @@ -11,13 +11,13 @@ import argparse import sys -from b2._cli.arg_parser_types import ( +from b2._internal._cli.arg_parser_types import ( parse_comma_separated_list, parse_millis_from_float_timestamp, parse_range, ) -from b2.arg_parser import B2ArgumentParser -from b2.console_tool import B2 +from b2._internal.arg_parser import B2ArgumentParser +from b2._internal.console_tool import B2 from .test_base import TestBase diff --git a/test/unit/test_base.py b/test/unit/test_base.py index 80f712952..f454e2c34 100644 --- a/test/unit/test_base.py +++ b/test/unit/test_base.py @@ -11,9 +11,15 @@ import re import unittest from contextlib import contextmanager +from typing import Type +import pytest + +@pytest.mark.usefixtures('unit_test_console_tool_class') class TestBase(unittest.TestCase): + console_tool_class: Type + @contextmanager def assertRaises(self, exc, msg=None): try: diff --git a/test/unit/test_console_tool.py b/test/unit/test_console_tool.py index fdfc3f4a5..6e2dbbe5a 100644 --- a/test/unit/test_console_tool.py +++ b/test/unit/test_console_tool.py @@ -32,13 +32,13 @@ from b2sdk.v2.exception import Conflict # Any error for testing fast-fail of the rm command. from more_itertools import one -from b2._cli.const import ( +from b2._internal._cli.const import ( B2_APPLICATION_KEY_ENV_VAR, B2_APPLICATION_KEY_ID_ENV_VAR, B2_ENVIRONMENT_ENV_VAR, ) -from b2.console_tool import ConsoleTool, Rm -from b2.version import VERSION +from b2._internal.b2v3.registry import Rm +from b2._internal.version import VERSION from .test_base import TestBase @@ -77,7 +77,8 @@ def _run_command_ignore_output(self, argv): success, but ignoring the stdout. """ stdout, stderr = self._get_stdouterr() - actual_status = ConsoleTool(self.b2_api, stdout, stderr).run_command(['b2'] + argv) + actual_status = self.console_tool_class(self.b2_api, stdout, + stderr).run_command(['b2'] + argv) actual_stderr = self._trim_trailing_spaces(stderr.getvalue()) if actual_stderr != '': @@ -190,7 +191,7 @@ def _run_command( """ expected_stderr = self._normalize_expected_output(expected_stderr, format_vars) stdout, stderr = self._get_stdouterr() - console_tool = ConsoleTool(self.b2_api, stdout, stderr) + console_tool = self.console_tool_class(self.b2_api, stdout, stderr) try: actual_status = console_tool.run_command(['b2'] + argv) except SystemExit as e: @@ -1747,7 +1748,7 @@ def test_get_bucket_with_hidden(self): # Hide some new files. Don't check the results here; it will be clear enough that # something has failed if the output of 'get-bucket' does not match the canon. stdout, stderr = self._get_stdouterr() - console_tool = ConsoleTool(self.b2_api, stdout, stderr) + console_tool = self.console_tool_class(self.b2_api, stdout, stderr) console_tool.run_command(['b2', 'hide-file', 'my-bucket', 'hidden1']) console_tool.run_command(['b2', 'hide-file', 'my-bucket', 'hidden2']) console_tool.run_command(['b2', 'hide-file', 'my-bucket', 'hidden3']) @@ -1808,7 +1809,7 @@ def test_get_bucket_complex(self): # Hide some new files. Don't check the results here; it will be clear enough that # something has failed if the output of 'get-bucket' does not match the canon. stdout, stderr = self._get_stdouterr() - console_tool = ConsoleTool(self.b2_api, stdout, stderr) + console_tool = self.console_tool_class(self.b2_api, stdout, stderr) console_tool.run_command(['b2', 'hide-file', 'my-bucket', '1/hidden1']) console_tool.run_command(['b2', 'hide-file', 'my-bucket', '1/hidden1']) console_tool.run_command(['b2', 'hide-file', 'my-bucket', '1/hidden2']) @@ -2358,7 +2359,7 @@ def test_bad_terminal(self): ] + list(range(25)) ) stderr = mock.MagicMock() - console_tool = ConsoleTool(self.b2_api, stdout, stderr) + console_tool = self.console_tool_class(self.b2_api, stdout, stderr) console_tool.run_command(['b2', 'authorize-account', self.account_id, self.master_key]) def test_passing_api_parameters(self): @@ -2385,7 +2386,7 @@ def test_passing_api_parameters(self): }, ] for command, params in product(commands, parameters): - console_tool = ConsoleTool( + console_tool = self.console_tool_class( None, # do not initialize b2 api to allow passing in additional parameters mock.MagicMock(), mock.MagicMock(), diff --git a/test/unit/test_copy.py b/test/unit/test_copy.py index e06ead89d..2af3744a7 100644 --- a/test/unit/test_copy.py +++ b/test/unit/test_copy.py @@ -19,7 +19,7 @@ EncryptionSetting, ) -from b2.console_tool import CopyFileById +from b2._internal.console_tool import CopyFileById from .test_base import TestBase diff --git a/test/unit/test_represent_file_metadata.py b/test/unit/test_represent_file_metadata.py index 30d3c775e..df78d6179 100644 --- a/test/unit/test_represent_file_metadata.py +++ b/test/unit/test_represent_file_metadata.py @@ -25,7 +25,7 @@ StubAccountInfo, ) -from b2.console_tool import ConsoleTool, DownloadCommand +from b2._internal.console_tool import ConsoleTool, DownloadCommand from .test_base import TestBase