Add option for printing a colored diff (#1266)
This commit is contained in:
parent
1382eabb3f
commit
8d6d92aa5b
@ -3,6 +3,7 @@
|
|||||||
### Unreleased
|
### Unreleased
|
||||||
|
|
||||||
- reindent docstrings when reindenting code around it (#1053)
|
- reindent docstrings when reindenting code around it (#1053)
|
||||||
|
- show colored diffs (#1266)
|
||||||
|
|
||||||
### 19.10b0
|
### 19.10b0
|
||||||
|
|
||||||
|
78
black.py
78
black.py
@ -39,6 +39,7 @@
|
|||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
from typing_extensions import Final
|
from typing_extensions import Final
|
||||||
from mypy_extensions import mypyc_attr
|
from mypy_extensions import mypyc_attr
|
||||||
@ -59,6 +60,9 @@
|
|||||||
|
|
||||||
from _black_version import version as __version__
|
from _black_version import version as __version__
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import colorama # noqa: F401
|
||||||
|
|
||||||
DEFAULT_LINE_LENGTH = 88
|
DEFAULT_LINE_LENGTH = 88
|
||||||
DEFAULT_EXCLUDES = r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist)/" # noqa: B950
|
DEFAULT_EXCLUDES = r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist)/" # noqa: B950
|
||||||
DEFAULT_INCLUDES = r"\.pyi?$"
|
DEFAULT_INCLUDES = r"\.pyi?$"
|
||||||
@ -140,12 +144,18 @@ class WriteBack(Enum):
|
|||||||
YES = 1
|
YES = 1
|
||||||
DIFF = 2
|
DIFF = 2
|
||||||
CHECK = 3
|
CHECK = 3
|
||||||
|
COLOR_DIFF = 4
|
||||||
|
|
||||||
@classmethod
|
@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:
|
if check and not diff:
|
||||||
return cls.CHECK
|
return cls.CHECK
|
||||||
|
|
||||||
|
if diff and color:
|
||||||
|
return cls.COLOR_DIFF
|
||||||
|
|
||||||
return cls.DIFF if diff else cls.YES
|
return cls.DIFF if diff else cls.YES
|
||||||
|
|
||||||
|
|
||||||
@ -380,6 +390,11 @@ def target_version_option_callback(
|
|||||||
is_flag=True,
|
is_flag=True,
|
||||||
help="Don't write the files back, just output a diff for each file on stdout.",
|
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(
|
@click.option(
|
||||||
"--fast/--safe",
|
"--fast/--safe",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
@ -458,6 +473,7 @@ def main(
|
|||||||
target_version: List[TargetVersion],
|
target_version: List[TargetVersion],
|
||||||
check: bool,
|
check: bool,
|
||||||
diff: bool,
|
diff: bool,
|
||||||
|
color: bool,
|
||||||
fast: bool,
|
fast: bool,
|
||||||
pyi: bool,
|
pyi: bool,
|
||||||
py36: bool,
|
py36: bool,
|
||||||
@ -470,7 +486,7 @@ def main(
|
|||||||
config: Optional[str],
|
config: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""The uncompromising code formatter."""
|
"""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 target_version:
|
||||||
if py36:
|
if py36:
|
||||||
err("Cannot use both --target-version and --py36")
|
err("Cannot use both --target-version and --py36")
|
||||||
@ -718,12 +734,15 @@ def format_file_in_place(
|
|||||||
if write_back == WriteBack.YES:
|
if write_back == WriteBack.YES:
|
||||||
with open(src, "w", encoding=encoding, newline=newline) as f:
|
with open(src, "w", encoding=encoding, newline=newline) as f:
|
||||||
f.write(dst_contents)
|
f.write(dst_contents)
|
||||||
elif write_back == WriteBack.DIFF:
|
elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
src_name = f"{src}\t{then} +0000"
|
src_name = f"{src}\t{then} +0000"
|
||||||
dst_name = f"{src}\t{now} +0000"
|
dst_name = f"{src}\t{now} +0000"
|
||||||
diff_contents = diff(src_contents, dst_contents, src_name, dst_name)
|
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():
|
with lock or nullcontext():
|
||||||
f = io.TextIOWrapper(
|
f = io.TextIOWrapper(
|
||||||
sys.stdout.buffer,
|
sys.stdout.buffer,
|
||||||
@ -731,12 +750,57 @@ def format_file_in_place(
|
|||||||
newline=newline,
|
newline=newline,
|
||||||
write_through=True,
|
write_through=True,
|
||||||
)
|
)
|
||||||
|
f = wrap_stream_for_windows(f)
|
||||||
f.write(diff_contents)
|
f.write(diff_contents)
|
||||||
f.detach()
|
f.detach()
|
||||||
|
|
||||||
return True
|
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(
|
def format_stdin_to_stdout(
|
||||||
fast: bool, *, write_back: WriteBack = WriteBack.NO, mode: Mode
|
fast: bool, *, write_back: WriteBack = WriteBack.NO, mode: Mode
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@ -762,11 +826,15 @@ def format_stdin_to_stdout(
|
|||||||
)
|
)
|
||||||
if write_back == WriteBack.YES:
|
if write_back == WriteBack.YES:
|
||||||
f.write(dst)
|
f.write(dst)
|
||||||
elif write_back == WriteBack.DIFF:
|
elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
src_name = f"STDIN\t{then} +0000"
|
src_name = f"STDIN\t{then} +0000"
|
||||||
dst_name = f"STDOUT\t{now} +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()
|
f.detach()
|
||||||
|
|
||||||
|
|
||||||
|
6
setup.py
6
setup.py
@ -77,7 +77,11 @@ def get_long_description() -> str:
|
|||||||
"typing_extensions>=3.7.4",
|
"typing_extensions>=3.7.4",
|
||||||
"mypy_extensions>=0.4.3",
|
"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=[
|
classifiers=[
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
|
@ -264,6 +264,28 @@ def test_piping_diff(self) -> None:
|
|||||||
actual = actual.rstrip() + "\n" # the diff output has a trailing space
|
actual = actual.rstrip() + "\n" # the diff output has a trailing space
|
||||||
self.assertEqual(expected, actual)
|
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)
|
@patch("black.dump_to_file", dump_to_stderr)
|
||||||
def test_function(self) -> None:
|
def test_function(self) -> None:
|
||||||
source, expected = read_data("function")
|
source, expected = read_data("function")
|
||||||
@ -352,6 +374,25 @@ def test_expression_diff(self) -> None:
|
|||||||
)
|
)
|
||||||
self.assertEqual(expected, actual, msg)
|
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)
|
@patch("black.dump_to_file", dump_to_stderr)
|
||||||
def test_fstring(self) -> None:
|
def test_fstring(self) -> None:
|
||||||
source, expected = read_data("fstring")
|
source, expected = read_data("fstring")
|
||||||
|
Loading…
Reference in New Issue
Block a user