import click
import pytest
-import regex as re
+import re
from click import unstyle
from click.testing import CliRunner
from pathspec import PathSpec
import black.files
from black import Feature, TargetVersion
from black import re_compile_maybe_verbose as compile_pattern
-from black.cache import get_cache_file
+from black.cache import get_cache_dir, get_cache_file
from black.debug import DebugVisitor
from black.output import color_diff, diff
from black.report import Report
R = TypeVar("R")
# Match the time output in a diff, but nothing else
-DIFF_TIME = re.compile(r"\t[\d-:+\. ]+")
+DIFF_TIME = re.compile(r"\t[\d\-:+\. ]+")
@contextmanager
def __init__(self) -> None:
self.default_map: Dict[str, Any] = {}
+ # Dummy root, since most of the tests don't care about it
+ self.obj: Dict[str, Any] = {"root": PROJECT_ROOT}
class FakeParameter(click.Parameter):
runner = BlackRunner()
if ignore_config:
args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
- result = runner.invoke(black.main, args)
+ result = runner.invoke(black.main, args, catch_exceptions=False)
assert result.stdout_bytes is not None
assert result.stderr_bytes is not None
msg = (
os.unlink(tmp_file)
self.assertFormatEqual(expected, actual)
+ def test_experimental_string_processing_warns(self) -> None:
+ self.assertWarns(
+ black.mode.Deprecated, black.Mode, experimental_string_processing=True
+ )
+
def test_piping(self) -> None:
source, expected = read_data("src/black/__init__", data=False)
result = BlackRunner().invoke(
)
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[1m", actual)
self.assertIn("\033[36m", actual)
self.assertIn("\033[32m", actual)
self.assertIn("\033[31m", actual)
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[1m", actual)
self.assertIn("\033[36m", actual)
self.assertIn("\033[32m", actual)
self.assertIn("\033[31m", actual)
@patch("black.dump_to_file", dump_to_stderr)
def test_string_quotes(self) -> None:
source, expected = read_data("string_quotes")
- mode = black.Mode(experimental_string_processing=True)
+ mode = black.Mode(preview=True)
assert_format(source, expected, mode)
mode = replace(mode, string_normalization=False)
not_normalized = fs(source, mode=mode)
straddling = "x + y"
black.lib2to3_parse(straddling)
- black.lib2to3_parse(straddling, {TargetVersion.PY27})
black.lib2to3_parse(straddling, {TargetVersion.PY36})
- black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36})
py2_only = "print x"
- black.lib2to3_parse(py2_only)
- black.lib2to3_parse(py2_only, {TargetVersion.PY27})
with self.assertRaises(black.InvalidInput):
black.lib2to3_parse(py2_only, {TargetVersion.PY36})
- with self.assertRaises(black.InvalidInput):
- black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36})
py3_only = "exec(x, end=y)"
black.lib2to3_parse(py3_only)
- with self.assertRaises(black.InvalidInput):
- black.lib2to3_parse(py3_only, {TargetVersion.PY27})
black.lib2to3_parse(py3_only, {TargetVersion.PY36})
- black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36})
def test_get_features_used_decorator(self) -> None:
# Test the feature detection of new decorator syntax
self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
node = black.lib2to3_parse("def fn(a, /, b): ...")
self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
+ node = black.lib2to3_parse("def fn(): yield a, b")
+ self.assertEqual(black.get_features_used(node), set())
+ node = black.lib2to3_parse("def fn(): return a, b")
+ self.assertEqual(black.get_features_used(node), set())
+ node = black.lib2to3_parse("def fn(): yield *b, c")
+ self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
+ node = black.lib2to3_parse("def fn(): return a, *b, c")
+ self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
+ node = black.lib2to3_parse("x = a, *b, c")
+ self.assertEqual(black.get_features_used(node), set())
+ node = black.lib2to3_parse("x: Any = regular")
+ self.assertEqual(black.get_features_used(node), set())
+ node = black.lib2to3_parse("x: Any = (regular, regular)")
+ self.assertEqual(black.get_features_used(node), set())
+ node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
+ self.assertEqual(black.get_features_used(node), set())
+ node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
+ self.assertEqual(
+ black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
+ )
+
+ def test_get_features_used_for_future_flags(self) -> None:
+ for src, features in [
+ ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
+ (
+ "from __future__ import (other, annotations)",
+ {Feature.FUTURE_ANNOTATIONS},
+ ),
+ ("a = 1 + 2\nfrom something import annotations", set()),
+ ("from __future__ import x, y", set()),
+ ]:
+ with self.subTest(src=src, features=features):
+ node = black.lib2to3_parse(src)
+ future_imports = black.get_future_imports(node)
+ self.assertEqual(
+ black.get_features_used(node, future_imports=future_imports),
+ features,
+ )
def test_get_future_imports(self) -> None:
node = black.lib2to3_parse("\n")
)
self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
+ @pytest.mark.incompatible_with_mypyc
def test_debug_visitor(self) -> None:
source, _ = read_data("debug_visitor.py")
expected, _ = read_data("debug_visitor.out")
self.assertEqual(len(n.children), 1)
self.assertEqual(n.children[0].type, black.token.ENDMARKER)
+ @pytest.mark.incompatible_with_mypyc
@unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
def test_assertFormatEqual(self) -> None:
out_lines = []
self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
out_str = "".join(out_lines)
- self.assertTrue("Expected tree:" in out_str)
- self.assertTrue("Actual tree:" in out_str)
+ self.assertIn("Expected tree:", out_str)
+ self.assertIn("Actual tree:", out_str)
self.assertEqual("".join(err_lines), "")
@event_loop()
# Multi file command.
self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
- def test_no_files(self) -> None:
+ def test_no_src_fails(self) -> None:
with cache_dir():
- # Without an argument, black exits with error code 0.
- self.invokeBlack([])
+ self.invokeBlack([], exit_code=1)
+
+ def test_src_and_code_fails(self) -> None:
+ with cache_dir():
+ self.invokeBlack([".", "-c", "0"], exit_code=1)
def test_broken_symlink(self) -> None:
with cache_dir() as workspace:
actual = result.output
self.assertFormatEqual(actual, expected)
+ @pytest.mark.incompatible_with_mypyc
def test_reformat_one_with_stdin(self) -> None:
with patch(
"black.format_stdin_to_stdout",
fsts.assert_called_once()
report.done.assert_called_with(path, black.Changed.YES)
+ @pytest.mark.incompatible_with_mypyc
def test_reformat_one_with_stdin_filename(self) -> None:
with patch(
"black.format_stdin_to_stdout",
# __BLACK_STDIN_FILENAME__ should have been stripped
report.done.assert_called_with(expected, black.Changed.YES)
+ @pytest.mark.incompatible_with_mypyc
def test_reformat_one_with_stdin_filename_pyi(self) -> None:
with patch(
"black.format_stdin_to_stdout",
# __BLACK_STDIN_FILENAME__ should have been stripped
report.done.assert_called_with(expected, black.Changed.YES)
+ @pytest.mark.incompatible_with_mypyc
def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
with patch(
"black.format_stdin_to_stdout",
# __BLACK_STDIN_FILENAME__ should have been stripped
report.done.assert_called_with(expected, black.Changed.YES)
+ @pytest.mark.incompatible_with_mypyc
def test_reformat_one_with_stdin_and_existing_path(self) -> None:
with patch(
"black.format_stdin_to_stdout",
def test_required_version_matches_version(self) -> None:
self.invokeBlack(
- ["--required-version", black.__version__], exit_code=0, ignore_config=True
+ ["--required-version", black.__version__, "-c", "0"],
+ 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
+ result = BlackRunner().invoke(
+ black.main,
+ ["--required-version", "20.99b", "-c", "0"],
)
+ self.assertEqual(result.exit_code, 1)
+ self.assertIn("required version", result.stderr)
def test_preserves_line_endings(self) -> None:
with TemporaryDirectory() as workspace:
self.assertEqual(config["color"], True)
self.assertEqual(config["line_length"], 79)
self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
+ self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
self.assertEqual(config["exclude"], r"\.pyi?$")
self.assertEqual(config["include"], r"\.py?$")
self.assertEqual(config["exclude"], r"\.pyi?$")
self.assertEqual(config["include"], r"\.py?$")
+ @pytest.mark.incompatible_with_mypyc
def test_find_project_root(self) -> None:
with TemporaryDirectory() as workspace:
root = Path(workspace)
src_python.touch()
self.assertEqual(
- black.find_project_root((src_dir, test_dir)), root.resolve()
+ black.find_project_root((src_dir, test_dir)),
+ (root.resolve(), "pyproject.toml"),
+ )
+ self.assertEqual(
+ black.find_project_root((src_dir,)),
+ (src_dir.resolve(), "pyproject.toml"),
+ )
+ self.assertEqual(
+ black.find_project_root((src_python,)),
+ (src_dir.resolve(), "pyproject.toml"),
)
- 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",
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 = BlackRunner().invoke(
- black.main,
- ["-", "-q", "--target-version=py27"],
- input=BytesIO(source),
- )
-
- self.assertEqual(result.exit_code, 0)
- actual = result.stdout
- self.assertFormatEqual(actual, expected)
-
@staticmethod
def compare_results(
result: click.testing.Result, expected_value: str, expected_exit_code: int
assert output == result_diff, "The output did not match the expected value."
assert result.exit_code == 0, "The exit code is incorrect."
+ @pytest.mark.incompatible_with_mypyc
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
self.compare_results(result, formatted, 0)
+ @pytest.mark.incompatible_with_mypyc
def test_code_option_config(self) -> None:
"""
Test that the code option finds the pyproject.toml in the current directory.
call_args[0].lower() == str(pyproject_path).lower()
), "Incorrect config loaded."
+ @pytest.mark.incompatible_with_mypyc
def test_code_option_parent_config(self) -> None:
"""
Test that the code option finds the pyproject.toml in the parent directory.
call_args[0].lower() == str(pyproject_path).lower()
), "Incorrect config loaded."
+ def test_for_handled_unexpected_eof_error(self) -> None:
+ """
+ Test that an unexpected EOF SyntaxError is nicely presented.
+ """
+ with pytest.raises(black.parsing.InvalidInput) as exc_info:
+ black.lib2to3_parse("print(", {})
+
+ exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
+
+ def test_equivalency_ast_parse_failure_includes_error(self) -> None:
+ with pytest.raises(AssertionError) as err:
+ black.assert_equivalent("a«»a = 1", "a«»a = 1")
+
+ err.match("--safe")
+ # Unfortunately the SyntaxError message has changed in newer versions so we
+ # can't match it directly.
+ err.match("invalid character")
+ err.match(r"\(<unknown>, line 1\)")
+
class TestCaching:
+ def test_get_cache_dir(
+ self,
+ tmp_path: Path,
+ monkeypatch: pytest.MonkeyPatch,
+ ) -> None:
+ # Create multiple cache directories
+ workspace1 = tmp_path / "ws1"
+ workspace1.mkdir()
+ workspace2 = tmp_path / "ws2"
+ workspace2.mkdir()
+
+ # Force user_cache_dir to use the temporary directory for easier assertions
+ patch_user_cache_dir = patch(
+ target="black.cache.user_cache_dir",
+ autospec=True,
+ return_value=str(workspace1),
+ )
+
+ # If BLACK_CACHE_DIR is not set, use user_cache_dir
+ monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
+ with patch_user_cache_dir:
+ assert get_cache_dir() == workspace1
+
+ # If it is set, use the path provided in the env var.
+ monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
+ assert get_cache_dir() == workspace2
+
def test_cache_broken_file(self) -> None:
mode = DEFAULT_MODE
with cache_dir() as workspace:
src: Sequence[Union[str, Path]],
expected: Sequence[Union[str, Path]],
*,
+ ctx: Optional[FakeContext] = 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=FakeContext(),
+ ctx=ctx or FakeContext(),
src=gs_src,
quiet=False,
verbose=False,
report=black.Report(),
stdin_filename=stdin_filename,
)
- assert sorted(list(collected)) == sorted(gs_expected)
+ assert sorted(collected) == sorted(gs_expected)
class TestFileCollection:
base / "b/.definitely_exclude/a.pyi",
]
src = [base / "b/"]
- assert_collected_sources(src, expected, extend_exclude=r"/exclude/")
+ ctx = FakeContext()
+ ctx.obj["root"] = base
+ assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
- @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+ @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
# CLI. Exclude is supposed to only apply to the recursive discovery of files.
src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
)
+ @pytest.mark.incompatible_with_mypyc
def test_symlink_out_of_root_directory(self) -> None:
path = MagicMock()
root = THIS_DIR.resolve()
child.is_symlink.assert_called()
assert child.is_symlink.call_count == 2
- @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+ @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
def test_get_sources_with_stdin(self) -> None:
src = ["-"]
expected = ["-"]
assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
- @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+ @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
def test_get_sources_with_stdin_filename(self) -> None:
src = ["-"]
stdin_filename = str(THIS_DIR / "data/collections.py")
stdin_filename=stdin_filename,
)
- @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+ @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
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
stdin_filename=stdin_filename,
)
- @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+ @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
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
stdin_filename=stdin_filename,
)
- @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
+ @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
# Force exclude should exclude the file when passing it through
# stdin_filename
)
-@pytest.mark.python2
-@pytest.mark.parametrize("explicit", [True, False], ids=["explicit", "autodetection"])
-def test_python_2_deprecation_with_target_version(explicit: bool) -> None:
- args = [
- "--config",
- str(THIS_DIR / "empty.toml"),
- str(DATA_DIR / "python2.py"),
- "--check",
- ]
- if explicit:
- args.append("--target-version=py27")
- with cache_dir():
- result = BlackRunner().invoke(black.main, args)
- assert "DEPRECATION: Python 2 support will be removed" in result.stderr
-
-
-@pytest.mark.python2
-def test_python_2_deprecation_autodetection_extended() -> None:
- # this test has a similar construction to test_get_features_used_decorator
- python2, non_python2 = read_data("python2_detection")
- for python2_case in python2.split("###"):
- node = black.lib2to3_parse(python2_case)
- assert black.detect_target_versions(node) == {TargetVersion.PY27}, python2_case
- for non_python2_case in non_python2.split("###"):
- node = black.lib2to3_parse(non_python2_case)
- assert black.detect_target_versions(node) != {
- TargetVersion.PY27
- }, non_python2_case
-
-
-with open(black.__file__, "r", encoding="utf-8") as _bf:
- black_source_lines = _bf.readlines()
+try:
+ with open(black.__file__, "r", encoding="utf-8") as _bf:
+ black_source_lines = _bf.readlines()
+except UnicodeDecodeError:
+ if not black.COMPILED:
+ raise
def tracefunc(