Skip to content
Merged
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
38 changes: 33 additions & 5 deletions commit_check/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,45 @@
__version__ (package version)
"""

import os
import sys
from importlib.metadata import version, PackageNotFoundError

# Exit codes used across the package
PASS = 0
FAIL = 1

# ANSI color codes used for CLI output
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RESET_COLOR = "\033[0m"

def supports_color() -> bool:
"""Whether the terminal renders ANSI color.

A piped or redirected stream is read as plain text (a CI log, a file, an
agent harness), where the escape sequences are noise, so this errs towards
saying no when ``stdout`` is not a terminal.

``FORCE_COLOR`` overrides the detection in both directions: ``0`` turns
color off even on a terminal, any other value turns it on even when piped.

An empty ``TERM`` is the same "no terminal type" signal as ``dumb``: the
user has deliberately said there are no terminfo capabilities, so emit
plain text. An *unset* ``TERM`` is different — it just means nobody set
it, and a real terminal is still likely color-capable.
"""
forced = os.environ.get("FORCE_COLOR")
if forced:
return forced != "0"
if not sys.stdout.isatty():
return False
if os.environ.get("TERM") in ("", "dumb"):
return False
return True


# ANSI color codes used for CLI output, empty when stdout cannot render color.
RED = "\033[91m" if supports_color() else ""
GREEN = "\033[92m" if supports_color() else ""
YELLOW = "\033[93m" if supports_color() else ""
RESET_COLOR = "\033[0m" if supports_color() else ""

# Follow conventional commits
DEFAULT_COMMIT_TYPES = [
Expand Down
77 changes: 77 additions & 0 deletions tests/util_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import importlib
import pytest
import subprocess
import commit_check
from commit_check import supports_color
from commit_check.util import (
fetch_remote_ref,
fetch_upstream_ref,
Expand Down Expand Up @@ -753,6 +756,80 @@ def test_blank_line_closes_the_block(self, capfd, mocker):
assert lines[-2].startswith("Docs: ")
assert "" not in lines[:-1]

class TestColor:
"""ANSI color belongs on a terminal, not in piped output.

A CI log or an agent harness reads the escape payload as noise, so the
color codes are dropped when ``stdout`` is not a terminal.
"""

@pytest.mark.benchmark
def test_not_supported_when_piped(self, mocker):
mocker.patch.dict("os.environ", {}, clear=True)
mocker.patch("sys.stdout.isatty", return_value=False)
assert supports_color() is False

@pytest.mark.benchmark
def test_forced_even_when_piped(self, mocker):
mocker.patch.dict("os.environ", {"FORCE_COLOR": "1"}, clear=True)
mocker.patch("sys.stdout.isatty", return_value=False)
assert supports_color() is True

@pytest.mark.benchmark
def test_force_zero_turns_color_off(self, mocker):
"""Setting it to 0 must not read as "set, therefore on"."""
mocker.patch.dict("os.environ", {"FORCE_COLOR": "0"}, clear=True)
mocker.patch("sys.stdout.isatty", return_value=True)
assert supports_color() is False

@pytest.mark.benchmark
def test_force_empty_falls_through_to_detection(self, mocker):
mocker.patch.dict("os.environ", {"FORCE_COLOR": ""}, clear=True)
mocker.patch("sys.stdout.isatty", return_value=False)
assert supports_color() is False

@pytest.mark.benchmark
def test_dumb_term_turns_color_off(self, mocker):
mocker.patch.dict("os.environ", {"TERM": "dumb"}, clear=True)
mocker.patch("sys.stdout.isatty", return_value=True)
assert supports_color() is False

@pytest.mark.benchmark
def test_empty_term_turns_color_off(self, mocker):
"""``TERM=`` is a deliberate "no terminal" signal, like ``dumb``."""
mocker.patch.dict("os.environ", {"TERM": ""}, clear=True)
mocker.patch("sys.stdout.isatty", return_value=True)
assert supports_color() is False

@pytest.mark.benchmark
def test_unset_term_still_allows_color_on_a_tty(self, mocker):
"""An absent TERM is not a refusal — the terminal may still render."""
mocker.patch.dict("os.environ", {}, clear=True)
mocker.patch("sys.stdout.isatty", return_value=True)
assert supports_color() is True

@pytest.mark.benchmark
def test_constants_empty_when_color_off(self, mocker):
"""The constants are pre-emptied when stdout cannot render color."""
mocker.patch.dict("os.environ", {"TERM": "dumb"}, clear=True)
mocker.patch("sys.stdout.isatty", return_value=True)
importlib.reload(commit_check)
assert commit_check.RED == ""
assert commit_check.GREEN == ""
assert commit_check.YELLOW == ""
assert commit_check.RESET_COLOR == ""

@pytest.mark.benchmark
def test_constants_set_when_color_on(self, mocker):
"""FORCE_COLOR=1 keeps the raw escape codes in place."""
mocker.patch.dict("os.environ", {"FORCE_COLOR": "1"}, clear=True)
mocker.patch("sys.stdout.isatty", return_value=False)
importlib.reload(commit_check)
assert commit_check.RED == "\033[91m"
assert commit_check.GREEN == "\033[92m"
assert commit_check.YELLOW == "\033[93m"
assert commit_check.RESET_COLOR == "\033[0m"

class TestPrintSuggestion:
@pytest.mark.benchmark
def test_print_suggestion(self, capfd):
Expand Down