import logging
import multiprocessing
import os
+import re
import sys
import types
import unittest
import click
import pytest
-import re
from click import unstyle
from click.testing import CliRunner
from pathspec import PathSpec
dump_to_stderr,
ff,
fs,
- read_data,
get_case_path,
+ read_data,
read_data_from_file,
)
versions = black.detect_target_versions(root)
self.assertIn(black.TargetVersion.PY38, versions)
+ def test_detect_debug_f_strings(self) -> None:
+ root = black.lib2to3_parse("""f"{x=}" """)
+ features = black.get_features_used(root)
+ self.assertIn(black.Feature.DEBUG_F_STRINGS, features)
+ versions = black.detect_target_versions(root)
+ self.assertIn(black.TargetVersion.PY38, versions)
+
+ root = black.lib2to3_parse(
+ """f"{x}"\nf'{"="}'\nf'{(x:=5)}'\nf'{f(a="3=")}'\nf'{x:=10}'\n"""
+ )
+ features = black.get_features_used(root)
+ self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
+
+ # We don't yet support feature version detection in nested f-strings
+ root = black.lib2to3_parse(
+ """f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """
+ )
+ features = black.get_features_used(root)
+ self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
+
@patch("black.dump_to_file", dump_to_stderr)
def test_string_quotes(self) -> None:
source, expected = read_data("miscellaneous", "string_quotes")
black.assert_equivalent(source, not_normalized)
black.assert_stable(source, not_normalized, mode=mode)
+ def test_skip_source_first_line(self) -> None:
+ source, _ = read_data("miscellaneous", "invalid_header")
+ tmp_file = Path(black.dump_to_file(source))
+ # Full source should fail (invalid syntax at header)
+ self.invokeBlack([str(tmp_file), "--diff", "--check"], exit_code=123)
+ # So, skipping the first line should work
+ result = BlackRunner().invoke(
+ black.main, [str(tmp_file), "-x", f"--config={EMPTY_CONFIG}"]
+ )
+ self.assertEqual(result.exit_code, 0)
+ with open(tmp_file, encoding="utf8") as f:
+ actual = f.read()
+ self.assertFormatEqual(source, actual)
+
+ def test_skip_source_first_line_when_mixing_newlines(self) -> None:
+ code_mixing_newlines = b"Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n"
+ expected = b"Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n"
+ with TemporaryDirectory() as workspace:
+ test_file = Path(workspace) / "skip_header.py"
+ test_file.write_bytes(code_mixing_newlines)
+ mode = replace(DEFAULT_MODE, skip_source_first_line=True)
+ ff(test_file, mode=mode, write_back=black.WriteBack.YES)
+ self.assertEqual(test_file.read_bytes(), expected)
+
def test_skip_magic_trailing_comma(self) -> None:
source, _ = read_data("simple_cases", "expression")
expected, _ = read_data(
self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
self.assertEqual(
unstyle(str(report)),
- "1 file reformatted, 2 files left unchanged, 1 file failed to"
- " reformat.",
+ (
+ "1 file reformatted, 2 files left unchanged, 1 file failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.done(Path("f3"), black.Changed.YES)
self.assertEqual(out_lines[-1], "reformatted f3")
self.assertEqual(
unstyle(str(report)),
- "2 files reformatted, 2 files left unchanged, 1 file failed to"
- " reformat.",
+ (
+ "2 files reformatted, 2 files left unchanged, 1 file failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.failed(Path("e2"), "boom")
self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
self.assertEqual(
unstyle(str(report)),
- "2 files reformatted, 2 files left unchanged, 2 files failed to"
- " reformat.",
+ (
+ "2 files reformatted, 2 files left unchanged, 2 files failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.path_ignored(Path("wat"), "no match")
self.assertEqual(out_lines[-1], "wat ignored: no match")
self.assertEqual(
unstyle(str(report)),
- "2 files reformatted, 2 files left unchanged, 2 files failed to"
- " reformat.",
+ (
+ "2 files reformatted, 2 files left unchanged, 2 files failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.done(Path("f4"), black.Changed.NO)
self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
self.assertEqual(
unstyle(str(report)),
- "2 files reformatted, 3 files left unchanged, 2 files failed to"
- " reformat.",
+ (
+ "2 files reformatted, 3 files left unchanged, 2 files failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.check = True
self.assertEqual(
unstyle(str(report)),
- "2 files would be reformatted, 3 files would be left unchanged, 2 files"
- " would fail to reformat.",
+ (
+ "2 files would be reformatted, 3 files would be left unchanged, 2"
+ " files would fail to reformat."
+ ),
)
report.check = False
report.diff = True
self.assertEqual(
unstyle(str(report)),
- "2 files would be reformatted, 3 files would be left unchanged, 2 files"
- " would fail to reformat.",
+ (
+ "2 files would be reformatted, 3 files would be left unchanged, 2"
+ " files would fail to reformat."
+ ),
)
def test_report_quiet(self) -> None:
self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
self.assertEqual(
unstyle(str(report)),
- "1 file reformatted, 2 files left unchanged, 1 file failed to"
- " reformat.",
+ (
+ "1 file reformatted, 2 files left unchanged, 1 file failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.done(Path("f3"), black.Changed.YES)
self.assertEqual(len(err_lines), 1)
self.assertEqual(
unstyle(str(report)),
- "2 files reformatted, 2 files left unchanged, 1 file failed to"
- " reformat.",
+ (
+ "2 files reformatted, 2 files left unchanged, 1 file failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.failed(Path("e2"), "boom")
self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
self.assertEqual(
unstyle(str(report)),
- "2 files reformatted, 2 files left unchanged, 2 files failed to"
- " reformat.",
+ (
+ "2 files reformatted, 2 files left unchanged, 2 files failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.path_ignored(Path("wat"), "no match")
self.assertEqual(len(err_lines), 2)
self.assertEqual(
unstyle(str(report)),
- "2 files reformatted, 2 files left unchanged, 2 files failed to"
- " reformat.",
+ (
+ "2 files reformatted, 2 files left unchanged, 2 files failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.done(Path("f4"), black.Changed.NO)
self.assertEqual(len(err_lines), 2)
self.assertEqual(
unstyle(str(report)),
- "2 files reformatted, 3 files left unchanged, 2 files failed to"
- " reformat.",
+ (
+ "2 files reformatted, 3 files left unchanged, 2 files failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.check = True
self.assertEqual(
unstyle(str(report)),
- "2 files would be reformatted, 3 files would be left unchanged, 2 files"
- " would fail to reformat.",
+ (
+ "2 files would be reformatted, 3 files would be left unchanged, 2"
+ " files would fail to reformat."
+ ),
)
report.check = False
report.diff = True
self.assertEqual(
unstyle(str(report)),
- "2 files would be reformatted, 3 files would be left unchanged, 2 files"
- " would fail to reformat.",
+ (
+ "2 files would be reformatted, 3 files would be left unchanged, 2"
+ " files would fail to reformat."
+ ),
)
def test_report_normal(self) -> None:
self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
self.assertEqual(
unstyle(str(report)),
- "1 file reformatted, 2 files left unchanged, 1 file failed to"
- " reformat.",
+ (
+ "1 file reformatted, 2 files left unchanged, 1 file failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.done(Path("f3"), black.Changed.YES)
self.assertEqual(out_lines[-1], "reformatted f3")
self.assertEqual(
unstyle(str(report)),
- "2 files reformatted, 2 files left unchanged, 1 file failed to"
- " reformat.",
+ (
+ "2 files reformatted, 2 files left unchanged, 1 file failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.failed(Path("e2"), "boom")
self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
self.assertEqual(
unstyle(str(report)),
- "2 files reformatted, 2 files left unchanged, 2 files failed to"
- " reformat.",
+ (
+ "2 files reformatted, 2 files left unchanged, 2 files failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.path_ignored(Path("wat"), "no match")
self.assertEqual(len(err_lines), 2)
self.assertEqual(
unstyle(str(report)),
- "2 files reformatted, 2 files left unchanged, 2 files failed to"
- " reformat.",
+ (
+ "2 files reformatted, 2 files left unchanged, 2 files failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.done(Path("f4"), black.Changed.NO)
self.assertEqual(len(err_lines), 2)
self.assertEqual(
unstyle(str(report)),
- "2 files reformatted, 3 files left unchanged, 2 files failed to"
- " reformat.",
+ (
+ "2 files reformatted, 3 files left unchanged, 2 files failed to"
+ " reformat."
+ ),
)
self.assertEqual(report.return_code, 123)
report.check = True
self.assertEqual(
unstyle(str(report)),
- "2 files would be reformatted, 3 files would be left unchanged, 2 files"
- " would fail to reformat.",
+ (
+ "2 files would be reformatted, 3 files would be left unchanged, 2"
+ " files would fail to reformat."
+ ),
)
report.check = False
report.diff = True
self.assertEqual(
unstyle(str(report)),
- "2 files would be reformatted, 3 files would be left unchanged, 2 files"
- " would fail to reformat.",
+ (
+ "2 files would be reformatted, 3 files would be left unchanged, 2"
+ " files would fail to reformat."
+ ),
)
def test_lib2to3_parse(self) -> None:
self.assertEqual(black.get_features_used(node), set())
node = black.lib2to3_parse("try: pass\nexcept *Group: pass")
self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR})
+ node = black.lib2to3_parse("a[*b]")
+ self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
+ node = black.lib2to3_parse("a[x, *y(), z] = t")
+ self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
+ node = black.lib2to3_parse("def fn(*args: *T): pass")
+ self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
def test_get_features_used_for_future_flags(self) -> None:
for src, features in [
if nl == "\n":
self.assertNotIn(b"\r\n", output)
+ def test_normalize_line_endings(self) -> None:
+ with TemporaryDirectory() as workspace:
+ test_file = Path(workspace) / "test.py"
+ for data, expected in (
+ (b"c\r\nc\n ", b"c\r\nc\r\n"),
+ (b"l\nl\r\n ", b"l\nl\n"),
+ ):
+ test_file.write_bytes(data)
+ ff(test_file, write_back=black.WriteBack.YES)
+ self.assertEqual(test_file.read_bytes(), expected)
+
def test_assert_equivalent_different_asts(self) -> None:
with self.assertRaises(AssertionError):
black.assert_equivalent("{}", "None")
(src_dir.resolve(), "pyproject.toml"),
)
+ with change_directory(test_dir):
+ self.assertEqual(
+ black.find_project_root(("-",), stdin_filename="../src/a.py"),
+ (src_dir.resolve(), "pyproject.toml"),
+ )
+
@patch(
"black.files.find_user_pyproject_toml",
)
black.assert_stable(source, output, mode=DEFAULT_MODE)
def test_bpo_2142_workaround(self) -> None:
-
# https://bugs.python.org/issue2142
source, _ = read_data("miscellaneous", "missing_final_newline")
src = (workspace / f"test{tag}.py").resolve()
with src.open("w") as fobj:
fobj.write("print('hello')")
- with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
+ with patch(
+ "black.concurrency.Manager", wraps=multiprocessing.Manager
+ ) as mgr:
cmd = ["--diff", str(workspace)]
if color:
cmd.append("--color")
str(cached): black.get_cache_info(cached),
str(cached_but_changed): (0.0, 0),
}
- todo, done = black.filter_cached(
+ todo, done = black.cache.filter_cached(
cache, {uncached, cached, cached_but_changed}
)
assert todo == {uncached, cached_but_changed}
ctx.obj["root"] = base
assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
+ def test_gitignore_used_on_multiple_sources(self) -> None:
+ root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
+ expected = [
+ 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)
+
@patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
def test_exclude_for_issue_1572(self) -> None:
# Exclude shouldn't touch files that were explicitly given to Black through the
None,
None,
report,
- gitignore,
+ {path: gitignore},
verbose=False,
quiet=False,
)
None,
None,
report,
- root_gitignore,
+ {path: root_gitignore},
verbose=False,
quiet=False,
)
)
assert sorted(expected) == sorted(sources)
+ def test_nested_gitignore_directly_in_source_directory(self) -> None:
+ # https://github.com/psf/black/issues/2598
+ path = Path(DATA_DIR / "nested_gitignore_tests")
+ src = Path(path / "root" / "child")
+ expected = [src / "a.py", src / "c.py"]
+ assert_collected_sources([src], expected)
+
def test_invalid_gitignore(self) -> None:
path = THIS_DIR / "data" / "invalid_gitignore_tests"
empty_config = path / "pyproject.toml"
gitignore = path / "a" / ".gitignore"
assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
+ def test_gitignore_that_ignores_subfolders(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)
+
+ # If .gitignore with */* is nested
+ root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
+ expected = [
+ root / "a.py",
+ root / "subdir" / "b.py",
+ ]
+ ctx = FakeContext()
+ ctx.obj["root"] = root
+ assert_collected_sources([root], expected, ctx=ctx)
+
+ # 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)
+
def test_empty_include(self) -> None:
path = DATA_DIR / "include_exclude_tests"
src = [path]
None,
None,
report,
- gitignore,
+ {path: gitignore},
verbose=False,
quiet=False,
)