Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cloudsmith_cli/cli/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import functools
import logging
import os
import sys

import click
from click.core import ParameterSource

from cloudsmith_cli.cli import validators
from cloudsmith_cli.core.utils import ColorMode, TTYMode, color_enabled

from ..core.credentials.chain import CredentialProviderChain
from ..core.credentials.models import CredentialContext
Expand Down Expand Up @@ -151,6 +153,11 @@ def wrapper(ctx, *args, **kwargs):
def common_cli_output_options(f):
"""Add common CLI output options to commands."""

@click.option(
"--color",
default="auto",
type=click.Choice(ColorMode, case_sensitive=False),
)
@click.option(
"-d",
"--debug",
Expand Down Expand Up @@ -178,6 +185,13 @@ def common_cli_output_options(f):
def wrapper(ctx, *args, **kwargs):
# pylint: disable=missing-docstring
opts = config.get_or_create_options(ctx)

ctx.color = color_enabled(
dict(os.environ),
kwargs.pop("color"),
TTYMode.ENABLED if sys.stdout.isatty() else TTYMode.DISABLED,
)
Comment on lines +189 to +193

opts.debug = kwargs.pop("debug") or opts.debug
_configure_debug_logging(opts.debug)
opts.output = kwargs.pop("output_format")
Expand Down
123 changes: 123 additions & 0 deletions cloudsmith_cli/core/tests/test_terminal_color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from enum import Flag, auto
from typing import ClassVar

import pytest

from cloudsmith_cli.core.utils import (
ColorMode,
TTYMode,
color_enabled,
is_interactive,
)


class Desired(Flag):
NONE = 0
COLOR = auto()
INTERACTIVE = auto()


class TestTerminalUISuppression:
"""Testing precedence in environment variables for controlling interactivity and colour"""

env_tests: ClassVar[list] = [
pytest.param(
{},
ColorMode.AUTO,
TTYMode.ENABLED,
Desired.COLOR | Desired.INTERACTIVE,
id="default behaviour",
),
pytest.param(
{},
ColorMode.NEVER,
TTYMode.DISABLED,
Desired.NONE,
id="force disable tty and color mode",
),
pytest.param(
{"NO_COLOR": ""},
ColorMode.AUTO,
TTYMode.ENABLED,
Desired.COLOR | Desired.INTERACTIVE,
id="empty no color var",
),
pytest.param(
{"CLOUDSMITH_FORCE_COLOR": "true"},
ColorMode.AUTO,
TTYMode.ENABLED,
Desired.COLOR | Desired.INTERACTIVE,
id="force ANSI output",
),
pytest.param(
{"NO_COLOR": "true", "CLOUDSMITH_FORCE_COLOR": "true"},
ColorMode.AUTO,
TTYMode.ENABLED,
Desired.INTERACTIVE,
id="force ANSI output, respects NO_COLOR",
),
pytest.param(
{"TERM": "dumb"},
ColorMode.AUTO,
TTYMode.ENABLED,
Desired.INTERACTIVE,
id="no color enabled for TERM=dumb",
),
pytest.param(
{"CI": "true"},
ColorMode.AUTO,
TTYMode.ENABLED,
Desired.COLOR,
id="in a CI server, do not use interactive features",
),
pytest.param(
{"CI": "true", "NO_COLOR": "true"},
ColorMode.AUTO,
TTYMode.ENABLED,
Desired.NONE,
id="suppress interactivity and colour",
),
pytest.param(
{
"NO_COLOR": "true",
"TERM": "dumb",
"CLOUDSMITH_FORCE_COLOR": "true",
},
ColorMode.AUTO,
TTYMode.ENABLED,
Desired.INTERACTIVE,
id="test NO_COLOR always respected",
),
pytest.param(
{
"TERM": "dumb",
"CLOUDSMITH_FORCE_COLOR": "true",
},
ColorMode.AUTO,
TTYMode.ENABLED,
Desired.COLOR | Desired.INTERACTIVE,
id="ensure force tty respected over term dumb",
),
]

@pytest.mark.parametrize("env,colorMode,ttyMode,desired", env_tests)
def test_no_color_environment_variables(
self,
env: dict[str, str],
colorMode: ColorMode,
ttyMode: TTYMode,
desired: Desired,
):
want_color = Desired.COLOR in desired
assert color_enabled(env, colorMode, ttyMode) == want_color, (
f"colour suppression check failed for environment: {env} wanted {desired}"
)

@pytest.mark.parametrize("env,colorMode,ttyMode,desired", env_tests)
def test_no_interactive_environment_variables(
self, env, colorMode, ttyMode, desired
):
want_interactive = Desired.INTERACTIVE in desired
assert is_interactive(env, ttyMode) == want_interactive, (
f"interactive suppression check failed for environment {env} wanted {desired}"
)
50 changes: 50 additions & 0 deletions cloudsmith_cli/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import hashlib
import os
from enum import Enum, auto

import click

Expand All @@ -11,6 +12,55 @@ def get_help_website():
return "https://docs.cloudsmith.com/developer-tools/cli"


class ColorMode(Enum):
AUTO = auto()
ALWAYS = auto()
NEVER = auto()


class TTYMode(Enum):
ENABLED = auto()
DISABLED = auto()


def color_enabled(env: dict[str, str], colorMode: ColorMode, ttyMode: TTYMode) -> bool:
"""Suppresses ANSI colour based on environment and CLI input"""
match colorMode:
case ColorMode.ALWAYS:
return True
case ColorMode.NEVER:
return False
case ColorMode.AUTO:
pass

if env.get("NO_COLOR"):
return False
if env.get("CLOUDSMITH_FORCE_COLOR") == "true":
return True
if env.get("TERM") == "dumb":
return False
Comment thread
coillteoir marked this conversation as resolved.

match ttyMode:
case TTYMode.ENABLED:
return True
case TTYMode.DISABLED:
return False
return True


def is_interactive(env: dict[str, str], tty_mode: TTYMode) -> bool:
"""Suppresses interactive prompts for user input based on conditions in the shell environment"""
if env.get("CI"):
return False

match tty_mode:
case TTYMode.ENABLED:
return True
case TTYMode.DISABLED:
return False
return True


def get_github_website():
"""Get the URL for the GitHub project."""
return "https://github.com/cloudsmith-io/cloudsmith-cli"
Expand Down
Loading