diff options
| -rw-r--r-- | color.py | 44 | ||||
| -rw-r--r-- | tests/test_color.py | 82 |
2 files changed, 106 insertions, 20 deletions
| @@ -84,29 +84,39 @@ def _Color(fg=None, bg=None, attr=None): | |||
| 84 | 84 | ||
| 85 | DEFAULT = None | 85 | DEFAULT = None |
| 86 | 86 | ||
| 87 | # Placholder value that indicates we need to check if the user is in an | ||
| 88 | # interactive terminal session to determine if we turn on color or not. | ||
| 89 | _CHECK_CONSOLE = object() | ||
| 90 | |||
| 91 | # https://git-scm.com/docs/git-config#Documentation/git-config.txt-colorui | ||
| 92 | _CONFIG_TO_COLOR_SETTING = { | ||
| 93 | "false": False, | ||
| 94 | "never": False, | ||
| 95 | "no": False, | ||
| 96 | "auto": _CHECK_CONSOLE, | ||
| 97 | "true": _CHECK_CONSOLE, | ||
| 98 | "yes": _CHECK_CONSOLE, | ||
| 99 | "always": True, | ||
| 100 | } | ||
| 101 | |||
| 87 | 102 | ||
| 88 | def SetDefaultColoring(state: Optional[str]) -> None: | 103 | def SetDefaultColoring(state: Optional[str]) -> None: |
| 89 | """Set coloring behavior to |state|. | 104 | """Set coloring behavior to |state|. |
| 90 | 105 | ||
| 91 | This is useful for overriding config options via the command line. | 106 | This is useful for overriding config options via the command line. |
| 92 | """ | 107 | """ |
| 93 | if state is None: | ||
| 94 | # Leave it alone -- return quick! | ||
| 95 | return | ||
| 96 | 108 | ||
| 97 | global DEFAULT | 109 | global DEFAULT |
| 98 | state = state.lower() | 110 | |
| 99 | if state in ("auto",): | 111 | if isinstance(state, str): |
| 112 | state = state.lower() | ||
| 113 | if state in _CONFIG_TO_COLOR_SETTING: | ||
| 100 | DEFAULT = state | 114 | DEFAULT = state |
| 101 | elif state in ("always", "yes", "true"): | ||
| 102 | DEFAULT = "always" | ||
| 103 | elif state in ("never", "no", "false"): | ||
| 104 | DEFAULT = "never" | ||
| 105 | 115 | ||
| 106 | 116 | ||
| 107 | class Coloring: | 117 | class Coloring: |
| 108 | def __init__(self, config, section_type): | 118 | def __init__(self, config, section_type): |
| 109 | self._section = "color.%s" % section_type | 119 | self._section = f"color.{section_type}" |
| 110 | self._config = config | 120 | self._config = config |
| 111 | self._out = sys.stdout | 121 | self._out = sys.stdout |
| 112 | 122 | ||
| @@ -115,16 +125,12 @@ class Coloring: | |||
| 115 | on = self._config.GetString(self._section) | 125 | on = self._config.GetString(self._section) |
| 116 | if on is None: | 126 | if on is None: |
| 117 | on = self._config.GetString("color.ui") | 127 | on = self._config.GetString("color.ui") |
| 128 | if isinstance(on, str): | ||
| 129 | on = on.lower() | ||
| 118 | 130 | ||
| 119 | if on == "auto": | 131 | self._on = _CONFIG_TO_COLOR_SETTING.get(on, _CHECK_CONSOLE) |
| 120 | if pager.active or os.isatty(1): | 132 | if self._on is _CHECK_CONSOLE: |
| 121 | self._on = True | 133 | self._on = pager.active or os.isatty(1) |
| 122 | else: | ||
| 123 | self._on = False | ||
| 124 | elif on in ("true", "always"): | ||
| 125 | self._on = True | ||
| 126 | else: | ||
| 127 | self._on = False | ||
| 128 | 134 | ||
| 129 | def redirect(self, out): | 135 | def redirect(self, out): |
| 130 | self._out = out | 136 | 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 @@ | |||
| 14 | 14 | ||
| 15 | """Unittests for the color.py module.""" | 15 | """Unittests for the color.py module.""" |
| 16 | 16 | ||
| 17 | from unittest import mock | ||
| 18 | |||
| 17 | import pytest | 19 | import pytest |
| 18 | import utils_for_test | 20 | import utils_for_test |
| 19 | 21 | ||
| @@ -24,9 +26,14 @@ import git_config | |||
| 24 | @pytest.fixture | 26 | @pytest.fixture |
| 25 | def coloring() -> color.Coloring: | 27 | def coloring() -> color.Coloring: |
| 26 | """Create a Coloring object for testing.""" | 28 | """Create a Coloring object for testing.""" |
| 29 | return _make_coloring("always") | ||
| 30 | |||
| 31 | |||
| 32 | def _make_coloring(default_state: str) -> color.Coloring: | ||
| 33 | """Set the default color mode and return a Coloring using test config.""" | ||
| 27 | config_fixture = utils_for_test.FIXTURES_DIR / "test.gitconfig" | 34 | config_fixture = utils_for_test.FIXTURES_DIR / "test.gitconfig" |
| 28 | config = git_config.GitConfig(config_fixture) | 35 | config = git_config.GitConfig(config_fixture) |
| 29 | color.SetDefaultColoring("true") | 36 | color.SetDefaultColoring(default_state) |
| 30 | return color.Coloring(config, "status") | 37 | return color.Coloring(config, "status") |
| 31 | 38 | ||
| 32 | 39 | ||
| @@ -72,3 +79,76 @@ def test_Color_Parse_empty_entry(coloring: color.Coloring) -> None: | |||
| 72 | assert val == "\033[2;34;47m" | 79 | assert val == "\033[2;34;47m" |
| 73 | val = coloring._parse("empty", "green", "white", "bold") | 80 | val = coloring._parse("empty", "green", "white", "bold") |
| 74 | assert val == "\033[1;32;47m" | 81 | assert val == "\033[1;32;47m" |
| 82 | |||
| 83 | |||
| 84 | class TestSetDefaultColoring: | ||
| 85 | """Tests for SetDefaultColoring.""" | ||
| 86 | |||
| 87 | def test_none_leaves_default_unchanged(self) -> None: | ||
| 88 | color.DEFAULT = "auto" | ||
| 89 | color.SetDefaultColoring(None) | ||
| 90 | assert color.DEFAULT == "auto" | ||
| 91 | |||
| 92 | @pytest.mark.parametrize( | ||
| 93 | "value, expected", | ||
| 94 | ( | ||
| 95 | # auto/true/yes all store their lowercase form. | ||
| 96 | ("auto", "auto"), | ||
| 97 | ("Auto", "auto"), | ||
| 98 | ("true", "true"), | ||
| 99 | ("True", "true"), | ||
| 100 | ("yes", "yes"), | ||
| 101 | ("Yes", "yes"), | ||
| 102 | # "always" stores as "always". | ||
| 103 | ("always", "always"), | ||
| 104 | ("Always", "always"), | ||
| 105 | # never/no/false store their lowercase form. | ||
| 106 | ("never", "never"), | ||
| 107 | ("no", "no"), | ||
| 108 | ("false", "false"), | ||
| 109 | ), | ||
| 110 | ) | ||
| 111 | def test_maps_to_expected(self, value: str, expected: str) -> None: | ||
| 112 | color.SetDefaultColoring(value) | ||
| 113 | assert color.DEFAULT == expected | ||
| 114 | |||
| 115 | def test_unrecognised_leaves_default_unchanged(self) -> None: | ||
| 116 | color.DEFAULT = "auto" | ||
| 117 | color.SetDefaultColoring("garbage") | ||
| 118 | assert color.DEFAULT == "auto" | ||
| 119 | |||
| 120 | |||
| 121 | class TestColoringInit: | ||
| 122 | """Tests for Coloring.__init__ color mode logic.""" | ||
| 123 | |||
| 124 | @pytest.mark.parametrize( | ||
| 125 | "state, isatty, pager_active, expected", | ||
| 126 | ( | ||
| 127 | # "always" enables color unconditionally. | ||
| 128 | ("always", False, False, True), | ||
| 129 | # "never" disables color unconditionally. | ||
| 130 | ("never", True, True, False), | ||
| 131 | # auto/true/yes enable color only on a TTY or active pager. | ||
| 132 | ("auto", True, False, True), | ||
| 133 | ("auto", False, False, False), | ||
| 134 | ("auto", False, True, True), | ||
| 135 | ("true", True, False, True), | ||
| 136 | ("true", False, False, False), | ||
| 137 | ("true", False, True, True), | ||
| 138 | ("yes", True, False, True), | ||
| 139 | ("yes", False, False, False), | ||
| 140 | ("yes", False, True, True), | ||
| 141 | ), | ||
| 142 | ) | ||
| 143 | def test_color_mode( | ||
| 144 | self, | ||
| 145 | state: str, | ||
| 146 | isatty: bool, | ||
| 147 | pager_active: bool, | ||
| 148 | expected: bool, | ||
| 149 | ) -> None: | ||
| 150 | with mock.patch("os.isatty", return_value=isatty), mock.patch( | ||
| 151 | "pager.active", pager_active | ||
| 152 | ): | ||
| 153 | c = _make_coloring(state) | ||
| 154 | assert c.is_on is expected | ||
