Skip to content

Commit

Permalink
Add --color/--no-color and PY_COLORS=
Browse files Browse the repository at this point in the history
The environment variable `PY_COLORS` overrides `color =` from
`pyproject.toml`, and the command line options override both of those.
  • Loading branch information
akaihola committed Apr 5, 2022
1 parent 1e742d7 commit 3d77739
Show file tree
Hide file tree
Showing 12 changed files with 353 additions and 112 deletions.
27 changes: 17 additions & 10 deletions src/darker/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
git_is_repository,
)
from darker.help import ISORT_INSTRUCTION
from darker.highlighting import colorize
from darker.highlighting import colorize, should_use_color
from darker.import_sorting import apply_isort, isort
from darker.linting import run_linters
from darker.utils import GIT_DATEFORMAT, TextDocument, debug_dump, get_common_root
Expand Down Expand Up @@ -251,7 +251,11 @@ def modify_file(path: Path, new_content: TextDocument) -> None:


def print_diff(
path: Path, old: TextDocument, new: TextDocument, root: Path = None
path: Path,
old: TextDocument,
new: TextDocument,
root: Path,
use_color: bool,
) -> None:
"""Print ``black --diff`` style output for the changes
Expand All @@ -263,8 +267,6 @@ def print_diff(
Modification times should be in the format "YYYY-MM-DD HH:MM:SS:mmmmmm +0000"
"""
if root is None:
root = Path.cwd()
relative_path = path.resolve().relative_to(root).as_posix()
diff = "\n".join(
line.rstrip("\n")
Expand All @@ -278,12 +280,12 @@ def print_diff(
n=5, # Black shows 5 lines of context, do the same
)
)
print(colorize(diff, "diff"))
print(colorize(diff, "diff", use_color))


def print_source(new: TextDocument) -> None:
def print_source(new: TextDocument, use_color: bool) -> None:
"""Print the reformatted Python source code"""
if sys.stdout.isatty():
if use_color:
try:
(
highlight,
Expand Down Expand Up @@ -425,6 +427,7 @@ def main(argv: List[str] = None) -> int:
f for f in changed_files_to_process if root / f not in files_to_blacken
}

use_color = should_use_color(config["color"])
formatting_failures_on_modified_lines = False
for path, old, new in sorted(
format_edited_parts(
Expand All @@ -440,13 +443,17 @@ def main(argv: List[str] = None) -> int:
):
formatting_failures_on_modified_lines = True
if output_mode == OutputMode.DIFF:
print_diff(path, old, new, root)
print_diff(path, old, new, root, use_color)
elif output_mode == OutputMode.CONTENT:
print_source(new)
print_source(new, use_color)
if write_modified_files:
modify_file(path, new)
linter_failures_on_modified_lines = run_linters(
args.lint, root, changed_files_to_process, revrange
args.lint,
root,
changed_files_to_process,
revrange,
use_color,
)
return (
1
Expand Down
10 changes: 7 additions & 3 deletions src/darker/command_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
get_effective_config,
get_modified_config,
load_config,
override_color_with_environment,
)
from darker.version import __version__

Expand Down Expand Up @@ -52,6 +53,8 @@ def add_arg(help_text: Optional[str], *name_or_flags: str, **kwargs: Any) -> Non
const=-10,
)
add_arg(hlp.QUIET, "-q", "--quiet", action="log_level", dest="log_level", const=10)
add_arg(hlp.COLOR, "--color", action="store_const", dest="color", const=True)
add_arg(hlp.NO_COLOR, "--no-color", action="store_const", dest="color", const=False)
add_arg(hlp.VERSION, "--version", action="version", version=__version__)
add_arg(
hlp.SKIP_STRING_NORMALIZATION,
Expand Down Expand Up @@ -93,8 +96,8 @@ def parse_command_line(argv: List[str]) -> Tuple[Namespace, DarkerConfig, Darker
"""Return the parsed command line, using defaults from a configuration file
Also return the effective configuration which combines defaults, the configuration
read from ``pyproject.toml`` (or the path given in ``--config``), and command line
arguments.
read from ``pyproject.toml`` (or the path given in ``--config``), environment
variables, and command line arguments.
Finally, also return the set of configuration options which differ from defaults.
Expand All @@ -105,7 +108,8 @@ def parse_command_line(argv: List[str]) -> Tuple[Namespace, DarkerConfig, Darker

# 2. Locate `pyproject.toml` based on those paths, or in the current directory if no
# paths were given. Load Darker configuration from it.
config = load_config(args.src)
pyproject_config = load_config(args.src)
config = override_color_with_environment(pyproject_config)

# 3. Use configuration as defaults for re-parsing command line arguments, and don't
# require file/directory paths if they are specified in configuration.
Expand Down
17 changes: 17 additions & 0 deletions src/darker/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Load and save configuration in TOML format"""

import logging
import os
import sys
from argparse import ArgumentParser, Namespace
from pathlib import Path
Expand Down Expand Up @@ -36,6 +37,7 @@ class DarkerConfig(TypedDict, total=False):
lint: List[str]
config: str
log_level: int
color: bool
skip_string_normalization: bool
skip_magic_trailing_comma: bool
line_length: int
Expand Down Expand Up @@ -97,6 +99,21 @@ def validate_config_output_mode(config: DarkerConfig) -> None:
)


def override_color_with_environment(pyproject_config: DarkerConfig) -> DarkerConfig:
"""Override ``color`` if the ``PY_COLORS`` environment variable is '0' or '1'
:param config: The configuration read from ``pyproject.toml``
:return: The modified configuration
"""
py_colors = os.getenv("PY_COLORS")
if py_colors not in {"0", "1"}:
return pyproject_config
config = pyproject_config.copy()
config["color"] = py_colors == "1"
return config


def load_config(srcs: Iterable[str]) -> DarkerConfig:
"""Find and load Darker configuration from given path or pyproject.toml
Expand Down
8 changes: 8 additions & 0 deletions src/darker/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@

VERBOSE = "Show steps taken and summarize modifications"
QUIET = "Reduce amount of output"
COLOR = (
"Use colors even for non-terminal output. Overrides the environment variable"
" PY_COLORS=0"
)
NO_COLOR = (
"Don't use colors even for terminal output. Overrides the environment variable"
" PY_COLORS=1"
)

VERSION = "Show the version of `darker`"

Expand Down
58 changes: 50 additions & 8 deletions src/darker/highlighting/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,54 @@
"""Highlighting of terminal output"""

try:
import pygments # noqa: F401
except ImportError:
from darker.highlighting import without_pygments
# pylint: disable=import-outside-toplevel,unused-import

colorize = without_pygments.colorize
else:
from darker.highlighting import with_pygments
import sys
from typing import Optional, cast

colorize = with_pygments.colorize

def should_use_color(config_color: Optional[bool]) -> bool:
"""Return ``True`` if configuration and package support allow output highlighting
In ``config_color``, the combination of ``color =`` in ``pyproject.toml``, the
``PY_COLORS`` environment variable, and the ``--color``/``--no-color`` command line
options determine whether the user wants to force enable or disable highlighting.
If highlighting isn't forced either way, it is automatically enabled for terminal
output.
Finally, if ``pygments`` isn't installed, highlighting is disabled.
:param config_color: The configuration as parsed from ``pyproject.toml`` and
overridden using environment variables and/or command line
options
:return: ``True`` if highlighting should be used
"""
if config_color is not None:
use_color = config_color
else:
use_color = sys.stdout.isatty()
if use_color:
try:
import pygments # noqa

return True
except ImportError:
pass
return False


def colorize(output: str, lexer_name: str, use_color: bool) -> str:
"""Return the output highlighted for terminal if Pygments is available"""
if not use_color:
return output
from pygments import highlight
from pygments.formatters.terminal import TerminalFormatter
from pygments.lexers import get_lexer_by_name

lexer = get_lexer_by_name(lexer_name)
highlighted = highlight(output, lexer, TerminalFormatter())
if "\n" not in output:
# see https://github.com/pygments/pygments/issues/1107
highlighted = highlighted.rstrip("\n")
return cast(str, highlighted)
20 changes: 0 additions & 20 deletions src/darker/highlighting/with_pygments.py

This file was deleted.

6 changes: 0 additions & 6 deletions src/darker/highlighting/without_pygments.py

This file was deleted.

9 changes: 5 additions & 4 deletions src/darker/linting.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def _check_linter_output(


def run_linter(
cmdline: str, root: Path, paths: Set[Path], revrange: RevisionRange
cmdline: str, root: Path, paths: Set[Path], revrange: RevisionRange, use_color: bool
) -> int:
"""Run the given linter and print linting errors falling on changed lines
Expand Down Expand Up @@ -141,8 +141,8 @@ def run_linter(
if path_in_repo != prev_path or linter_error_linenum > prev_linenum + 1:
print()
prev_path, prev_linenum = path_in_repo, linter_error_linenum
print(colorize(location, "lint_location"), end=" ")
print(colorize(description, "lint_description"))
print(colorize(location, "lint_location", use_color), end=" ")
print(colorize(description, "lint_description", use_color))
error_count += 1
return error_count

Expand All @@ -152,6 +152,7 @@ def run_linters(
root: Path,
paths: Set[Path],
revrange: RevisionRange,
use_color: bool,
) -> int:
"""Run the given linters on a set of files in the repository
Expand All @@ -170,6 +171,6 @@ def run_linters(
# 12. extract line numbers in each file reported by a linter for changed lines
# 13. print only linter error lines which fall on changed lines
return sum(
run_linter(linter_cmdline, root, paths, revrange)
run_linter(linter_cmdline, root, paths, revrange, use_color)
for linter_cmdline in linter_cmdlines
)
71 changes: 69 additions & 2 deletions src/darker/tests/test_command_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

"""Unit tests for :mod:`darker.command_line` and :mod:`darker.__main__`"""

import os
import re
from argparse import ArgumentError
from importlib import reload
Expand Down Expand Up @@ -241,6 +242,69 @@ def test_parse_command_line_config_src(
expect_config=("log_level", "CRITICAL"),
expect_modified=("log_level", "CRITICAL"),
),
dict(
argv=["."],
environ={},
expect_value=("color", None),
expect_config=("color", None),
expect_modified=("color", ...),
),
dict(
argv=["."],
environ={"PY_COLORS": "0"},
expect_value=("color", False),
expect_config=("color", False),
expect_modified=("color", False),
),
dict(
argv=["."],
environ={"PY_COLORS": "1"},
expect_value=("color", True),
expect_config=("color", True),
expect_modified=("color", True),
),
dict(
argv=["--color", "."],
environ={},
expect_value=("color", True),
expect_config=("color", True),
expect_modified=("color", True),
),
dict(
argv=["--color", "."],
environ={"PY_COLORS": "0"},
expect_value=("color", True),
expect_config=("color", True),
expect_modified=("color", True),
),
dict(
argv=["--color", "."],
environ={"PY_COLORS": "1"},
expect_value=("color", True),
expect_config=("color", True),
expect_modified=("color", True),
),
dict(
argv=["--no-color", "."],
environ={},
expect_value=("color", False),
expect_config=("color", False),
expect_modified=("color", False),
),
dict(
argv=["--no-color", "."],
environ={"PY_COLORS": "0"},
expect_value=("color", False),
expect_config=("color", False),
expect_modified=("color", False),
),
dict(
argv=["--no-color", "."],
environ={"PY_COLORS": "1"},
expect_value=("color", False),
expect_config=("color", False),
expect_modified=("color", False),
),
dict(
argv=["."],
expect_value=("skip_string_normalization", None),
Expand Down Expand Up @@ -303,14 +367,17 @@ def test_parse_command_line_config_src(
expect_config=("src", ["valid/path", "another/valid/path"]),
expect_modified=("src", ["valid/path", "another/valid/path"]),
),
environ={},
)
def test_parse_command_line(
tmp_path, monkeypatch, argv, expect_value, expect_config, expect_modified
tmp_path, monkeypatch, argv, environ, expect_value, expect_config, expect_modified
):
"""``parse_command_line()`` parses options correctly"""
monkeypatch.chdir(tmp_path)
(tmp_path / "dummy.py").touch()
with raises_if_exception(expect_value) as expect_exception:
with patch.dict(os.environ, environ, clear=True), raises_if_exception(
expect_value
) as expect_exception:

args, effective_cfg, modified_cfg = parse_command_line(argv)

Expand Down

0 comments on commit 3d77739

Please sign in to comment.