|  | 
| 7 | 7 | import pathlib | 
| 8 | 8 | import shutil | 
| 9 | 9 | import sys | 
| 10 |  | -from typing import TYPE_CHECKING, Any | 
|  | 10 | +from typing import TYPE_CHECKING, Any, TextIO | 
| 11 | 11 | 
 | 
| 12 | 12 | import git | 
| 13 | 13 | from tox.plugin import impl | 
|  | 
| 33 | 33 |     "::error title=tox-extra detected git dirty status:: " + WARNING_MSG_GIT_DIRTY | 
| 34 | 34 | ) | 
| 35 | 35 | 
 | 
| 36 |  | -# Change the color of stderr from default red to a dimmed grey | 
| 37 |  | -if "TOX_STDERR_COLOR" not in os.environ: | 
| 38 |  | -    os.environ["TOX_STDERR_COLOR"] = "LIGHTBLACK_EX" | 
|  | 36 | + | 
|  | 37 | +# Based on Ansible implementation | 
|  | 38 | +def to_bool(value: str | bool | None) -> bool:  # pragma: no cover  # noqa: FBT001 | 
|  | 39 | +    """Return a bool for the arg.""" | 
|  | 40 | +    if value is None or isinstance(value, bool): | 
|  | 41 | +        return bool(value) | 
|  | 42 | +    if isinstance(value, str): | 
|  | 43 | +        value = value.lower() | 
|  | 44 | +    return value in ("yes", "on", "1", "true", 1) | 
|  | 45 | + | 
|  | 46 | + | 
|  | 47 | +def should_do_markup(stream: TextIO = sys.stdout) -> bool:  # pragma: no cover | 
|  | 48 | +    """Decide about use of ANSI colors.""" | 
|  | 49 | +    py_colors = None | 
|  | 50 | + | 
|  | 51 | +    # https://xkcd.com/927/ | 
|  | 52 | +    for env_var in [ | 
|  | 53 | +        "PY_COLORS", | 
|  | 54 | +        "CLICOLOR", | 
|  | 55 | +        "FORCE_COLOR", | 
|  | 56 | +        "TOX_COLORED", | 
|  | 57 | +        "GITHUB_ACTIONS",  # they support ANSI | 
|  | 58 | +    ]: | 
|  | 59 | +        value = os.environ.get(env_var, None) | 
|  | 60 | +        if value is not None: | 
|  | 61 | +            py_colors = to_bool(value) | 
|  | 62 | +            break | 
|  | 63 | + | 
|  | 64 | +    # If deliberately disabled colors | 
|  | 65 | +    if os.environ.get("NO_COLOR", None): | 
|  | 66 | +        return False | 
|  | 67 | + | 
|  | 68 | +    # User configuration requested colors | 
|  | 69 | +    if py_colors is not None: | 
|  | 70 | +        return to_bool(py_colors) | 
|  | 71 | + | 
|  | 72 | +    term = os.environ.get("TERM", "") | 
|  | 73 | +    if "xterm" in term: | 
|  | 74 | +        return True | 
|  | 75 | + | 
|  | 76 | +    if term == "dumb": | 
|  | 77 | +        return False | 
|  | 78 | + | 
|  | 79 | +    # Use tty detection logic as last resort because there are numerous | 
|  | 80 | +    # factors that can make isatty return a misleading value, including: | 
|  | 81 | +    # - stdin.isatty() is the only one returning true, even on a real terminal | 
|  | 82 | +    # - stderr returning false if user uses a error stream coloring solution | 
|  | 83 | +    return stream.isatty() | 
| 39 | 84 | 
 | 
| 40 | 85 | 
 | 
| 41 | 86 | def is_git_dirty(path: str) -> bool: | 
| @@ -109,3 +154,21 @@ def tox_after_run_commands( | 
| 109 | 154 |         if os.environ.get("CI") == "true": | 
| 110 | 155 |             raise Fail(ERROR_MSG_GIT_DIRTY) | 
| 111 | 156 |         logger.warning(WARNING_MSG_GIT_DIRTY) | 
|  | 157 | + | 
|  | 158 | + | 
|  | 159 | +if should_do_markup(): | 
|  | 160 | +    # Workaround for tools that do not naturally detect colors in CI system | 
|  | 161 | +    # like Github Actions. Still, when already defined we will not add them. | 
|  | 162 | +    overrides = { | 
|  | 163 | +        "ANSIBLE_FORCE_COLOR": "1", | 
|  | 164 | +        "COLOR": "yes", | 
|  | 165 | +        "FORCE_COLOR": "1", | 
|  | 166 | +        "MYPY_FORCE_COLOR": "1", | 
|  | 167 | +        "PRE_COMMIT_COLOR": "always", | 
|  | 168 | +        "PY_COLORS": "1", | 
|  | 169 | +        "TOX_COLORED": "yes", | 
|  | 170 | +        "TOX_STDERR_COLOR": "LIGHTBLACK_EX",  # stderr in grey instead of red | 
|  | 171 | +    } | 
|  | 172 | +    for k, v in overrides.items(): | 
|  | 173 | +        if k not in os.environ: | 
|  | 174 | +            os.environ[k] = v | 
0 commit comments