Skip to content

Commit

Permalink
Add color output
Browse files Browse the repository at this point in the history
  • Loading branch information
dosisod committed Dec 2, 2023
1 parent aab9676 commit 2572ee1
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 6 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,16 @@ This will output a JSON file with the following information:
Larger files naturally take longer to check, but files that take way too long should be
looked into, as an issue might only manifest themselves when a file reaches a certain size.

## Disable Color

Color output is enabled by default in Refurb. To disable it, do one of the following:

* Set the `NO_COLOR` env var.

* Use the `--no-color` flag.

* Set `color = false` in the config file.

## Developing / Contributing

### Setup
Expand Down
51 changes: 47 additions & 4 deletions refurb/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import re
import time
from collections.abc import Sequence
from collections.abc import Callable, Sequence
from contextlib import suppress
from functools import cache, partial
from importlib import metadata
Expand Down Expand Up @@ -257,10 +257,53 @@ def format_as_github_annotation(error: Error | str) -> str:
)


def format_errors(errors: Sequence[Error | str], settings: Settings) -> str:
formatter = format_as_github_annotation if settings.format == "github" else str
ERROR_DIFF_PATTERN = re.compile(r"`([^`]*)`([^`]*)`([^`]*)`")


def format_with_color(error: Error | str) -> str:
if isinstance(error, str):
return error

blue = "\x1b[94m"
yellow = "\x1b[33m"
gray = "\x1b[90m"
green = "\x1b[92m"
red = "\x1b[91m"
reset = "\x1b[0m"

# Add red/green color for diffs, assuming the 2 pairs of backticks are in the form:
# Replace `old` with `new`
if error.msg.count("`") == 4:
parts = [
f"{gray}`{red}\\1{gray}`{reset}",
"\\2",
f"{gray}`{green}\\3{gray}`{reset}",
]

done = "\n".join(formatter(error) for error in errors) # type: ignore
error.msg = ERROR_DIFF_PATTERN.sub("".join(parts), error.msg)

parts = [
f"{blue}{error.filename}{reset}",
f"{gray}:{error.line}:{error.column + 1}{reset}",
" ",
f"{yellow}[{error.prefix}{error.code}]{reset}",
f"{gray}:{reset}",
" ",
error.msg,
]

return "".join(parts)


def format_errors(errors: Sequence[Error | str], settings: Settings) -> str:
if settings.format == "github":
formatter: Callable[[Error | str], str] = format_as_github_annotation
elif settings.color:
formatter = format_with_color
else:
formatter = str

done = "\n".join(formatter(error) for error in errors)

if not settings.quiet and any(isinstance(err, Error) for err in errors):
done += "\n\nRun `refurb --explain ERR` to further explain an error. Use `--quiet` to silence this message"
Expand Down
14 changes: 12 additions & 2 deletions refurb/settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import os
import re
import sys
from dataclasses import dataclass, field, replace
Expand Down Expand Up @@ -43,13 +44,17 @@ class Settings:
sort_by: Literal["filename", "error"] | None = None
verbose: bool = False
timing_stats: Path | None = None
color: bool = True

def __post_init__(self) -> None:
if self.enable_all and self.disable_all:
raise ValueError(
'refurb: "enable all" and "disable all" can\'t be used at the same time' # noqa: E501
)

if os.getenv("NO_COLOR"):
self.color = False

@staticmethod
def merge(old: Settings, new: Settings) -> Settings:
if not old.disable_all and new.disable_all:
Expand Down Expand Up @@ -85,6 +90,7 @@ def merge(old: Settings, new: Settings) -> Settings:
sort_by=new.sort_by or old.sort_by,
verbose=old.verbose or new.verbose,
timing_stats=old.timing_stats or new.timing_stats,
color=old.color and new.color,
)

def get_python_version(self) -> tuple[int, int]:
Expand Down Expand Up @@ -155,8 +161,8 @@ def parse_amendment(amendment: dict[str, Any]) -> set[ErrorClassifier]: # type:


def pop_type(ty: type[T], type_name: str = "") -> Callable[..., T]: # type: ignore[misc]
def inner(config: dict[str, Any], name: str) -> T: # type: ignore[misc]
x = config.pop(name, ty())
def inner(config: dict[str, Any], name: str, *, default: T | None = None) -> T: # type: ignore[misc]
x = config.pop(name, default or ty())

if isinstance(x, ty):
return x
Expand Down Expand Up @@ -188,6 +194,7 @@ def parse_config_file(contents: str) -> Settings:
settings.quiet = pop_bool(config, "quiet")
settings.disable_all = pop_bool(config, "disable_all")
settings.enable_all = pop_bool(config, "enable_all")
settings.color = pop_bool(config, "color", default=True)

enable = pop_list(config, "enable")
disable = pop_list(config, "disable")
Expand Down Expand Up @@ -313,6 +320,9 @@ def get_next_arg(arg: str, args: Iterator[str]) -> str:
elif arg == "--timing-stats":
settings.timing_stats = Path(get_next_arg(arg, iargs))

elif arg == "--no-color":
settings.color = False

elif arg == "--":
settings.mypy_args = list(iargs)

Expand Down
15 changes: 15 additions & 0 deletions test/test_arg_parsing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import os
from pathlib import Path
from unittest.mock import patch

import pytest

Expand Down Expand Up @@ -125,6 +127,7 @@ def test_parse_config_file() -> None:
enable = ["FURB111", "FURB222"]
format = "github"
sort_by = "error"
color = false
"""

config = parse_config_file(contents)
Expand All @@ -135,6 +138,7 @@ def test_parse_config_file() -> None:
enable={ErrorCode(111), ErrorCode(222)},
format="github",
sort_by="error",
color=False,
)


Expand Down Expand Up @@ -666,3 +670,14 @@ def test_parse_timing_stats_flag() -> None:
def test_parse_timing_stats_flag_without_arg_is_an_error() -> None:
with pytest.raises(ValueError, match='refurb: missing argument after "--timing-stats"'):
parse_args(["--timing-stats"])


def test_parse_no_color_flag() -> None:
assert parse_args(["--no-color"]) == Settings(color=False)


def test_no_color_env_var_disables_color() -> None:
with patch.dict(os.environ, {"NO_COLOR": "1"}):
settings = Settings()

assert not settings.color
24 changes: 24 additions & 0 deletions test/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,27 @@ def test_timing_stats_outputs_stats_file() -> None:
return

pytest.fail("Data is not in proper format")


def test_color_is_enabled_by_default():
with patch("builtins.print") as p:
main(["test/data/err_123.py"])

p.assert_called_once()
assert "\x1b" in p.call_args[0][0]


def test_no_color_printed_when_disabled():
with patch("builtins.print") as p:
main(["test/data/err_123.py", "--no-color"])

p.assert_called_once()
assert "\x1b" not in p.call_args[0][0]


def test_error_github_actions_formatting():
with patch("builtins.print") as p:
main(["test/data/err_123.py", "--format", "github"])

p.assert_called_once()
assert "::error" in p.call_args[0][0]

0 comments on commit 2572ee1

Please sign in to comment.