from contextlib import contextmanager
from dataclasses import replace
import inspect
-from io import BytesIO, TextIOWrapper
+import io
+from io import BytesIO
import os
from pathlib import Path
from platform import system
import types
from typing import (
Any,
- BinaryIO,
Callable,
Dict,
- Generator,
List,
Iterator,
TypeVar,
)
+import pytest
import unittest
from unittest.mock import patch, MagicMock
+from parameterized import parameterized
import click
from click import unstyle
import black
from black import Feature, TargetVersion
+from black.cache import get_cache_file
+from black.debug import DebugVisitor
+from black.output import diff, color_diff
+from black.report import Report
+import black.files
from pathspec import PathSpec
# Import other test classes
from tests.util import (
THIS_DIR,
+ change_directory,
read_data,
DETERMINISTIC_HEADER,
BlackBaseTestCase,
ff,
dump_to_stderr,
)
-from .test_primer import PrimerCLITests # noqa: F401
THIS_FILE = Path(__file__)
T = TypeVar("T")
R = TypeVar("R")
+# Match the time output in a diff, but nothing else
+DIFF_TIME = re.compile(r"\t[\d-:+\. ]+")
+
@contextmanager
def cache_dir(exists: bool = True) -> Iterator[Path]:
cache_dir = Path(workspace)
if not exists:
cache_dir = cache_dir / "new"
- with patch("black.CACHE_DIR", cache_dir):
+ with patch("black.cache.CACHE_DIR", cache_dir):
yield cache_dir
class BlackRunner(CliRunner):
- """Modify CliRunner so that stderr is not merged with stdout.
-
- This is a hack that can be removed once we depend on Click 7.x"""
+ """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
def __init__(self) -> None:
- self.stderrbuf = BytesIO()
- self.stdoutbuf = BytesIO()
- self.stdout_bytes = b""
- self.stderr_bytes = b""
- super().__init__()
-
- @contextmanager
- def isolation(self, *args: Any, **kwargs: Any) -> Generator[BinaryIO, None, None]:
- with super().isolation(*args, **kwargs) as output:
- try:
- hold_stderr = sys.stderr
- sys.stderr = TextIOWrapper(self.stderrbuf, encoding=self.charset)
- yield output
- finally:
- self.stdout_bytes = sys.stdout.buffer.getvalue() # type: ignore
- self.stderr_bytes = sys.stderr.buffer.getvalue() # type: ignore
- sys.stderr = hold_stderr
+ super().__init__(mix_stderr=False)
class BlackTestCase(BlackBaseTestCase):
if ignore_config:
args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
result = runner.invoke(black.main, args)
+ assert result.stdout_bytes is not None
+ assert result.stderr_bytes is not None
self.assertEqual(
result.exit_code,
exit_code,
msg=(
f"Failed with args: {args}\n"
- f"stdout: {runner.stdout_bytes.decode()!r}\n"
- f"stderr: {runner.stderr_bytes.decode()!r}\n"
+ f"stdout: {result.stdout_bytes.decode()!r}\n"
+ f"stderr: {result.stderr_bytes.decode()!r}\n"
f"exception: {result.exception}"
),
)
)
self.assertEqual(result.exit_code, 0)
self.assertFormatEqual(expected, result.output)
- black.assert_equivalent(source, result.output)
- black.assert_stable(source, result.output, DEFAULT_MODE)
+ if source != result.output:
+ black.assert_equivalent(source, result.output)
+ black.assert_stable(source, result.output, DEFAULT_MODE)
def test_piping_diff(self) -> None:
diff_header = re.compile(
actual = fs(source)
black.assert_stable(source, actual, DEFAULT_MODE)
+ @patch("black.dump_to_file", dump_to_stderr)
+ def test_trailing_comma_optional_parens_stability1_pass2(self) -> None:
+ source, _expected = read_data("trailing_comma_optional_parens1")
+ actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
+ black.assert_stable(source, actual, DEFAULT_MODE)
+
+ @patch("black.dump_to_file", dump_to_stderr)
+ def test_trailing_comma_optional_parens_stability2_pass2(self) -> None:
+ source, _expected = read_data("trailing_comma_optional_parens2")
+ actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
+ black.assert_stable(source, actual, DEFAULT_MODE)
+
+ @patch("black.dump_to_file", dump_to_stderr)
+ def test_trailing_comma_optional_parens_stability3_pass2(self) -> None:
+ source, _expected = read_data("trailing_comma_optional_parens3")
+ actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
+ black.assert_stable(source, actual, DEFAULT_MODE)
+
@patch("black.dump_to_file", dump_to_stderr)
def test_pep_572(self) -> None:
source, expected = read_data("pep_572")
if sys.version_info >= (3, 8):
black.assert_equivalent(source, actual)
+ @patch("black.dump_to_file", dump_to_stderr)
+ def test_pep_572_remove_parens(self) -> None:
+ source, expected = read_data("pep_572_remove_parens")
+ actual = fs(source)
+ self.assertFormatEqual(expected, actual)
+ black.assert_stable(source, actual, DEFAULT_MODE)
+ if sys.version_info >= (3, 8):
+ black.assert_equivalent(source, actual)
+
+ @patch("black.dump_to_file", dump_to_stderr)
+ def test_pep_572_do_not_remove_parens(self) -> None:
+ source, expected = read_data("pep_572_do_not_remove_parens")
+ # the AST safety checks will fail, but that's expected, just make sure no
+ # parentheses are touched
+ actual = black.format_str(source, mode=DEFAULT_MODE)
+ self.assertFormatEqual(expected, actual)
+
def test_pep_572_version_detection(self) -> None:
source, _ = read_data("pep_572")
root = black.lib2to3_parse(source)
versions = black.detect_target_versions(root)
self.assertIn(black.TargetVersion.PY38, versions)
+ @parameterized.expand([(3, 9), (3, 10)])
+ def test_pep_572_newer_syntax(self, major: int, minor: int) -> None:
+ source, expected = read_data(f"pep_572_py{major}{minor}")
+ actual = fs(source, mode=DEFAULT_MODE)
+ self.assertFormatEqual(expected, actual)
+ if sys.version_info >= (major, minor):
+ black.assert_equivalent(source, actual)
+
def test_expression_ff(self) -> None:
source, expected = read_data("expression")
tmp_file = Path(black.dump_to_file(source))
def test_expression_diff(self) -> None:
source, _ = read_data("expression.py")
+ config = THIS_DIR / "data" / "empty_pyproject.toml"
expected, _ = read_data("expression.diff")
tmp_file = Path(black.dump_to_file(source))
diff_header = re.compile(
r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
)
try:
- result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
+ result = BlackRunner().invoke(
+ black.main, ["--diff", str(tmp_file), f"--config={config}"]
+ )
self.assertEqual(result.exit_code, 0)
finally:
os.unlink(tmp_file)
actual = result.output
actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
- actual = actual.rstrip() + "\n" # the diff output has a trailing space
if expected != actual:
dump = black.dump_to_file(actual)
msg = (
def test_expression_diff_with_color(self) -> None:
source, _ = read_data("expression.py")
+ config = THIS_DIR / "data" / "empty_pyproject.toml"
expected, _ = read_data("expression.diff")
tmp_file = Path(black.dump_to_file(source))
try:
result = BlackRunner().invoke(
- black.main, ["--diff", "--color", str(tmp_file)]
+ black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
)
finally:
os.unlink(tmp_file)
@patch("black.dump_to_file", dump_to_stderr)
def test_string_quotes(self) -> None:
source, expected = read_data("string_quotes")
- actual = fs(source)
+ mode = black.Mode(experimental_string_processing=True)
+ actual = fs(source, mode=mode)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(source, actual)
- black.assert_stable(source, actual, DEFAULT_MODE)
- mode = replace(DEFAULT_MODE, string_normalization=False)
+ black.assert_stable(source, actual, mode)
+ mode = replace(mode, string_normalization=False)
not_normalized = fs(source, mode=mode)
self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
black.assert_equivalent(source, not_normalized)
black.assert_equivalent(source, actual)
black.assert_stable(source, actual, mode)
+ def test_skip_magic_trailing_comma(self) -> None:
+ source, _ = read_data("expression.py")
+ expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
+ tmp_file = Path(black.dump_to_file(source))
+ diff_header = re.compile(
+ rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
+ r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
+ )
+ try:
+ result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
+ self.assertEqual(result.exit_code, 0)
+ finally:
+ os.unlink(tmp_file)
+ actual = result.output
+ actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
+ actual = actual.rstrip() + "\n" # the diff output has a trailing space
+ if expected != actual:
+ dump = black.dump_to_file(actual)
+ msg = (
+ "Expected diff isn't equal to the actual. If you made changes to"
+ " expression.py and this is an anticipated difference, overwrite"
+ f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
+ )
+ self.assertEqual(expected, actual, msg)
+
+ @pytest.mark.python2
@patch("black.dump_to_file", dump_to_stderr)
def test_python2_print_function(self) -> None:
source, expected = read_data("python2_print_function")
self.assertFormatEqual(contents_spc, fs(contents_tab))
def test_report_verbose(self) -> None:
- report = black.Report(verbose=True)
+ report = Report(verbose=True)
out_lines = []
err_lines = []
def err(msg: str, **kwargs: Any) -> None:
err_lines.append(msg)
- with patch("black.out", out), patch("black.err", err):
+ with patch("black.output._out", out), patch("black.output._err", err):
report.done(Path("f1"), black.Changed.NO)
self.assertEqual(len(out_lines), 1)
self.assertEqual(len(err_lines), 0)
)
def test_report_quiet(self) -> None:
- report = black.Report(quiet=True)
+ report = Report(quiet=True)
out_lines = []
err_lines = []
def err(msg: str, **kwargs: Any) -> None:
err_lines.append(msg)
- with patch("black.out", out), patch("black.err", err):
+ with patch("black.output._out", out), patch("black.output._err", err):
report.done(Path("f1"), black.Changed.NO)
self.assertEqual(len(out_lines), 0)
self.assertEqual(len(err_lines), 0)
def err(msg: str, **kwargs: Any) -> None:
err_lines.append(msg)
- with patch("black.out", out), patch("black.err", err):
+ with patch("black.output._out", out), patch("black.output._err", err):
report.done(Path("f1"), black.Changed.NO)
self.assertEqual(len(out_lines), 0)
self.assertEqual(len(err_lines), 0)
def err(msg: str, **kwargs: Any) -> None:
err_lines.append(msg)
- with patch("black.out", out), patch("black.err", err):
- black.DebugVisitor.show(source)
+ with patch("black.debug.out", out):
+ DebugVisitor.show(source)
actual = "\n".join(out_lines) + "\n"
log_name = ""
if expected != actual:
def err(msg: str, **kwargs: Any) -> None:
err_lines.append(msg)
- with patch("black.out", out), patch("black.err", err):
+ with patch("black.output._out", out), patch("black.output._err", err):
with self.assertRaises(AssertionError):
self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
def test_cache_broken_file(self) -> None:
mode = DEFAULT_MODE
with cache_dir() as workspace:
- cache_file = black.get_cache_file(mode)
+ cache_file = get_cache_file(mode)
with cache_file.open("w") as fobj:
fobj.write("this is not a pickle")
self.assertEqual(black.read_cache(mode), {})
fobj.write("print('hello')")
self.invokeBlack([str(src)])
cache = black.read_cache(mode)
- self.assertIn(src, cache)
+ self.assertIn(str(src), cache)
def test_cache_single_file_already_cached(self) -> None:
mode = DEFAULT_MODE
with two.open("r") as fobj:
self.assertEqual(fobj.read(), 'print("hello")\n')
cache = black.read_cache(mode)
- self.assertIn(one, cache)
- self.assertIn(two, cache)
+ self.assertIn(str(one), cache)
+ self.assertIn(str(two), cache)
def test_no_cache_when_writeback_diff(self) -> None:
mode = DEFAULT_MODE
"black.write_cache"
) as write_cache:
self.invokeBlack([str(src), "--diff"])
- cache_file = black.get_cache_file(mode)
+ cache_file = get_cache_file(mode)
self.assertFalse(cache_file.exists())
write_cache.assert_not_called()
read_cache.assert_not_called()
"black.write_cache"
) as write_cache:
self.invokeBlack([str(src), "--diff", "--color"])
- cache_file = black.get_cache_file(mode)
+ cache_file = get_cache_file(mode)
self.assertFalse(cache_file.exists())
write_cache.assert_not_called()
read_cache.assert_not_called()
black.main, ["-"], input=BytesIO(b"print('hello')")
)
self.assertEqual(result.exit_code, 0)
- cache_file = black.get_cache_file(mode)
+ cache_file = get_cache_file(mode)
self.assertFalse(cache_file.exists())
def test_read_cache_no_cachefile(self) -> None:
src.touch()
black.write_cache({}, [src], mode)
cache = black.read_cache(mode)
- self.assertIn(src, cache)
- self.assertEqual(cache[src], black.get_cache_info(src))
+ self.assertIn(str(src), cache)
+ self.assertEqual(cache[str(src)], black.get_cache_info(src))
def test_filter_cached(self) -> None:
with TemporaryDirectory() as workspace:
uncached.touch()
cached.touch()
cached_but_changed.touch()
- cache = {cached: black.get_cache_info(cached), cached_but_changed: (0.0, 0)}
+ cache = {
+ str(cached): black.get_cache_info(cached),
+ str(cached_but_changed): (0.0, 0),
+ }
todo, done = black.filter_cached(
cache, {uncached, cached, cached_but_changed}
)
fobj.write('print("hello")\n')
self.invokeBlack([str(workspace)], exit_code=123)
cache = black.read_cache(mode)
- self.assertNotIn(failing, cache)
- self.assertIn(clean, cache)
+ self.assertNotIn(str(failing), cache)
+ self.assertIn(str(clean), cache)
def test_write_cache_write_fail(self) -> None:
mode = DEFAULT_MODE
path.touch()
black.write_cache({}, [path], mode)
one = black.read_cache(mode)
- self.assertIn(path, one)
+ self.assertIn(str(path), one)
two = black.read_cache(short_mode)
- self.assertNotIn(path, two)
+ self.assertNotIn(str(path), two)
def test_single_file_force_pyi(self) -> None:
pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
actual = fh.read()
# verify cache with --pyi is separate
pyi_cache = black.read_cache(pyi_mode)
- self.assertIn(path, pyi_cache)
+ self.assertIn(str(path), pyi_cache)
normal_cache = black.read_cache(DEFAULT_MODE)
- self.assertNotIn(path, normal_cache)
+ self.assertNotIn(str(path), normal_cache)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(contents, actual)
black.assert_stable(contents, actual, pyi_mode)
pyi_cache = black.read_cache(pyi_mode)
normal_cache = black.read_cache(reg_mode)
for path in paths:
- self.assertIn(path, pyi_cache)
- self.assertNotIn(path, normal_cache)
+ self.assertIn(str(path), pyi_cache)
+ self.assertNotIn(str(path), normal_cache)
def test_pipe_force_pyi(self) -> None:
source, expected = read_data("force_pyi")
actual = fh.read()
# verify cache with --target-version is separate
py36_cache = black.read_cache(py36_mode)
- self.assertIn(path, py36_cache)
+ self.assertIn(str(path), py36_cache)
normal_cache = black.read_cache(reg_mode)
- self.assertNotIn(path, normal_cache)
+ self.assertNotIn(str(path), normal_cache)
self.assertEqual(actual, expected)
@event_loop()
pyi_cache = black.read_cache(py36_mode)
normal_cache = black.read_cache(reg_mode)
for path in paths:
- self.assertIn(path, pyi_cache)
- self.assertNotIn(path, normal_cache)
+ self.assertIn(str(path), pyi_cache)
+ self.assertNotIn(str(path), normal_cache)
def test_pipe_force_py36(self) -> None:
source, expected = read_data("force_py36")
this_abs = THIS_DIR.resolve()
sources.extend(
black.gen_python_files(
- path.iterdir(), this_abs, include, exclude, None, report, gitignore
+ path.iterdir(),
+ this_abs,
+ include,
+ exclude,
+ None,
+ None,
+ report,
+ gitignore,
+ verbose=False,
+ quiet=False,
+ )
+ )
+ self.assertEqual(sorted(expected), sorted(sources))
+
+ def test_gitignore_used_as_default(self) -> None:
+ path = Path(THIS_DIR / "data" / "include_exclude_tests")
+ include = re.compile(r"\.pyi?$")
+ extend_exclude = re.compile(r"/exclude/")
+ src = str(path / "b/")
+ report = black.Report()
+ expected: List[Path] = [
+ path / "b/.definitely_exclude/a.py",
+ path / "b/.definitely_exclude/a.pyi",
+ ]
+ sources = list(
+ black.get_sources(
+ ctx=FakeContext(),
+ src=(src,),
+ quiet=True,
+ verbose=False,
+ include=include,
+ exclude=None,
+ extend_exclude=extend_exclude,
+ force_exclude=None,
+ report=report,
+ stdin_filename=None,
)
)
self.assertEqual(sorted(expected), sorted(sources))
src=(src,),
quiet=True,
verbose=False,
- include=include,
- exclude=exclude,
+ include=re.compile(include),
+ exclude=re.compile(exclude),
+ extend_exclude=None,
+ force_exclude=None,
+ report=report,
+ stdin_filename=None,
+ )
+ )
+ self.assertEqual(sorted(expected), sorted(sources))
+
+ @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+ def test_get_sources_with_stdin(self) -> None:
+ include = ""
+ exclude = r"/exclude/|a\.py"
+ src = "-"
+ report = black.Report()
+ expected = [Path("-")]
+ sources = list(
+ black.get_sources(
+ ctx=FakeContext(),
+ src=(src,),
+ quiet=True,
+ verbose=False,
+ include=re.compile(include),
+ exclude=re.compile(exclude),
+ extend_exclude=None,
+ force_exclude=None,
+ report=report,
+ stdin_filename=None,
+ )
+ )
+ self.assertEqual(sorted(expected), sorted(sources))
+
+ @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+ def test_get_sources_with_stdin_filename(self) -> None:
+ include = ""
+ exclude = r"/exclude/|a\.py"
+ src = "-"
+ report = black.Report()
+ stdin_filename = str(THIS_DIR / "data/collections.py")
+ expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
+ sources = list(
+ black.get_sources(
+ ctx=FakeContext(),
+ src=(src,),
+ quiet=True,
+ verbose=False,
+ include=re.compile(include),
+ exclude=re.compile(exclude),
+ extend_exclude=None,
+ force_exclude=None,
+ report=report,
+ stdin_filename=stdin_filename,
+ )
+ )
+ self.assertEqual(sorted(expected), sorted(sources))
+
+ @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+ def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
+ # Exclude shouldn't exclude stdin_filename since it is mimicking the
+ # file being passed directly. This is the same as
+ # test_exclude_for_issue_1572
+ path = THIS_DIR / "data" / "include_exclude_tests"
+ include = ""
+ exclude = r"/exclude/|a\.py"
+ src = "-"
+ report = black.Report()
+ stdin_filename = str(path / "b/exclude/a.py")
+ expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
+ sources = list(
+ black.get_sources(
+ ctx=FakeContext(),
+ src=(src,),
+ quiet=True,
+ verbose=False,
+ include=re.compile(include),
+ exclude=re.compile(exclude),
+ extend_exclude=None,
force_exclude=None,
report=report,
+ stdin_filename=stdin_filename,
)
)
self.assertEqual(sorted(expected), sorted(sources))
+ @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+ def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
+ # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
+ # file being passed directly. This is the same as
+ # test_exclude_for_issue_1572
+ path = THIS_DIR / "data" / "include_exclude_tests"
+ include = ""
+ extend_exclude = r"/exclude/|a\.py"
+ src = "-"
+ report = black.Report()
+ stdin_filename = str(path / "b/exclude/a.py")
+ expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
+ sources = list(
+ black.get_sources(
+ ctx=FakeContext(),
+ src=(src,),
+ quiet=True,
+ verbose=False,
+ include=re.compile(include),
+ exclude=re.compile(""),
+ extend_exclude=re.compile(extend_exclude),
+ force_exclude=None,
+ report=report,
+ stdin_filename=stdin_filename,
+ )
+ )
+ self.assertEqual(sorted(expected), sorted(sources))
+
+ @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
+ # Force exclude should exclude the file when passing it through
+ # stdin_filename
+ path = THIS_DIR / "data" / "include_exclude_tests"
+ include = ""
+ force_exclude = r"/exclude/|a\.py"
+ src = "-"
+ report = black.Report()
+ stdin_filename = str(path / "b/exclude/a.py")
+ sources = list(
+ black.get_sources(
+ ctx=FakeContext(),
+ src=(src,),
+ quiet=True,
+ verbose=False,
+ include=re.compile(include),
+ exclude=re.compile(""),
+ extend_exclude=None,
+ force_exclude=re.compile(force_exclude),
+ report=report,
+ stdin_filename=stdin_filename,
+ )
+ )
+ self.assertEqual([], sorted(sources))
+
+ def test_reformat_one_with_stdin(self) -> None:
+ with patch(
+ "black.format_stdin_to_stdout",
+ return_value=lambda *args, **kwargs: black.Changed.YES,
+ ) as fsts:
+ report = MagicMock()
+ path = Path("-")
+ black.reformat_one(
+ path,
+ fast=True,
+ write_back=black.WriteBack.YES,
+ mode=DEFAULT_MODE,
+ report=report,
+ )
+ fsts.assert_called_once()
+ report.done.assert_called_with(path, black.Changed.YES)
+
+ def test_reformat_one_with_stdin_filename(self) -> None:
+ with patch(
+ "black.format_stdin_to_stdout",
+ return_value=lambda *args, **kwargs: black.Changed.YES,
+ ) as fsts:
+ report = MagicMock()
+ p = "foo.py"
+ path = Path(f"__BLACK_STDIN_FILENAME__{p}")
+ expected = Path(p)
+ black.reformat_one(
+ path,
+ fast=True,
+ write_back=black.WriteBack.YES,
+ mode=DEFAULT_MODE,
+ report=report,
+ )
+ fsts.assert_called_once_with(
+ fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
+ )
+ # __BLACK_STDIN_FILENAME__ should have been stripped
+ report.done.assert_called_with(expected, black.Changed.YES)
+
+ def test_reformat_one_with_stdin_filename_pyi(self) -> None:
+ with patch(
+ "black.format_stdin_to_stdout",
+ return_value=lambda *args, **kwargs: black.Changed.YES,
+ ) as fsts:
+ report = MagicMock()
+ p = "foo.pyi"
+ path = Path(f"__BLACK_STDIN_FILENAME__{p}")
+ expected = Path(p)
+ black.reformat_one(
+ path,
+ fast=True,
+ write_back=black.WriteBack.YES,
+ mode=DEFAULT_MODE,
+ report=report,
+ )
+ fsts.assert_called_once_with(
+ fast=True,
+ write_back=black.WriteBack.YES,
+ mode=replace(DEFAULT_MODE, is_pyi=True),
+ )
+ # __BLACK_STDIN_FILENAME__ should have been stripped
+ report.done.assert_called_with(expected, black.Changed.YES)
+
+ def test_reformat_one_with_stdin_and_existing_path(self) -> None:
+ with patch(
+ "black.format_stdin_to_stdout",
+ return_value=lambda *args, **kwargs: black.Changed.YES,
+ ) as fsts:
+ report = MagicMock()
+ # Even with an existing file, since we are forcing stdin, black
+ # should output to stdout and not modify the file inplace
+ p = Path(str(THIS_DIR / "data/collections.py"))
+ # Make sure is_file actually returns True
+ self.assertTrue(p.is_file())
+ path = Path(f"__BLACK_STDIN_FILENAME__{p}")
+ expected = Path(p)
+ black.reformat_one(
+ path,
+ fast=True,
+ write_back=black.WriteBack.YES,
+ mode=DEFAULT_MODE,
+ report=report,
+ )
+ fsts.assert_called_once()
+ # __BLACK_STDIN_FILENAME__ should have been stripped
+ report.done.assert_called_with(expected, black.Changed.YES)
+
+ def test_reformat_one_with_stdin_empty(self) -> None:
+ output = io.StringIO()
+ with patch("io.TextIOWrapper", lambda *args, **kwargs: output):
+ try:
+ black.format_stdin_to_stdout(
+ fast=True,
+ content="",
+ write_back=black.WriteBack.YES,
+ mode=DEFAULT_MODE,
+ )
+ except io.UnsupportedOperation:
+ pass # StringIO does not support detach
+ assert output.getvalue() == ""
+
def test_gitignore_exclude(self) -> None:
path = THIS_DIR / "data" / "include_exclude_tests"
include = re.compile(r"\.pyi?$")
this_abs = THIS_DIR.resolve()
sources.extend(
black.gen_python_files(
- path.iterdir(), this_abs, include, exclude, None, report, gitignore
+ path.iterdir(),
+ this_abs,
+ include,
+ exclude,
+ None,
+ None,
+ report,
+ gitignore,
+ verbose=False,
+ quiet=False,
+ )
+ )
+ self.assertEqual(sorted(expected), sorted(sources))
+
+ def test_nested_gitignore(self) -> None:
+ path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
+ include = re.compile(r"\.pyi?$")
+ exclude = re.compile(r"")
+ root_gitignore = black.files.get_gitignore(path)
+ report = black.Report()
+ expected: List[Path] = [
+ Path(path / "x.py"),
+ Path(path / "root/b.py"),
+ Path(path / "root/c.py"),
+ Path(path / "root/child/c.py"),
+ ]
+ this_abs = THIS_DIR.resolve()
+ sources = list(
+ black.gen_python_files(
+ path.iterdir(),
+ this_abs,
+ include,
+ exclude,
+ None,
+ None,
+ report,
+ root_gitignore,
+ verbose=False,
+ quiet=False,
)
)
self.assertEqual(sorted(expected), sorted(sources))
+ def test_invalid_gitignore(self) -> None:
+ path = THIS_DIR / "data" / "invalid_gitignore_tests"
+ empty_config = path / "pyproject.toml"
+ result = BlackRunner().invoke(
+ black.main, ["--verbose", "--config", str(empty_config), str(path)]
+ )
+ assert result.exit_code == 1
+ assert result.stderr_bytes is not None
+
+ gitignore = path / ".gitignore"
+ assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
+
+ def test_invalid_nested_gitignore(self) -> None:
+ path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
+ empty_config = path / "pyproject.toml"
+ result = BlackRunner().invoke(
+ black.main, ["--verbose", "--config", str(empty_config), str(path)]
+ )
+ assert result.exit_code == 1
+ assert result.stderr_bytes is not None
+
+ gitignore = path / "a" / ".gitignore"
+ assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
+
def test_empty_include(self) -> None:
path = THIS_DIR / "data" / "include_exclude_tests"
report = black.Report()
Path(path / "b/.definitely_exclude/a.pie"),
Path(path / "b/.definitely_exclude/a.py"),
Path(path / "b/.definitely_exclude/a.pyi"),
+ Path(path / ".gitignore"),
+ Path(path / "pyproject.toml"),
]
this_abs = THIS_DIR.resolve()
sources.extend(
empty,
re.compile(black.DEFAULT_EXCLUDES),
None,
+ None,
report,
gitignore,
+ verbose=False,
+ quiet=False,
)
)
self.assertEqual(sorted(expected), sorted(sources))
- def test_empty_exclude(self) -> None:
+ def test_extend_exclude(self) -> None:
path = THIS_DIR / "data" / "include_exclude_tests"
report = black.Report()
gitignore = PathSpec.from_lines("gitwildmatch", [])
- empty = re.compile(r"")
sources: List[Path] = []
expected = [
- Path(path / "b/dont_exclude/a.py"),
- Path(path / "b/dont_exclude/a.pyi"),
Path(path / "b/exclude/a.py"),
- Path(path / "b/exclude/a.pyi"),
- Path(path / "b/.definitely_exclude/a.py"),
- Path(path / "b/.definitely_exclude/a.pyi"),
+ Path(path / "b/dont_exclude/a.py"),
]
this_abs = THIS_DIR.resolve()
sources.extend(
path.iterdir(),
this_abs,
re.compile(black.DEFAULT_INCLUDES),
- empty,
+ re.compile(r"\.pyi$"),
+ re.compile(r"\.definitely_exclude"),
None,
report,
gitignore,
+ verbose=False,
+ quiet=False,
)
)
self.assertEqual(sorted(expected), sorted(sources))
- def test_invalid_include_exclude(self) -> None:
- for option in ["--include", "--exclude"]:
+ def test_invalid_cli_regex(self) -> None:
+ for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
+ def test_required_version_matches_version(self) -> None:
+ self.invokeBlack(
+ ["--required-version", black.__version__], exit_code=0, ignore_config=True
+ )
+
+ def test_required_version_does_not_match_version(self) -> None:
+ self.invokeBlack(
+ ["--required-version", "20.99b"], exit_code=1, ignore_config=True
+ )
+
def test_preserves_line_endings(self) -> None:
with TemporaryDirectory() as workspace:
test_file = Path(workspace) / "test.py"
black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
)
self.assertEqual(result.exit_code, 0)
- output = runner.stdout_bytes
+ output = result.stdout_bytes
self.assertIn(nl.encode("utf8"), output)
if nl == "\n":
self.assertNotIn(b"\r\n", output)
try:
list(
black.gen_python_files(
- path.iterdir(), root, include, exclude, None, report, gitignore
+ path.iterdir(),
+ root,
+ include,
+ exclude,
+ None,
+ None,
+ report,
+ gitignore,
+ verbose=False,
+ quiet=False,
)
)
except ValueError as ve:
with self.assertRaises(ValueError):
list(
black.gen_python_files(
- path.iterdir(), root, include, exclude, None, report, gitignore
+ path.iterdir(),
+ root,
+ include,
+ exclude,
+ None,
+ None,
+ report,
+ gitignore,
+ verbose=False,
+ quiet=False,
)
)
path.iterdir.assert_called()
def test_shhh_click(self) -> None:
try:
- from click import _unicodefun # type: ignore
+ from click import _unicodefun
except ModuleNotFoundError:
self.skipTest("Incompatible Click version")
if not hasattr(_unicodefun, "_verify_python3_env"):
with patch("locale.getpreferredencoding") as gpe:
gpe.return_value = "ASCII"
with self.assertRaises(RuntimeError):
- _unicodefun._verify_python3_env()
+ _unicodefun._verify_python3_env() # type: ignore
# Now, let's silence Click...
black.patch_click()
# ...and confirm it's silent.
with patch("locale.getpreferredencoding") as gpe:
gpe.return_value = "ASCII"
try:
- _unicodefun._verify_python3_env()
+ _unicodefun._verify_python3_env() # type: ignore
except RuntimeError as re:
self.fail(f"`patch_click()` failed, exception still raised: {re}")
critical=fail,
log=fail,
):
- ff(THIS_FILE)
+ ff(THIS_DIR / "util.py")
def test_invalid_config_return_code(self) -> None:
tmp_file = Path(black.dump_to_file())
self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
+ @patch(
+ "black.files.find_user_pyproject_toml",
+ black.files.find_user_pyproject_toml.__wrapped__,
+ )
+ def test_find_user_pyproject_toml_linux(self) -> None:
+ if system() == "Windows":
+ return
+
+ # Test if XDG_CONFIG_HOME is checked
+ with TemporaryDirectory() as workspace:
+ tmp_user_config = Path(workspace) / "black"
+ with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
+ self.assertEqual(
+ black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
+ )
+
+ # Test fallback for XDG_CONFIG_HOME
+ with patch.dict("os.environ"):
+ os.environ.pop("XDG_CONFIG_HOME", None)
+ fallback_user_config = Path("~/.config").expanduser() / "black"
+ self.assertEqual(
+ black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
+ )
+
+ def test_find_user_pyproject_toml_windows(self) -> None:
+ if system() != "Windows":
+ return
+
+ user_config_path = Path.home() / ".black"
+ self.assertEqual(
+ black.files.find_user_pyproject_toml(), user_config_path.resolve()
+ )
+
def test_bpo_33660_workaround(self) -> None:
if system() == "Windows":
return
# https://bugs.python.org/issue33660
-
- old_cwd = Path.cwd()
- try:
- root = Path("/")
- os.chdir(str(root))
+ root = Path("/")
+ with change_directory(root):
path = Path("workspace") / "project"
report = black.Report(verbose=True)
normalized_path = black.normalize_path_maybe_ignore(path, root, report)
self.assertEqual(normalized_path, "workspace/project")
+
+ def test_newline_comment_interaction(self) -> None:
+ source = "class A:\\\r\n# type: ignore\n pass\n"
+ output = black.format_str(source, mode=DEFAULT_MODE)
+ black.assert_stable(source, output, mode=DEFAULT_MODE)
+
+ def test_bpo_2142_workaround(self) -> None:
+
+ # https://bugs.python.org/issue2142
+
+ source, _ = read_data("missing_final_newline.py")
+ # read_data adds a trailing newline
+ source = source.rstrip()
+ expected, _ = read_data("missing_final_newline.diff")
+ tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
+ diff_header = re.compile(
+ rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
+ r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
+ )
+ try:
+ result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
+ self.assertEqual(result.exit_code, 0)
finally:
- os.chdir(str(old_cwd))
+ os.unlink(tmp_file)
+ actual = result.output
+ actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
+ self.assertEqual(actual, expected)
+
+ @pytest.mark.python2
+ def test_docstring_reformat_for_py27(self) -> None:
+ """
+ Check that stripping trailing whitespace from Python 2 docstrings
+ doesn't trigger a "not equivalent to source" error
+ """
+ source = (
+ b'def foo():\r\n """Testing\r\n Testing """\r\n print "Foo"\r\n'
+ )
+ expected = 'def foo():\n """Testing\n Testing"""\n print "Foo"\n'
+
+ result = CliRunner().invoke(
+ black.main,
+ ["-", "-q", "--target-version=py27"],
+ input=BytesIO(source),
+ )
+
+ self.assertEqual(result.exit_code, 0)
+ actual = result.output
+ self.assertFormatEqual(actual, expected)
+
+ @staticmethod
+ def compare_results(
+ result: click.testing.Result, expected_value: str, expected_exit_code: int
+ ) -> None:
+ """Helper method to test the value and exit code of a click Result."""
+ assert (
+ result.output == expected_value
+ ), "The output did not match the expected value."
+ assert result.exit_code == expected_exit_code, "The exit code is incorrect."
+
+ def test_code_option(self) -> None:
+ """Test the code option with no changes."""
+ code = 'print("Hello world")\n'
+ args = ["--code", code]
+ result = CliRunner().invoke(black.main, args)
+
+ self.compare_results(result, code, 0)
+
+ def test_code_option_changed(self) -> None:
+ """Test the code option when changes are required."""
+ code = "print('hello world')"
+ formatted = black.format_str(code, mode=DEFAULT_MODE)
+
+ args = ["--code", code]
+ result = CliRunner().invoke(black.main, args)
+
+ self.compare_results(result, formatted, 0)
+
+ def test_code_option_check(self) -> None:
+ """Test the code option when check is passed."""
+ args = ["--check", "--code", 'print("Hello world")\n']
+ result = CliRunner().invoke(black.main, args)
+ self.compare_results(result, "", 0)
+
+ def test_code_option_check_changed(self) -> None:
+ """Test the code option when changes are required, and check is passed."""
+ args = ["--check", "--code", "print('hello world')"]
+ result = CliRunner().invoke(black.main, args)
+ self.compare_results(result, "", 1)
+
+ def test_code_option_diff(self) -> None:
+ """Test the code option when diff is passed."""
+ code = "print('hello world')"
+ formatted = black.format_str(code, mode=DEFAULT_MODE)
+ result_diff = diff(code, formatted, "STDIN", "STDOUT")
+
+ args = ["--diff", "--code", code]
+ result = CliRunner().invoke(black.main, args)
+
+ # Remove time from diff
+ output = DIFF_TIME.sub("", result.output)
+
+ assert output == result_diff, "The output did not match the expected value."
+ assert result.exit_code == 0, "The exit code is incorrect."
+
+ def test_code_option_color_diff(self) -> None:
+ """Test the code option when color and diff are passed."""
+ code = "print('hello world')"
+ formatted = black.format_str(code, mode=DEFAULT_MODE)
+
+ result_diff = diff(code, formatted, "STDIN", "STDOUT")
+ result_diff = color_diff(result_diff)
+
+ args = ["--diff", "--color", "--code", code]
+ result = CliRunner().invoke(black.main, args)
+
+ # Remove time from diff
+ output = DIFF_TIME.sub("", result.output)
+
+ assert output == result_diff, "The output did not match the expected value."
+ assert result.exit_code == 0, "The exit code is incorrect."
+
+ def test_code_option_safe(self) -> None:
+ """Test that the code option throws an error when the sanity checks fail."""
+ # Patch black.assert_equivalent to ensure the sanity checks fail
+ with patch.object(black, "assert_equivalent", side_effect=AssertionError):
+ code = 'print("Hello world")'
+ error_msg = f"{code}\nerror: cannot format <string>: \n"
+
+ args = ["--safe", "--code", code]
+ result = CliRunner().invoke(black.main, args)
+
+ self.compare_results(result, error_msg, 123)
+
+ def test_code_option_fast(self) -> None:
+ """Test that the code option ignores errors when the sanity checks fail."""
+ # Patch black.assert_equivalent to ensure the sanity checks fail
+ with patch.object(black, "assert_equivalent", side_effect=AssertionError):
+ code = 'print("Hello world")'
+ formatted = black.format_str(code, mode=DEFAULT_MODE)
+
+ args = ["--fast", "--code", code]
+ result = CliRunner().invoke(black.main, args)
+
+ self.compare_results(result, formatted, 0)
+
+ def test_code_option_config(self) -> None:
+ """
+ Test that the code option finds the pyproject.toml in the current directory.
+ """
+ with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
+ args = ["--code", "print"]
+ CliRunner().invoke(black.main, args)
+
+ pyproject_path = Path(Path().cwd(), "pyproject.toml").resolve()
+ assert (
+ len(parse.mock_calls) >= 1
+ ), "Expected config parse to be called with the current directory."
+
+ _, call_args, _ = parse.mock_calls[0]
+ assert (
+ call_args[0].lower() == str(pyproject_path).lower()
+ ), "Incorrect config loaded."
+
+ def test_code_option_parent_config(self) -> None:
+ """
+ Test that the code option finds the pyproject.toml in the parent directory.
+ """
+ with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
+ with change_directory(Path("tests")):
+ args = ["--code", "print"]
+ CliRunner().invoke(black.main, args)
+
+ pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
+ assert (
+ len(parse.mock_calls) >= 1
+ ), "Expected config parse to be called with the current directory."
+
+ _, call_args, _ = parse.mock_calls[0]
+ assert (
+ call_args[0].lower() == str(pyproject_path).lower()
+ ), "Incorrect config loaded."
with open(black.__file__, "r", encoding="utf-8") as _bf: