Skip to content

Commit

Permalink
Add option for printing a colored diff (#1266)
Browse files Browse the repository at this point in the history
  • Loading branch information
dougthor42 authored May 8, 2020
1 parent 1382eab commit 8d6d92a
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### Unreleased

- reindent docstrings when reindenting code around it (#1053)
- show colored diffs (#1266)

### 19.10b0

Expand Down
78 changes: 73 additions & 5 deletions black.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
TypeVar,
Union,
cast,
TYPE_CHECKING,
)
from typing_extensions import Final
from mypy_extensions import mypyc_attr
Expand All @@ -59,6 +60,9 @@

from _black_version import version as __version__

if TYPE_CHECKING:
import colorama # noqa: F401

DEFAULT_LINE_LENGTH = 88
DEFAULT_EXCLUDES = r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist)/" # noqa: B950
DEFAULT_INCLUDES = r"\.pyi?$"
Expand Down Expand Up @@ -140,12 +144,18 @@ class WriteBack(Enum):
YES = 1
DIFF = 2
CHECK = 3
COLOR_DIFF = 4

@classmethod
def from_configuration(cls, *, check: bool, diff: bool) -> "WriteBack":
def from_configuration(
cls, *, check: bool, diff: bool, color: bool = False
) -> "WriteBack":
if check and not diff:
return cls.CHECK

if diff and color:
return cls.COLOR_DIFF

return cls.DIFF if diff else cls.YES


Expand Down Expand Up @@ -380,6 +390,11 @@ def target_version_option_callback(
is_flag=True,
help="Don't write the files back, just output a diff for each file on stdout.",
)
@click.option(
"--color/--no-color",
is_flag=True,
help="Show colored diff. Only applies when `--diff` is given.",
)
@click.option(
"--fast/--safe",
is_flag=True,
Expand Down Expand Up @@ -458,6 +473,7 @@ def main(
target_version: List[TargetVersion],
check: bool,
diff: bool,
color: bool,
fast: bool,
pyi: bool,
py36: bool,
Expand All @@ -470,7 +486,7 @@ def main(
config: Optional[str],
) -> None:
"""The uncompromising code formatter."""
write_back = WriteBack.from_configuration(check=check, diff=diff)
write_back = WriteBack.from_configuration(check=check, diff=diff, color=color)
if target_version:
if py36:
err("Cannot use both --target-version and --py36")
Expand Down Expand Up @@ -718,25 +734,73 @@ def format_file_in_place(
if write_back == WriteBack.YES:
with open(src, "w", encoding=encoding, newline=newline) as f:
f.write(dst_contents)
elif write_back == WriteBack.DIFF:
elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
now = datetime.utcnow()
src_name = f"{src}\t{then} +0000"
dst_name = f"{src}\t{now} +0000"
diff_contents = diff(src_contents, dst_contents, src_name, dst_name)

if write_back == write_back.COLOR_DIFF:
diff_contents = color_diff(diff_contents)

with lock or nullcontext():
f = io.TextIOWrapper(
sys.stdout.buffer,
encoding=encoding,
newline=newline,
write_through=True,
)
f = wrap_stream_for_windows(f)
f.write(diff_contents)
f.detach()

return True


def color_diff(contents: str) -> str:
"""Inject the ANSI color codes to the diff."""
lines = contents.split("\n")
for i, line in enumerate(lines):
if line.startswith("+++") or line.startswith("---"):
line = "\033[1;37m" + line + "\033[0m" # bold white, reset
if line.startswith("@@"):
line = "\033[36m" + line + "\033[0m" # cyan, reset
if line.startswith("+"):
line = "\033[32m" + line + "\033[0m" # green, reset
elif line.startswith("-"):
line = "\033[31m" + line + "\033[0m" # red, reset
lines[i] = line
return "\n".join(lines)


def wrap_stream_for_windows(
f: io.TextIOWrapper,
) -> Union[io.TextIOWrapper, "colorama.AnsiToWin32.AnsiToWin32"]:
"""
Wrap the stream in colorama's wrap_stream so colors are shown on Windows.
If `colorama` is not found, then no change is made. If `colorama` does
exist, then it handles the logic to determine whether or not to change
things.
"""
try:
from colorama import initialise

# We set `strip=False` so that we can don't have to modify
# test_express_diff_with_color.
f = initialise.wrap_stream(
f, convert=None, strip=False, autoreset=False, wrap=True
)

# wrap_stream returns a `colorama.AnsiToWin32.AnsiToWin32` object
# which does not have a `detach()` method. So we fake one.
f.detach = lambda *args, **kwargs: None # type: ignore
except ImportError:
pass

return f


def format_stdin_to_stdout(
fast: bool, *, write_back: WriteBack = WriteBack.NO, mode: Mode
) -> bool:
Expand All @@ -762,11 +826,15 @@ def format_stdin_to_stdout(
)
if write_back == WriteBack.YES:
f.write(dst)
elif write_back == WriteBack.DIFF:
elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
now = datetime.utcnow()
src_name = f"STDIN\t{then} +0000"
dst_name = f"STDOUT\t{now} +0000"
f.write(diff(src, dst, src_name, dst_name))
d = diff(src, dst, src_name, dst_name)
if write_back == WriteBack.COLOR_DIFF:
d = color_diff(d)
f = wrap_stream_for_windows(f)
f.write(d)
f.detach()


Expand Down
6 changes: 5 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ def get_long_description() -> str:
"typing_extensions>=3.7.4",
"mypy_extensions>=0.4.3",
],
extras_require={"d": ["aiohttp>=3.3.2", "aiohttp-cors"]},
extras_require={
"d": ["aiohttp>=3.3.2", "aiohttp-cors"],
"colorama": ["colorama>=0.4.3"],
},
test_suite="tests.test_black",
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: Console",
Expand Down
41 changes: 41 additions & 0 deletions tests/test_black.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,28 @@ def test_piping_diff(self) -> None:
actual = actual.rstrip() + "\n" # the diff output has a trailing space
self.assertEqual(expected, actual)

def test_piping_diff_with_color(self) -> None:
source, _ = read_data("expression.py")
config = THIS_DIR / "data" / "empty_pyproject.toml"
args = [
"-",
"--fast",
f"--line-length={black.DEFAULT_LINE_LENGTH}",
"--diff",
"--color",
f"--config={config}",
]
result = BlackRunner().invoke(
black.main, args, input=BytesIO(source.encode("utf8"))
)
actual = result.output
# Again, the contents are checked in a different test, so only look for colors.
self.assertIn("\033[1;37m", actual)
self.assertIn("\033[36m", actual)
self.assertIn("\033[32m", actual)
self.assertIn("\033[31m", actual)
self.assertIn("\033[0m", actual)

@patch("black.dump_to_file", dump_to_stderr)
def test_function(self) -> None:
source, expected = read_data("function")
Expand Down Expand Up @@ -352,6 +374,25 @@ def test_expression_diff(self) -> None:
)
self.assertEqual(expected, actual, msg)

def test_expression_diff_with_color(self) -> None:
source, _ = read_data("expression.py")
expected, _ = read_data("expression.diff")
tmp_file = Path(black.dump_to_file(source))
try:
result = BlackRunner().invoke(
black.main, ["--diff", "--color", str(tmp_file)]
)
finally:
os.unlink(tmp_file)
actual = result.output
# We check the contents of the diff in `test_expression_diff`. All
# we need to check here is that color codes exist in the result.
self.assertIn("\033[1;37m", actual)
self.assertIn("\033[36m", actual)
self.assertIn("\033[32m", actual)
self.assertIn("\033[31m", actual)
self.assertIn("\033[0m", actual)

@patch("black.dump_to_file", dump_to_stderr)
def test_fstring(self) -> None:
source, expected = read_data("fstring")
Expand Down

0 comments on commit 8d6d92a

Please sign in to comment.