diff --git a/color.py b/color.py index 03fb65537..406863453 100644 --- a/color.py +++ b/color.py @@ -84,29 +84,39 @@ def _Color(fg=None, bg=None, attr=None): DEFAULT = None +# Placholder value that indicates we need to check if the user is in an +# interactive terminal session to determine if we turn on color or not. +_CHECK_CONSOLE = object() + +# https://git-scm.com/docs/git-config#Documentation/git-config.txt-colorui +_CONFIG_TO_COLOR_SETTING = { + "false": False, + "never": False, + "no": False, + "auto": _CHECK_CONSOLE, + "true": _CHECK_CONSOLE, + "yes": _CHECK_CONSOLE, + "always": True, +} + def SetDefaultColoring(state: Optional[str]) -> None: """Set coloring behavior to |state|. This is useful for overriding config options via the command line. """ - if state is None: - # Leave it alone -- return quick! - return global DEFAULT - state = state.lower() - if state in ("auto",): + + if isinstance(state, str): + state = state.lower() + if state in _CONFIG_TO_COLOR_SETTING: DEFAULT = state - elif state in ("always", "yes", "true"): - DEFAULT = "always" - elif state in ("never", "no", "false"): - DEFAULT = "never" class Coloring: def __init__(self, config, section_type): - self._section = "color.%s" % section_type + self._section = f"color.{section_type}" self._config = config self._out = sys.stdout @@ -115,16 +125,12 @@ class Coloring: on = self._config.GetString(self._section) if on is None: on = self._config.GetString("color.ui") + if isinstance(on, str): + on = on.lower() - if on == "auto": - if pager.active or os.isatty(1): - self._on = True - else: - self._on = False - elif on in ("true", "always"): - self._on = True - else: - self._on = False + self._on = _CONFIG_TO_COLOR_SETTING.get(on, _CHECK_CONSOLE) + if self._on is _CHECK_CONSOLE: + self._on = pager.active or os.isatty(1) def redirect(self, out): self._out = out diff --git a/tests/test_color.py b/tests/test_color.py index 8b75d2199..72d74993d 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -14,6 +14,8 @@ """Unittests for the color.py module.""" +from unittest import mock + import pytest import utils_for_test @@ -24,9 +26,14 @@ import git_config @pytest.fixture def coloring() -> color.Coloring: """Create a Coloring object for testing.""" + return _make_coloring("always") + + +def _make_coloring(default_state: str) -> color.Coloring: + """Set the default color mode and return a Coloring using test config.""" config_fixture = utils_for_test.FIXTURES_DIR / "test.gitconfig" config = git_config.GitConfig(config_fixture) - color.SetDefaultColoring("true") + color.SetDefaultColoring(default_state) return color.Coloring(config, "status") @@ -72,3 +79,76 @@ def test_Color_Parse_empty_entry(coloring: color.Coloring) -> None: assert val == "\033[2;34;47m" val = coloring._parse("empty", "green", "white", "bold") assert val == "\033[1;32;47m" + + +class TestSetDefaultColoring: + """Tests for SetDefaultColoring.""" + + def test_none_leaves_default_unchanged(self) -> None: + color.DEFAULT = "auto" + color.SetDefaultColoring(None) + assert color.DEFAULT == "auto" + + @pytest.mark.parametrize( + "value, expected", + ( + # auto/true/yes all store their lowercase form. + ("auto", "auto"), + ("Auto", "auto"), + ("true", "true"), + ("True", "true"), + ("yes", "yes"), + ("Yes", "yes"), + # "always" stores as "always". + ("always", "always"), + ("Always", "always"), + # never/no/false store their lowercase form. + ("never", "never"), + ("no", "no"), + ("false", "false"), + ), + ) + def test_maps_to_expected(self, value: str, expected: str) -> None: + color.SetDefaultColoring(value) + assert color.DEFAULT == expected + + def test_unrecognised_leaves_default_unchanged(self) -> None: + color.DEFAULT = "auto" + color.SetDefaultColoring("garbage") + assert color.DEFAULT == "auto" + + +class TestColoringInit: + """Tests for Coloring.__init__ color mode logic.""" + + @pytest.mark.parametrize( + "state, isatty, pager_active, expected", + ( + # "always" enables color unconditionally. + ("always", False, False, True), + # "never" disables color unconditionally. + ("never", True, True, False), + # auto/true/yes enable color only on a TTY or active pager. + ("auto", True, False, True), + ("auto", False, False, False), + ("auto", False, True, True), + ("true", True, False, True), + ("true", False, False, False), + ("true", False, True, True), + ("yes", True, False, True), + ("yes", False, False, False), + ("yes", False, True, True), + ), + ) + def test_color_mode( + self, + state: str, + isatty: bool, + pager_active: bool, + expected: bool, + ) -> None: + with mock.patch("os.isatty", return_value=isatty), mock.patch( + "pager.active", pager_active + ): + c = _make_coloring(state) + assert c.is_on is expected