import re
import sys
import types
-import unittest
from concurrent.futures import ThreadPoolExecutor
from contextlib import contextmanager, redirect_stderr
from dataclasses import replace
import black.files
from black import Feature, TargetVersion
from black import re_compile_maybe_verbose as compile_pattern
-from black.cache import get_cache_dir, get_cache_file
+from black.cache import FileData, get_cache_dir, get_cache_file
from black.debug import DebugVisitor
from black.output import color_diff, diff
from black.report import Report
project_root = Path(THIS_DIR / "data" / "nested_gitignore_tests")
working_directory = project_root / "root"
target_abspath = working_directory / "child"
- target_contents = (
- src.relative_to(working_directory) for src in target_abspath.iterdir()
- )
+ target_contents = list(target_abspath.iterdir())
def mock_n_calls(responses: List[bool]) -> Callable[[], bool]:
def _mocked_calls() -> bool:
with patch("pathlib.Path.iterdir", return_value=target_contents), patch(
"pathlib.Path.cwd", return_value=working_directory
), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])):
- ctx = FakeContext()
- ctx.obj["root"] = project_root
+ # Note that the root folder (project_root) isn't the folder
+ # named "root" (aka working_directory)
report = MagicMock(verbose=True)
black.get_sources(
- ctx=ctx,
+ root=project_root,
src=("./child",),
quiet=False,
verbose=True,
for _, mock_args, _ in report.path_ignored.mock_calls
), "A symbolic link was reported."
report.path_ignored.assert_called_once_with(
- Path("child", "b.py"), "matches a .gitignore file content"
+ Path("root", "child", "b.py"), "matches a .gitignore file content"
)
def test_report_verbose(self) -> None:
self.assertEqual(len(n.children), 1)
self.assertEqual(n.children[0].type, black.token.ENDMARKER)
+ @patch("tests.conftest.PRINT_FULL_TREE", True)
+ @patch("tests.conftest.PRINT_TREE_DIFF", False)
@pytest.mark.incompatible_with_mypyc
- @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
- def test_assertFormatEqual(self) -> None:
+ def test_assertFormatEqual_print_full_tree(self) -> None:
out_lines = []
err_lines = []
self.assertIn("Actual tree:", out_str)
self.assertEqual("".join(err_lines), "")
+ @patch("tests.conftest.PRINT_FULL_TREE", False)
+ @patch("tests.conftest.PRINT_TREE_DIFF", True)
+ @pytest.mark.incompatible_with_mypyc
+ def test_assertFormatEqual_print_tree_diff(self) -> None:
+ out_lines = []
+ err_lines = []
+
+ def out(msg: str, **kwargs: Any) -> None:
+ out_lines.append(msg)
+
+ def err(msg: str, **kwargs: Any) -> None:
+ err_lines.append(msg)
+
+ with patch("black.output._out", out), patch("black.output._err", err):
+ with self.assertRaises(AssertionError):
+ self.assertFormatEqual("j = [1, 2, 3]\n", "j = [1, 2, 3,]\n")
+
+ out_str = "".join(out_lines)
+ self.assertIn("Tree Diff:", out_str)
+ self.assertIn("+ COMMA", out_str)
+ self.assertIn("+ ','", out_str)
+ self.assertEqual("".join(err_lines), "")
+
@event_loop()
@patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError))
def test_works_in_mono_process_only_environment(self) -> None:
self.invokeBlack([str(path), "--pyi"])
actual = path.read_text(encoding="utf-8")
# verify cache with --pyi is separate
- pyi_cache = black.read_cache(pyi_mode)
- self.assertIn(str(path), pyi_cache)
- normal_cache = black.read_cache(DEFAULT_MODE)
- self.assertNotIn(str(path), normal_cache)
+ pyi_cache = black.Cache.read(pyi_mode)
+ assert not pyi_cache.is_changed(path)
+ normal_cache = black.Cache.read(DEFAULT_MODE)
+ assert normal_cache.is_changed(path)
self.assertFormatEqual(expected, actual)
black.assert_equivalent(contents, actual)
black.assert_stable(contents, actual, pyi_mode)
actual = path.read_text(encoding="utf-8")
self.assertEqual(actual, expected)
# verify cache with --pyi is separate
- pyi_cache = black.read_cache(pyi_mode)
- normal_cache = black.read_cache(reg_mode)
+ pyi_cache = black.Cache.read(pyi_mode)
+ normal_cache = black.Cache.read(reg_mode)
for path in paths:
- self.assertIn(str(path), pyi_cache)
- self.assertNotIn(str(path), normal_cache)
+ assert not pyi_cache.is_changed(path)
+ assert normal_cache.is_changed(path)
def test_pipe_force_pyi(self) -> None:
source, expected = read_data("miscellaneous", "force_pyi")
self.invokeBlack([str(path), *PY36_ARGS])
actual = path.read_text(encoding="utf-8")
# verify cache with --target-version is separate
- py36_cache = black.read_cache(py36_mode)
- self.assertIn(str(path), py36_cache)
- normal_cache = black.read_cache(reg_mode)
- self.assertNotIn(str(path), normal_cache)
+ py36_cache = black.Cache.read(py36_mode)
+ assert not py36_cache.is_changed(path)
+ normal_cache = black.Cache.read(reg_mode)
+ assert normal_cache.is_changed(path)
self.assertEqual(actual, expected)
@event_loop()
actual = path.read_text(encoding="utf-8")
self.assertEqual(actual, expected)
# verify cache with --target-version is separate
- pyi_cache = black.read_cache(py36_mode)
- normal_cache = black.read_cache(reg_mode)
+ pyi_cache = black.Cache.read(py36_mode)
+ normal_cache = black.Cache.read(reg_mode)
for path in paths:
- self.assertIn(str(path), pyi_cache)
- self.assertNotIn(str(path), normal_cache)
+ assert not pyi_cache.is_changed(path)
+ assert normal_cache.is_changed(path)
def test_pipe_force_py36(self) -> None:
source, expected = read_data("miscellaneous", "force_py36")
with self.assertRaises(AssertionError):
black.assert_equivalent("{}", "None")
- def test_shhh_click(self) -> None:
- try:
- from click import _unicodefun # type: ignore
- except ImportError:
- self.skipTest("Incompatible Click version")
-
- if not hasattr(_unicodefun, "_verify_python_env"):
- self.skipTest("Incompatible Click version")
-
- # First, let's see if Click is crashing with a preferred ASCII charset.
- with patch("locale.getpreferredencoding") as gpe:
- gpe.return_value = "ASCII"
- with self.assertRaises(RuntimeError):
- _unicodefun._verify_python_env()
- # 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_python_env()
- except RuntimeError as re:
- self.fail(f"`patch_click()` failed, exception still raised: {re}")
-
def test_root_logger_not_used_directly(self) -> None:
def fail(*args: Any, **kwargs: Any) -> None:
self.fail("Record created with root logger")
with cache_dir() as workspace:
cache_file = get_cache_file(mode)
cache_file.write_text("this is not a pickle", encoding="utf-8")
- assert black.read_cache(mode) == {}
+ assert black.Cache.read(mode).file_data == {}
src = (workspace / "test.py").resolve()
src.write_text("print('hello')", encoding="utf-8")
invokeBlack([str(src)])
- cache = black.read_cache(mode)
- assert str(src) in cache
+ cache = black.Cache.read(mode)
+ assert not cache.is_changed(src)
def test_cache_single_file_already_cached(self) -> None:
mode = DEFAULT_MODE
with cache_dir() as workspace:
src = (workspace / "test.py").resolve()
src.write_text("print('hello')", encoding="utf-8")
- black.write_cache({}, [src], mode)
+ cache = black.Cache.read(mode)
+ cache.write([src])
invokeBlack([str(src)])
assert src.read_text(encoding="utf-8") == "print('hello')"
one.write_text("print('hello')", encoding="utf-8")
two = (workspace / "two.py").resolve()
two.write_text("print('hello')", encoding="utf-8")
- black.write_cache({}, [one], mode)
+ cache = black.Cache.read(mode)
+ cache.write([one])
invokeBlack([str(workspace)])
assert one.read_text(encoding="utf-8") == "print('hello')"
assert two.read_text(encoding="utf-8") == 'print("hello")\n'
- cache = black.read_cache(mode)
- assert str(one) in cache
- assert str(two) in cache
+ cache = black.Cache.read(mode)
+ assert not cache.is_changed(one)
+ assert not cache.is_changed(two)
+ @pytest.mark.incompatible_with_mypyc
@pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
def test_no_cache_when_writeback_diff(self, color: bool) -> None:
mode = DEFAULT_MODE
with cache_dir() as workspace:
src = (workspace / "test.py").resolve()
src.write_text("print('hello')", encoding="utf-8")
- with patch("black.read_cache") as read_cache, patch(
- "black.write_cache"
+ with patch.object(black.Cache, "read") as read_cache, patch.object(
+ black.Cache, "write"
) as write_cache:
cmd = [str(src), "--diff"]
if color:
invokeBlack(cmd)
cache_file = get_cache_file(mode)
assert cache_file.exists() is False
+ read_cache.assert_called_once()
write_cache.assert_not_called()
- read_cache.assert_not_called()
@pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
@event_loop()
def test_read_cache_no_cachefile(self) -> None:
mode = DEFAULT_MODE
with cache_dir():
- assert black.read_cache(mode) == {}
+ assert black.Cache.read(mode).file_data == {}
def test_write_cache_read_cache(self) -> None:
mode = DEFAULT_MODE
with cache_dir() as workspace:
src = (workspace / "test.py").resolve()
src.touch()
- black.write_cache({}, [src], mode)
- cache = black.read_cache(mode)
- assert str(src) in cache
- assert cache[str(src)] == black.get_cache_info(src)
+ write_cache = black.Cache.read(mode)
+ write_cache.write([src])
+ read_cache = black.Cache.read(mode)
+ assert not read_cache.is_changed(src)
+ @pytest.mark.incompatible_with_mypyc
def test_filter_cached(self) -> None:
with TemporaryDirectory() as workspace:
path = Path(workspace)
uncached.touch()
cached.touch()
cached_but_changed.touch()
- cache = {
- str(cached): black.get_cache_info(cached),
- str(cached_but_changed): (0.0, 0),
- }
- todo, done = black.cache.filter_cached(
- cache, {uncached, cached, cached_but_changed}
- )
+ cache = black.Cache.read(DEFAULT_MODE)
+
+ orig_func = black.Cache.get_file_data
+
+ def wrapped_func(path: Path) -> FileData:
+ if path == cached:
+ return orig_func(path)
+ if path == cached_but_changed:
+ return FileData(0.0, 0, "")
+ raise AssertionError
+
+ with patch.object(black.Cache, "get_file_data", side_effect=wrapped_func):
+ cache.write([cached, cached_but_changed])
+ todo, done = cache.filtered_cached({uncached, cached, cached_but_changed})
assert todo == {uncached, cached_but_changed}
assert done == {cached}
+ def test_filter_cached_hash(self) -> None:
+ with TemporaryDirectory() as workspace:
+ path = Path(workspace)
+ src = (path / "test.py").resolve()
+ src.write_text("print('hello')", encoding="utf-8")
+ st = src.stat()
+ cache = black.Cache.read(DEFAULT_MODE)
+ cache.write([src])
+ cached_file_data = cache.file_data[str(src)]
+
+ todo, done = cache.filtered_cached([src])
+ assert todo == set()
+ assert done == {src}
+ assert cached_file_data.st_mtime == st.st_mtime
+
+ # Modify st_mtime
+ cached_file_data = cache.file_data[str(src)] = FileData(
+ cached_file_data.st_mtime - 1,
+ cached_file_data.st_size,
+ cached_file_data.hash,
+ )
+ todo, done = cache.filtered_cached([src])
+ assert todo == set()
+ assert done == {src}
+ assert cached_file_data.st_mtime < st.st_mtime
+ assert cached_file_data.st_size == st.st_size
+ assert cached_file_data.hash == black.Cache.hash_digest(src)
+
+ # Modify contents
+ src.write_text("print('hello world')", encoding="utf-8")
+ new_st = src.stat()
+ todo, done = cache.filtered_cached([src])
+ assert todo == {src}
+ assert done == set()
+ assert cached_file_data.st_mtime < new_st.st_mtime
+ assert cached_file_data.st_size != new_st.st_size
+ assert cached_file_data.hash != black.Cache.hash_digest(src)
+
def test_write_cache_creates_directory_if_needed(self) -> None:
mode = DEFAULT_MODE
with cache_dir(exists=False) as workspace:
assert not workspace.exists()
- black.write_cache({}, [], mode)
+ cache = black.Cache.read(mode)
+ cache.write([])
assert workspace.exists()
@event_loop()
clean = (workspace / "clean.py").resolve()
clean.write_text('print("hello")\n', encoding="utf-8")
invokeBlack([str(workspace)], exit_code=123)
- cache = black.read_cache(mode)
- assert str(failing) not in cache
- assert str(clean) in cache
+ cache = black.Cache.read(mode)
+ assert cache.is_changed(failing)
+ assert not cache.is_changed(clean)
def test_write_cache_write_fail(self) -> None:
mode = DEFAULT_MODE
- with cache_dir(), patch.object(Path, "open") as mock:
- mock.side_effect = OSError
- black.write_cache({}, [], mode)
+ with cache_dir():
+ cache = black.Cache.read(mode)
+ with patch.object(Path, "open") as mock:
+ mock.side_effect = OSError
+ cache.write([])
def test_read_cache_line_lengths(self) -> None:
mode = DEFAULT_MODE
with cache_dir() as workspace:
path = (workspace / "file.py").resolve()
path.touch()
- black.write_cache({}, [path], mode)
- one = black.read_cache(mode)
- assert str(path) in one
- two = black.read_cache(short_mode)
- assert str(path) not in two
+ cache = black.Cache.read(mode)
+ cache.write([path])
+ one = black.Cache.read(mode)
+ assert not one.is_changed(path)
+ two = black.Cache.read(short_mode)
+ assert two.is_changed(path)
def assert_collected_sources(
src: Sequence[Union[str, Path]],
expected: Sequence[Union[str, Path]],
*,
- ctx: Optional[FakeContext] = None,
+ root: Optional[Path] = None,
exclude: Optional[str] = None,
include: Optional[str] = None,
extend_exclude: Optional[str] = None,
)
gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
collected = black.get_sources(
- ctx=ctx or FakeContext(),
+ root=root or THIS_DIR,
src=gs_src,
quiet=False,
verbose=False,
base / "b/.definitely_exclude/a.pyi",
]
src = [base / "b/"]
- ctx = FakeContext()
- ctx.obj["root"] = base
- assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
+ assert_collected_sources(src, expected, root=base, extend_exclude=r"/exclude/")
def test_gitignore_used_on_multiple_sources(self) -> None:
root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
root / "dir1" / "b.py",
root / "dir2" / "b.py",
]
- ctx = FakeContext()
- ctx.obj["root"] = root
src = [root / "dir1", root / "dir2"]
- assert_collected_sources(src, expected, ctx=ctx)
+ assert_collected_sources(src, expected, root=root)
@patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
def test_exclude_for_issue_1572(self) -> None:
# If gitignore with */* is in root
root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir")
expected = [root / "b.py"]
- ctx = FakeContext()
- ctx.obj["root"] = root
- assert_collected_sources([root], expected, ctx=ctx)
+ assert_collected_sources([root], expected, root=root)
# If .gitignore with */* is nested
root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
root / "a.py",
root / "subdir" / "b.py",
]
- ctx = FakeContext()
- ctx.obj["root"] = root
- assert_collected_sources([root], expected, ctx=ctx)
+ assert_collected_sources([root], expected, root=root)
# If command is executed from outer dir
root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
target = root / "subdir"
expected = [target / "b.py"]
- ctx = FakeContext()
- ctx.obj["root"] = root
- assert_collected_sources([target], expected, ctx=ctx)
+ assert_collected_sources([target], expected, root=root)
def test_empty_include(self) -> None:
path = DATA_DIR / "include_exclude_tests"
)
@pytest.mark.incompatible_with_mypyc
- def test_symlink_out_of_root_directory(self) -> None:
+ def test_symlinks(self) -> None:
path = MagicMock()
root = THIS_DIR.resolve()
- child = MagicMock()
include = re.compile(black.DEFAULT_INCLUDES)
exclude = re.compile(black.DEFAULT_EXCLUDES)
report = black.Report()
gitignore = PathSpec.from_lines("gitwildmatch", [])
- # `child` should behave like a symlink which resolved path is clearly
- # outside of the `root` directory.
- path.iterdir.return_value = [child]
- child.resolve.return_value = Path("/a/b/c")
- child.as_posix.return_value = "/a/b/c"
- try:
- list(
- black.gen_python_files(
- path.iterdir(),
- root,
- include,
- exclude,
- None,
- None,
- report,
- {path: gitignore},
- verbose=False,
- quiet=False,
- )
+
+ regular = MagicMock()
+ outside_root_symlink = MagicMock()
+ ignored_symlink = MagicMock()
+
+ path.iterdir.return_value = [regular, outside_root_symlink, ignored_symlink]
+
+ regular.absolute.return_value = root / "regular.py"
+ regular.resolve.return_value = root / "regular.py"
+ regular.is_dir.return_value = False
+
+ outside_root_symlink.absolute.return_value = root / "symlink.py"
+ outside_root_symlink.resolve.return_value = Path("/nowhere")
+
+ ignored_symlink.absolute.return_value = root / ".mypy_cache" / "symlink.py"
+
+ files = list(
+ black.gen_python_files(
+ path.iterdir(),
+ root,
+ include,
+ exclude,
+ None,
+ None,
+ report,
+ {path: gitignore},
+ verbose=False,
+ quiet=False,
)
- except ValueError as ve:
- pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
+ )
+ assert files == [regular]
+
path.iterdir.assert_called_once()
- child.resolve.assert_called_once()
+ outside_root_symlink.resolve.assert_called_once()
+ ignored_symlink.resolve.assert_not_called()
@patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
def test_get_sources_with_stdin(self) -> None:
)
+class TestDeFactoAPI:
+ """Test that certain symbols that are commonly used externally keep working.
+
+ We don't (yet) formally expose an API (see issue #779), but we should endeavor to
+ keep certain functions that external users commonly rely on working.
+
+ """
+
+ def test_format_str(self) -> None:
+ # format_str and Mode should keep working
+ assert (
+ black.format_str("print('hello')", mode=black.Mode()) == 'print("hello")\n'
+ )
+
+ # you can pass line length
+ assert (
+ black.format_str("print('hello')", mode=black.Mode(line_length=42))
+ == 'print("hello")\n'
+ )
+
+ # invalid input raises InvalidInput
+ with pytest.raises(black.InvalidInput):
+ black.format_str("syntax error", mode=black.Mode())
+
+ def test_format_file_contents(self) -> None:
+ # You probably should be using format_str() instead, but let's keep
+ # this one around since people do use it
+ assert (
+ black.format_file_contents("x=1", fast=True, mode=black.Mode()) == "x = 1\n"
+ )
+
+ with pytest.raises(black.NothingChanged):
+ black.format_file_contents("x = 1\n", fast=True, mode=black.Mode())
+
+
try:
with open(black.__file__, "r", encoding="utf-8") as _bf:
black_source_lines = _bf.readlines()