From 9fc6877571f6d3268349f28fcf8033534768e193 Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Mon, 2 Nov 2026 01:29:45 +0000 Subject: [PATCH] fix(ENG-15484): accept --color on click versions before 8.2 click.Choice accepts an Enum only from click 8.2. Older click calls string methods on each choice, so every command, --help included, stopped with TypeError or AttributeError. The choices are now text, and the wrapper converts the chosen one to ColorMode. Fixes CLOUDSMITH-CLI-1W Fixes CLOUDSMITH-CLI-1Y Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + cloudsmith_cli/cli/decorators.py | 6 ++++-- cloudsmith_cli/cli/tests/test_decorators.py | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57ea81a1..b752e6f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - When the API or a proxy answers the SAML IdP request with an HTML page or a JSON value that is not an object, `cloudsmith auth` now shows "Failed to authenticate via SAML!" with the reason. Before this fix, the CLI stopped with a `JSONDecodeError` or an `AttributeError`. The 2FA code exchange and the SAML token refresh now also reject a response that is not a JSON object. The 2FA code exchange also rejects a response without an access token. - List commands that return no results now print `Results: 0 `, not `Results: 1-0 (0) of 0 (page: 1/1, ...)`. When the API response has no pagination headers, list commands print only the result count, not `of None`. - An API request no longer waits without a limit when the API host accepts the connection but does not reply. A request now fails when it cannot connect in 30 seconds, or when it gets no data for 90 seconds. The API CDN waits 60 seconds for the API, so the CLI does not stop a slow request before the CDN does. If a request fails, the CLI retries it up to 6 times by default (`--error-retry-max`). A POST request has only the 30-second connection timeout, because a retry after a read timeout can create a second entry. File uploads and downloads do not change. +- Every command, including `--help`, no longer stops with `TypeError` or `AttributeError` about `ColorMode` when the installed `click` is older than 8.2. The CLI needs `click` 8.2 or later, but the install of another package can replace it with an older `click`. ## [1.30.0] - 2026-10-22 diff --git a/cloudsmith_cli/cli/decorators.py b/cloudsmith_cli/cli/decorators.py index c78cc4e0..4d469b48 100644 --- a/cloudsmith_cli/cli/decorators.py +++ b/cloudsmith_cli/cli/decorators.py @@ -241,7 +241,9 @@ def common_cli_output_options(f): "--color", envvar="CLOUDSMITH_COLOR", default="auto", - type=click.Choice(ColorMode, case_sensitive=False), + type=click.Choice( + [mode.name.lower() for mode in ColorMode], case_sensitive=False + ), help="Control whether ANSI colour output is used: auto, always or never.", ) @click.option( @@ -296,7 +298,7 @@ def wrapper(ctx, *args, **kwargs): "CLOUDSMITH_FORCE_COLOR": kwargs.pop("force_color_env"), "TERM": kwargs.pop("term_env"), }, - kwargs.pop("color"), + ColorMode[kwargs.pop("color").upper()], TTYMode.ENABLED if sys.stdout.isatty() else TTYMode.DISABLED, ) diff --git a/cloudsmith_cli/cli/tests/test_decorators.py b/cloudsmith_cli/cli/tests/test_decorators.py index b069b792..a7ec8360 100644 --- a/cloudsmith_cli/cli/tests/test_decorators.py +++ b/cloudsmith_cli/cli/tests/test_decorators.py @@ -13,6 +13,7 @@ from ..commands.main import main from ..config import Options from ..decorators import ( + common_cli_output_options, common_package_action_options, initialise_api, initialise_session, @@ -363,3 +364,16 @@ def command(opts): assert result.exit_code == 2, result.output assert "Invalid value for '--api-headers'" in result.output + + +def test_color_choices_are_text_for_click_before_8_2(): + @click.command() + @common_cli_output_options + def command(opts): + click.echo(click.get_current_context().color) + + color_option = next(param for param in command.params if param.name == "color") + result = click.testing.CliRunner().invoke(command, ["--color", "NEVER"]) + + assert all(isinstance(choice, str) for choice in color_option.type.choices) + assert result.output == "False\n"