#!/usr/bin/env python3
-import multiprocessing
+
import asyncio
+import inspect
+import io
import logging
+import multiprocessing
+import os
+import sys
+import types
+import unittest
from concurrent.futures import ThreadPoolExecutor
-from contextlib import contextmanager
+from contextlib import contextmanager, redirect_stderr
from dataclasses import replace
-import inspect
-import io
from io import BytesIO
-import os
from pathlib import Path
from platform import system
-import regex as re
-import sys
from tempfile import TemporaryDirectory
-import types
from typing import (
Any,
Callable,
Dict,
- List,
Iterator,
+ List,
+ Optional,
+ Sequence,
TypeVar,
+ Union,
)
-import pytest
-import unittest
-from unittest.mock import patch, MagicMock
-from parameterized import parameterized
+from unittest.mock import MagicMock, patch
import click
+import pytest
+import re
from click import unstyle
from click.testing import CliRunner
+from pathspec import PathSpec
import black
+import black.files
from black import Feature, TargetVersion
-from black.cache import get_cache_file
+from black import re_compile_maybe_verbose as compile_pattern
+from black.cache import get_cache_dir, get_cache_file
from black.debug import DebugVisitor
-from black.output import diff, color_diff
+from black.output import color_diff, 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,
+ DATA_DIR,
+ DEFAULT_MODE,
DETERMINISTIC_HEADER,
+ PROJECT_ROOT,
+ PY36_VERSIONS,
+ THIS_DIR,
BlackBaseTestCase,
- DEFAULT_MODE,
- fs,
- ff,
+ assert_format,
+ change_directory,
dump_to_stderr,
+ ff,
+ fs,
+ read_data,
)
-
THIS_FILE = Path(__file__)
-PY36_VERSIONS = {
- TargetVersion.PY36,
- TargetVersion.PY37,
- TargetVersion.PY38,
- TargetVersion.PY39,
-}
+EMPTY_CONFIG = THIS_DIR / "data" / "empty_pyproject.toml"
PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
+DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
+DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
T = TypeVar("T")
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):
super().__init__(mix_stderr=False)
-class BlackTestCase(BlackBaseTestCase):
- def invokeBlack(
- self, args: List[str], exit_code: int = 0, ignore_config: bool = True
- ) -> None:
- runner = BlackRunner()
- 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: {result.stdout_bytes.decode()!r}\n"
- f"stderr: {result.stderr_bytes.decode()!r}\n"
- f"exception: {result.exception}"
- ),
- )
+def invokeBlack(
+ args: List[str], exit_code: int = 0, ignore_config: bool = True
+) -> None:
+ runner = BlackRunner()
+ if ignore_config:
+ args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *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 = (
+ f"Failed with args: {args}\n"
+ f"stdout: {result.stdout_bytes.decode()!r}\n"
+ f"stderr: {result.stderr_bytes.decode()!r}\n"
+ f"exception: {result.exception}"
+ )
+ assert result.exit_code == exit_code, msg
- @patch("black.dump_to_file", dump_to_stderr)
- def test_empty(self) -> None:
- source = expected = ""
- actual = fs(source)
- self.assertFormatEqual(expected, actual)
- black.assert_equivalent(source, actual)
- black.assert_stable(source, actual, DEFAULT_MODE)
+
+class BlackTestCase(BlackBaseTestCase):
+ invokeBlack = staticmethod(invokeBlack)
def test_empty_ff(self) -> None:
expected = ""
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(
black.main,
- ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
+ [
+ "-",
+ "--fast",
+ f"--line-length={black.DEFAULT_LINE_LENGTH}",
+ f"--config={EMPTY_CONFIG}",
+ ],
input=BytesIO(source.encode("utf8")),
)
self.assertEqual(result.exit_code, 0)
)
source, _ = read_data("expression.py")
expected, _ = read_data("expression.diff")
- config = THIS_DIR / "data" / "empty_pyproject.toml"
args = [
"-",
"--fast",
f"--line-length={black.DEFAULT_LINE_LENGTH}",
"--diff",
- f"--config={config}",
+ f"--config={EMPTY_CONFIG}",
]
result = BlackRunner().invoke(
black.main, args, input=BytesIO(source.encode("utf8"))
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}",
+ f"--config={EMPTY_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[1m", actual)
self.assertIn("\033[36m", actual)
self.assertIn("\033[32m", actual)
self.assertIn("\033[31m", actual)
black.assert_equivalent(source, actual)
black.assert_stable(source, actual, black.FileMode())
- @unittest.expectedFailure
- @patch("black.dump_to_file", dump_to_stderr)
- def test_trailing_comma_optional_parens_stability1(self) -> None:
- source, _expected = read_data("trailing_comma_optional_parens1")
- actual = fs(source)
- black.assert_stable(source, actual, DEFAULT_MODE)
-
- @unittest.expectedFailure
- @patch("black.dump_to_file", dump_to_stderr)
- def test_trailing_comma_optional_parens_stability2(self) -> None:
- source, _expected = read_data("trailing_comma_optional_parens2")
- actual = fs(source)
- black.assert_stable(source, actual, DEFAULT_MODE)
-
- @unittest.expectedFailure
- @patch("black.dump_to_file", dump_to_stderr)
- def test_trailing_comma_optional_parens_stability3(self) -> None:
- source, _expected = read_data("trailing_comma_optional_parens3")
- 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")
- 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_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(
)
try:
result = BlackRunner().invoke(
- black.main, ["--diff", str(tmp_file), f"--config={config}"]
+ black.main, ["--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
)
self.assertEqual(result.exit_code, 0)
finally:
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), f"--config={config}"]
+ black.main,
+ ["--diff", "--color", str(tmp_file), f"--config={EMPTY_CONFIG}"],
)
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[1m", 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)
- def test_pep_570(self) -> None:
- source, expected = read_data("pep_570")
- 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)
-
def test_detect_pos_only_arguments(self) -> None:
source, _ = read_data("pep_570")
root = black.lib2to3_parse(source)
@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)
- actual = fs(source, mode=mode)
- self.assertFormatEqual(expected, actual)
- black.assert_equivalent(source, actual)
- black.assert_stable(source, actual, mode)
+ mode = black.Mode(preview=True)
+ assert_format(source, expected, 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_stable(source, not_normalized, mode=mode)
- @patch("black.dump_to_file", dump_to_stderr)
- def test_docstring_no_string_normalization(self) -> None:
- """Like test_docstring but with string normalization off."""
- source, expected = read_data("docstring_no_string_normalization")
- mode = replace(DEFAULT_MODE, string_normalization=False)
- actual = fs(source, mode=mode)
- self.assertFormatEqual(expected, actual)
- black.assert_equivalent(source, actual)
- black.assert_stable(source, actual, mode)
-
- def test_long_strings_flag_disabled(self) -> None:
- """Tests for turning off the string processing logic."""
- source, expected = read_data("long_strings_flag_disabled")
- mode = replace(DEFAULT_MODE, experimental_string_processing=False)
- actual = fs(source, mode=mode)
- self.assertFormatEqual(expected, actual)
- black.assert_stable(expected, actual, mode)
-
- @patch("black.dump_to_file", dump_to_stderr)
- def test_numeric_literals(self) -> None:
- source, expected = read_data("numeric_literals")
- mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
- actual = fs(source, mode=mode)
- self.assertFormatEqual(expected, actual)
- black.assert_equivalent(source, actual)
- black.assert_stable(source, actual, mode)
-
- @patch("black.dump_to_file", dump_to_stderr)
- def test_numeric_literals_ignoring_underscores(self) -> None:
- source, expected = read_data("numeric_literals_skip_underscores")
- mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
- actual = fs(source, mode=mode)
- self.assertFormatEqual(expected, actual)
- 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")
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)])
+ result = BlackRunner().invoke(
+ black.main, ["-C", "--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
+ )
self.assertEqual(result.exit_code, 0)
finally:
os.unlink(tmp_file)
)
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")
- mode = replace(DEFAULT_MODE, target_versions={TargetVersion.PY27})
- actual = fs(source, mode=mode)
- self.assertFormatEqual(expected, actual)
- black.assert_equivalent(source, actual)
- black.assert_stable(source, actual, mode)
-
- @patch("black.dump_to_file", dump_to_stderr)
- def test_stub(self) -> None:
- mode = replace(DEFAULT_MODE, is_pyi=True)
- source, expected = read_data("stub.pyi")
- actual = fs(source, mode=mode)
- self.assertFormatEqual(expected, actual)
- black.assert_stable(source, actual, mode)
-
@patch("black.dump_to_file", dump_to_stderr)
def test_async_as_identifier(self) -> None:
source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
# but not on 3.6, because we use async as a reserved keyword
self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
- @patch("black.dump_to_file", dump_to_stderr)
- def test_python38(self) -> None:
- source, expected = read_data("python38")
- actual = fs(source)
- self.assertFormatEqual(expected, actual)
- major, minor = sys.version_info[:2]
- if major > 3 or (major == 3 and minor >= 8):
- black.assert_equivalent(source, actual)
- black.assert_stable(source, actual, DEFAULT_MODE)
-
- @patch("black.dump_to_file", dump_to_stderr)
- def test_python39(self) -> None:
- source, expected = read_data("python39")
- actual = fs(source)
- self.assertFormatEqual(expected, actual)
- major, minor = sys.version_info[:2]
- if major > 3 or (major == 3 and minor >= 9):
- black.assert_equivalent(source, actual)
- black.assert_stable(source, actual, DEFAULT_MODE)
-
def test_tab_comment_indentation(self) -> None:
contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
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), set())
node = black.lib2to3_parse(expected)
self.assertEqual(black.get_features_used(node), set())
+ node = black.lib2to3_parse("lambda a, /, b: ...")
+ 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), "")
- def test_cache_broken_file(self) -> None:
- mode = DEFAULT_MODE
- with cache_dir() as workspace:
- 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), {})
- src = (workspace / "test.py").resolve()
- with src.open("w") as fobj:
- fobj.write("print('hello')")
- self.invokeBlack([str(src)])
- cache = black.read_cache(mode)
- self.assertIn(str(src), cache)
-
- def test_cache_single_file_already_cached(self) -> None:
- mode = DEFAULT_MODE
+ @event_loop()
+ @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
+ def test_works_in_mono_process_only_environment(self) -> None:
with cache_dir() as workspace:
- src = (workspace / "test.py").resolve()
- with src.open("w") as fobj:
- fobj.write("print('hello')")
- black.write_cache({}, [src], mode)
- self.invokeBlack([str(src)])
- with src.open("r") as fobj:
- self.assertEqual(fobj.read(), "print('hello')")
+ for f in [
+ (workspace / "one.py").resolve(),
+ (workspace / "two.py").resolve(),
+ ]:
+ f.write_text('print("hello")\n')
+ self.invokeBlack([str(workspace)])
@event_loop()
- def test_cache_multiple_files(self) -> None:
- mode = DEFAULT_MODE
- with cache_dir() as workspace, patch(
- "black.ProcessPoolExecutor", new=ThreadPoolExecutor
- ):
- one = (workspace / "one.py").resolve()
- with one.open("w") as fobj:
- fobj.write("print('hello')")
- two = (workspace / "two.py").resolve()
- with two.open("w") as fobj:
- fobj.write("print('hello')")
- black.write_cache({}, [one], mode)
- self.invokeBlack([str(workspace)])
- with one.open("r") as fobj:
- self.assertEqual(fobj.read(), "print('hello')")
- with two.open("r") as fobj:
- self.assertEqual(fobj.read(), 'print("hello")\n')
- cache = black.read_cache(mode)
- self.assertIn(str(one), cache)
- self.assertIn(str(two), cache)
+ def test_check_diff_use_together(self) -> None:
+ with cache_dir():
+ # Files which will be reformatted.
+ src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
+ self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
+ # Files which will not be reformatted.
+ src2 = (THIS_DIR / "data" / "composition.py").resolve()
+ self.invokeBlack([str(src2), "--diff", "--check"])
+ # Multi file command.
+ self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
- def test_no_cache_when_writeback_diff(self) -> None:
- mode = DEFAULT_MODE
- with cache_dir() as workspace:
- src = (workspace / "test.py").resolve()
- with src.open("w") as fobj:
- fobj.write("print('hello')")
- with patch("black.read_cache") as read_cache, patch(
- "black.write_cache"
- ) as write_cache:
- self.invokeBlack([str(src), "--diff"])
- cache_file = get_cache_file(mode)
- self.assertFalse(cache_file.exists())
- write_cache.assert_not_called()
- read_cache.assert_not_called()
+ def test_no_src_fails(self) -> None:
+ with cache_dir():
+ self.invokeBlack([], exit_code=1)
- def test_no_cache_when_writeback_color_diff(self) -> None:
- mode = DEFAULT_MODE
- with cache_dir() as workspace:
- src = (workspace / "test.py").resolve()
- with src.open("w") as fobj:
- fobj.write("print('hello')")
- with patch("black.read_cache") as read_cache, patch(
- "black.write_cache"
- ) as write_cache:
- self.invokeBlack([str(src), "--diff", "--color"])
- cache_file = get_cache_file(mode)
- self.assertFalse(cache_file.exists())
- write_cache.assert_not_called()
- read_cache.assert_not_called()
+ def test_src_and_code_fails(self) -> None:
+ with cache_dir():
+ self.invokeBlack([".", "-c", "0"], exit_code=1)
- @event_loop()
- def test_output_locking_when_writeback_diff(self) -> None:
+ def test_broken_symlink(self) -> None:
with cache_dir() as workspace:
- for tag in range(0, 4):
- 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:
- self.invokeBlack(["--diff", str(workspace)], exit_code=0)
- # this isn't quite doing what we want, but if it _isn't_
- # called then we cannot be using the lock it provides
- mgr.assert_called()
+ symlink = workspace / "broken_link.py"
+ try:
+ symlink.symlink_to("nonexistent.py")
+ except (OSError, NotImplementedError) as e:
+ self.skipTest(f"Can't create symlinks: {e}")
+ self.invokeBlack([str(workspace.resolve())])
- @event_loop()
- def test_output_locking_when_writeback_color_diff(self) -> None:
- with cache_dir() as workspace:
- for tag in range(0, 4):
- 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:
- self.invokeBlack(["--diff", "--color", str(workspace)], exit_code=0)
- # this isn't quite doing what we want, but if it _isn't_
- # called then we cannot be using the lock it provides
- mgr.assert_called()
-
- def test_no_cache_when_stdin(self) -> None:
- mode = DEFAULT_MODE
- with cache_dir():
- result = CliRunner().invoke(
- black.main, ["-"], input=BytesIO(b"print('hello')")
- )
- self.assertEqual(result.exit_code, 0)
- cache_file = get_cache_file(mode)
- self.assertFalse(cache_file.exists())
-
- def test_read_cache_no_cachefile(self) -> None:
- mode = DEFAULT_MODE
- with cache_dir():
- self.assertEqual(black.read_cache(mode), {})
-
- 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)
- 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:
- path = Path(workspace)
- uncached = (path / "uncached").resolve()
- cached = (path / "cached").resolve()
- cached_but_changed = (path / "changed").resolve()
- 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.filter_cached(
- cache, {uncached, cached, cached_but_changed}
- )
- self.assertEqual(todo, {uncached, cached_but_changed})
- self.assertEqual(done, {cached})
-
- def test_write_cache_creates_directory_if_needed(self) -> None:
- mode = DEFAULT_MODE
- with cache_dir(exists=False) as workspace:
- self.assertFalse(workspace.exists())
- black.write_cache({}, [], mode)
- self.assertTrue(workspace.exists())
-
- @event_loop()
- def test_failed_formatting_does_not_get_cached(self) -> None:
- mode = DEFAULT_MODE
- with cache_dir() as workspace, patch(
- "black.ProcessPoolExecutor", new=ThreadPoolExecutor
- ):
- failing = (workspace / "failing.py").resolve()
- with failing.open("w") as fobj:
- fobj.write("not actually python")
- clean = (workspace / "clean.py").resolve()
- with clean.open("w") as fobj:
- fobj.write('print("hello")\n')
- self.invokeBlack([str(workspace)], exit_code=123)
- cache = black.read_cache(mode)
- self.assertNotIn(str(failing), cache)
- self.assertIn(str(clean), cache)
-
- 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)
-
- @event_loop()
- @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
- def test_works_in_mono_process_only_environment(self) -> None:
- with cache_dir() as workspace:
- for f in [
- (workspace / "one.py").resolve(),
- (workspace / "two.py").resolve(),
- ]:
- f.write_text('print("hello")\n')
- self.invokeBlack([str(workspace)])
-
- @event_loop()
- def test_check_diff_use_together(self) -> None:
- with cache_dir():
- # Files which will be reformatted.
- src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
- self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
- # Files which will not be reformatted.
- src2 = (THIS_DIR / "data" / "composition.py").resolve()
- self.invokeBlack([str(src2), "--diff", "--check"])
- # Multi file command.
- self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
-
- def test_no_files(self) -> None:
- with cache_dir():
- # Without an argument, black exits with error code 0.
- self.invokeBlack([])
-
- def test_broken_symlink(self) -> None:
- with cache_dir() as workspace:
- symlink = workspace / "broken_link.py"
- try:
- symlink.symlink_to("nonexistent.py")
- except OSError as e:
- self.skipTest(f"Can't create symlinks: {e}")
- self.invokeBlack([str(workspace.resolve())])
-
- def test_read_cache_line_lengths(self) -> None:
- mode = DEFAULT_MODE
- short_mode = replace(DEFAULT_MODE, line_length=1)
- with cache_dir() as workspace:
- path = (workspace / "file.py").resolve()
- path.touch()
- black.write_cache({}, [path], mode)
- one = black.read_cache(mode)
- self.assertIn(str(path), one)
- two = black.read_cache(short_mode)
- self.assertNotIn(str(path), two)
-
- def test_single_file_force_pyi(self) -> None:
- pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
- contents, expected = read_data("force_pyi")
+ def test_single_file_force_pyi(self) -> None:
+ pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
+ contents, expected = read_data("force_pyi")
with cache_dir() as workspace:
path = (workspace / "file.py").resolve()
with open(path, "w") as fh:
actual = result.output
self.assertFormatEqual(actual, expected)
- def test_include_exclude(self) -> None:
- path = THIS_DIR / "data" / "include_exclude_tests"
- include = re.compile(r"\.pyi?$")
- exclude = re.compile(r"/exclude/|/\.definitely_exclude/")
- report = black.Report()
- gitignore = PathSpec.from_lines("gitwildmatch", [])
- sources: List[Path] = []
- expected = [
- Path(path / "b/dont_exclude/a.py"),
- Path(path / "b/dont_exclude/a.pyi"),
- ]
- this_abs = THIS_DIR.resolve()
- sources.extend(
- black.gen_python_files(
- 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))
-
- @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
- 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.
- # https://github.com/psf/black/issues/1572
- path = THIS_DIR / "data" / "include_exclude_tests"
- include = ""
- exclude = r"/exclude/|a\.py"
- src = str(path / "b/exclude/a.py")
- report = black.Report()
- expected = [Path(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(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))
-
+ @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",
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?$")
- exclude = re.compile(r"")
- report = black.Report()
- gitignore = PathSpec.from_lines(
- "gitwildmatch", ["exclude/", ".definitely_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__, "-c", "0"],
+ exit_code=0,
+ ignore_config=True,
)
- sources: List[Path] = []
- expected = [
- Path(path / "b/dont_exclude/a.py"),
- Path(path / "b/dont_exclude/a.pyi"),
- ]
- this_abs = THIS_DIR.resolve()
- sources.extend(
- black.gen_python_files(
- path.iterdir(),
- this_abs,
- include,
- exclude,
- None,
- None,
- report,
- gitignore,
- verbose=False,
- quiet=False,
- )
+
+ def test_required_version_matches_partial_version(self) -> None:
+ self.invokeBlack(
+ ["--required-version", black.__version__.split(".")[0], "-c", "0"],
+ exit_code=0,
+ ignore_config=True,
)
- 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()
- gitignore = PathSpec.from_lines("gitwildmatch", [])
- empty = re.compile(r"")
- sources: List[Path] = []
- expected = [
- Path(path / "b/exclude/a.pie"),
- Path(path / "b/exclude/a.py"),
- Path(path / "b/exclude/a.pyi"),
- Path(path / "b/dont_exclude/a.pie"),
- Path(path / "b/dont_exclude/a.py"),
- Path(path / "b/dont_exclude/a.pyi"),
- 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(
- black.gen_python_files(
- path.iterdir(),
- this_abs,
- empty,
- re.compile(black.DEFAULT_EXCLUDES),
- None,
- None,
- report,
- gitignore,
- verbose=False,
- quiet=False,
- )
- )
- self.assertEqual(sorted(expected), sorted(sources))
-
- def test_extend_exclude(self) -> None:
- path = THIS_DIR / "data" / "include_exclude_tests"
- report = black.Report()
- gitignore = PathSpec.from_lines("gitwildmatch", [])
- sources: List[Path] = []
- expected = [
- Path(path / "b/exclude/a.py"),
- Path(path / "b/dont_exclude/a.py"),
- ]
- this_abs = THIS_DIR.resolve()
- sources.extend(
- black.gen_python_files(
- path.iterdir(),
- this_abs,
- re.compile(black.DEFAULT_INCLUDES),
- 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_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:
+ def test_required_version_does_not_match_on_minor_version(self) -> None:
self.invokeBlack(
- ["--required-version", black.__version__], exit_code=0, ignore_config=True
+ ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
+ exit_code=1,
+ 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:
with self.assertRaises(AssertionError):
black.assert_equivalent("{}", "None")
- def test_symlink_out_of_root_directory(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"
- child.is_symlink.return_value = True
- try:
- list(
- black.gen_python_files(
- path.iterdir(),
- root,
- include,
- exclude,
- None,
- None,
- report,
- gitignore,
- verbose=False,
- quiet=False,
- )
- )
- except ValueError as ve:
- self.fail(f"`get_python_files_in_dir()` failed: {ve}")
- path.iterdir.assert_called_once()
- child.resolve.assert_called_once()
- child.is_symlink.assert_called_once()
- # `child` should behave like a strange file which resolved path is clearly
- # outside of the `root` directory.
- child.is_symlink.return_value = False
- with self.assertRaises(ValueError):
- list(
- black.gen_python_files(
- path.iterdir(),
- root,
- include,
- exclude,
- None,
- None,
- report,
- gitignore,
- verbose=False,
- quiet=False,
- )
- )
- path.iterdir.assert_called()
- self.assertEqual(path.iterdir.call_count, 2)
- child.resolve.assert_called()
- self.assertEqual(child.resolve.call_count, 2)
- child.is_symlink.assert_called()
- self.assertEqual(child.is_symlink.call_count, 2)
-
def test_shhh_click(self) -> None:
try:
from click import _unicodefun
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"),
+ )
+
+ @patch(
+ "black.files.find_user_pyproject_toml",
+ )
+ def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
+ find_user_pyproject_toml.side_effect = RuntimeError()
+
+ with redirect_stderr(io.StringIO()) as stderr:
+ result = black.files.find_pyproject_toml(
+ path_search_start=(str(Path.cwd().root),)
)
- self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
- self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
+
+ assert result is None
+ err = stderr.getvalue()
+ assert "Ignoring user configuration" in err
@patch(
"black.files.find_user_pyproject_toml",
normalized_path = black.normalize_path_maybe_ignore(path, root, report)
self.assertEqual(normalized_path, "workspace/project")
+ def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
+ if system() != "Windows":
+ return
+
+ with TemporaryDirectory() as workspace:
+ root = Path(workspace)
+ junction_dir = root / "junction"
+ junction_target_outside_of_root = root / ".."
+ os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
+
+ report = black.Report(verbose=True)
+ normalized_path = black.normalize_path_maybe_ignore(
+ junction_dir, root, report
+ )
+ # Manually delete for Python < 3.8
+ os.system(f"rmdir {junction_dir}")
+
+ self.assertEqual(normalized_path, None)
+
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)
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
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.
"""
with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
args = ["--code", "print"]
- CliRunner().invoke(black.main, args)
+ # This is the only directory known to contain a pyproject.toml
+ with change_directory(PROJECT_ROOT):
+ CliRunner().invoke(black.main, args)
+ pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
- 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[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.
"""
with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
- with change_directory(Path("tests")):
+ with change_directory(THIS_DIR):
args = ["--code", "print"]
CliRunner().invoke(black.main, args)
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")
-with open(black.__file__, "r", encoding="utf-8") as _bf:
- black_source_lines = _bf.readlines()
+ 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\)")
-def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
- """Show function calls `from black/__init__.py` as they happen.
- Register this with `sys.settrace()` in a test you're debugging.
- """
- if event != "call":
- return tracefunc
+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),
+ )
- stack = len(inspect.stack()) - 19
- stack *= 2
- filename = frame.f_code.co_filename
- lineno = frame.f_lineno
- func_sig_lineno = lineno - 1
- funcname = black_source_lines[func_sig_lineno].strip()
- while funcname.startswith("@"):
- func_sig_lineno += 1
- funcname = black_source_lines[func_sig_lineno].strip()
- if "black/__init__.py" in filename:
- print(f"{' ' * stack}{lineno}:{funcname}")
- return tracefunc
+ # 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:
+ cache_file = get_cache_file(mode)
+ cache_file.write_text("this is not a pickle")
+ assert black.read_cache(mode) == {}
+ src = (workspace / "test.py").resolve()
+ src.write_text("print('hello')")
+ invokeBlack([str(src)])
+ cache = black.read_cache(mode)
+ assert str(src) in cache
+
+ 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')")
+ black.write_cache({}, [src], mode)
+ invokeBlack([str(src)])
+ assert src.read_text() == "print('hello')"
+
+ @event_loop()
+ def test_cache_multiple_files(self) -> None:
+ mode = DEFAULT_MODE
+ with cache_dir() as workspace, patch(
+ "black.ProcessPoolExecutor", new=ThreadPoolExecutor
+ ):
+ one = (workspace / "one.py").resolve()
+ with one.open("w") as fobj:
+ fobj.write("print('hello')")
+ two = (workspace / "two.py").resolve()
+ with two.open("w") as fobj:
+ fobj.write("print('hello')")
+ black.write_cache({}, [one], mode)
+ invokeBlack([str(workspace)])
+ with one.open("r") as fobj:
+ assert fobj.read() == "print('hello')"
+ with two.open("r") as fobj:
+ assert fobj.read() == 'print("hello")\n'
+ cache = black.read_cache(mode)
+ assert str(one) in cache
+ assert str(two) in cache
+ @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()
+ with src.open("w") as fobj:
+ fobj.write("print('hello')")
+ with patch("black.read_cache") as read_cache, patch(
+ "black.write_cache"
+ ) as write_cache:
+ cmd = [str(src), "--diff"]
+ if color:
+ cmd.append("--color")
+ invokeBlack(cmd)
+ cache_file = get_cache_file(mode)
+ assert cache_file.exists() is False
+ write_cache.assert_not_called()
+ read_cache.assert_not_called()
-if __name__ == "__main__":
- unittest.main(module="test_black")
+ @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
+ @event_loop()
+ def test_output_locking_when_writeback_diff(self, color: bool) -> None:
+ with cache_dir() as workspace:
+ for tag in range(0, 4):
+ 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:
+ cmd = ["--diff", str(workspace)]
+ if color:
+ cmd.append("--color")
+ invokeBlack(cmd, exit_code=0)
+ # this isn't quite doing what we want, but if it _isn't_
+ # called then we cannot be using the lock it provides
+ mgr.assert_called()
+
+ def test_no_cache_when_stdin(self) -> None:
+ mode = DEFAULT_MODE
+ with cache_dir():
+ result = CliRunner().invoke(
+ black.main, ["-"], input=BytesIO(b"print('hello')")
+ )
+ assert not result.exit_code
+ cache_file = get_cache_file(mode)
+ assert not cache_file.exists()
+
+ def test_read_cache_no_cachefile(self) -> None:
+ mode = DEFAULT_MODE
+ with cache_dir():
+ assert black.read_cache(mode) == {}
+
+ 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)
+
+ def test_filter_cached(self) -> None:
+ with TemporaryDirectory() as workspace:
+ path = Path(workspace)
+ uncached = (path / "uncached").resolve()
+ cached = (path / "cached").resolve()
+ cached_but_changed = (path / "changed").resolve()
+ 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.filter_cached(
+ cache, {uncached, cached, cached_but_changed}
+ )
+ assert todo == {uncached, cached_but_changed}
+ assert done == {cached}
+
+ 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)
+ assert workspace.exists()
+
+ @event_loop()
+ def test_failed_formatting_does_not_get_cached(self) -> None:
+ mode = DEFAULT_MODE
+ with cache_dir() as workspace, patch(
+ "black.ProcessPoolExecutor", new=ThreadPoolExecutor
+ ):
+ failing = (workspace / "failing.py").resolve()
+ with failing.open("w") as fobj:
+ fobj.write("not actually python")
+ clean = (workspace / "clean.py").resolve()
+ with clean.open("w") as fobj:
+ fobj.write('print("hello")\n')
+ invokeBlack([str(workspace)], exit_code=123)
+ cache = black.read_cache(mode)
+ assert str(failing) not in cache
+ assert str(clean) in cache
+
+ 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)
+
+ def test_read_cache_line_lengths(self) -> None:
+ mode = DEFAULT_MODE
+ short_mode = replace(DEFAULT_MODE, line_length=1)
+ 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
+
+
+def assert_collected_sources(
+ 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,
+ force_exclude: Optional[str] = None,
+ stdin_filename: Optional[str] = None,
+) -> None:
+ gs_src = tuple(str(Path(s)) for s in src)
+ gs_expected = [Path(s) for s in expected]
+ gs_exclude = None if exclude is None else compile_pattern(exclude)
+ gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
+ gs_extend_exclude = (
+ None if extend_exclude is None else compile_pattern(extend_exclude)
+ )
+ gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
+ collected = black.get_sources(
+ ctx=ctx or FakeContext(),
+ src=gs_src,
+ quiet=False,
+ verbose=False,
+ include=gs_include,
+ exclude=gs_exclude,
+ extend_exclude=gs_extend_exclude,
+ force_exclude=gs_force_exclude,
+ report=black.Report(),
+ stdin_filename=stdin_filename,
+ )
+ assert sorted(collected) == sorted(gs_expected)
+
+
+class TestFileCollection:
+ def test_include_exclude(self) -> None:
+ path = THIS_DIR / "data" / "include_exclude_tests"
+ src = [path]
+ expected = [
+ Path(path / "b/dont_exclude/a.py"),
+ Path(path / "b/dont_exclude/a.pyi"),
+ ]
+ assert_collected_sources(
+ src,
+ expected,
+ include=r"\.pyi?$",
+ exclude=r"/exclude/|/\.definitely_exclude/",
+ )
+
+ def test_gitignore_used_as_default(self) -> None:
+ base = Path(DATA_DIR / "include_exclude_tests")
+ expected = [
+ base / "b/.definitely_exclude/a.py",
+ 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/")
+
+ @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.
+ # https://github.com/psf/black/issues/1572
+ path = DATA_DIR / "include_exclude_tests"
+ src = [path / "b/exclude/a.py"]
+ expected = [path / "b/exclude/a.py"]
+ assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
+
+ def test_gitignore_exclude(self) -> None:
+ path = THIS_DIR / "data" / "include_exclude_tests"
+ include = re.compile(r"\.pyi?$")
+ exclude = re.compile(r"")
+ report = black.Report()
+ gitignore = PathSpec.from_lines(
+ "gitwildmatch", ["exclude/", ".definitely_exclude"]
+ )
+ sources: List[Path] = []
+ expected = [
+ Path(path / "b/dont_exclude/a.py"),
+ Path(path / "b/dont_exclude/a.pyi"),
+ ]
+ this_abs = THIS_DIR.resolve()
+ sources.extend(
+ black.gen_python_files(
+ path.iterdir(),
+ this_abs,
+ include,
+ exclude,
+ None,
+ None,
+ report,
+ gitignore,
+ verbose=False,
+ quiet=False,
+ )
+ )
+ assert 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,
+ )
+ )
+ assert 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 = DATA_DIR / "include_exclude_tests"
+ src = [path]
+ expected = [
+ Path(path / "b/exclude/a.pie"),
+ Path(path / "b/exclude/a.py"),
+ Path(path / "b/exclude/a.pyi"),
+ Path(path / "b/dont_exclude/a.pie"),
+ Path(path / "b/dont_exclude/a.py"),
+ Path(path / "b/dont_exclude/a.pyi"),
+ 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"),
+ ]
+ # Setting exclude explicitly to an empty string to block .gitignore usage.
+ assert_collected_sources(src, expected, include="", exclude="")
+
+ def test_extend_exclude(self) -> None:
+ path = DATA_DIR / "include_exclude_tests"
+ src = [path]
+ expected = [
+ Path(path / "b/exclude/a.py"),
+ Path(path / "b/dont_exclude/a.py"),
+ ]
+ assert_collected_sources(
+ 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 = 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,
+ gitignore,
+ verbose=False,
+ quiet=False,
+ )
+ )
+ except ValueError as ve:
+ pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
+ path.iterdir.assert_called_once()
+ child.resolve.assert_called_once()
+
+ @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(), None))
+ def test_get_sources_with_stdin_filename(self) -> None:
+ src = ["-"]
+ stdin_filename = str(THIS_DIR / "data/collections.py")
+ expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
+ assert_collected_sources(
+ src,
+ expected,
+ exclude=r"/exclude/a\.py",
+ stdin_filename=stdin_filename,
+ )
+
+ @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
+ # test_exclude_for_issue_1572
+ path = DATA_DIR / "include_exclude_tests"
+ src = ["-"]
+ stdin_filename = str(path / "b/exclude/a.py")
+ expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
+ assert_collected_sources(
+ src,
+ expected,
+ exclude=r"/exclude/|a\.py",
+ stdin_filename=stdin_filename,
+ )
+
+ @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
+ # test_exclude_for_issue_1572
+ src = ["-"]
+ path = THIS_DIR / "data" / "include_exclude_tests"
+ stdin_filename = str(path / "b/exclude/a.py")
+ expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
+ assert_collected_sources(
+ src,
+ expected,
+ extend_exclude=r"/exclude/|a\.py",
+ stdin_filename=stdin_filename,
+ )
+
+ @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
+ path = THIS_DIR / "data" / "include_exclude_tests"
+ stdin_filename = str(path / "b/exclude/a.py")
+ assert_collected_sources(
+ src=["-"],
+ expected=[],
+ force_exclude=r"/exclude/|a\.py",
+ stdin_filename=stdin_filename,
+ )
+
+
+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(
+ frame: types.FrameType, event: str, arg: Any
+) -> Callable[[types.FrameType, str, Any], Any]:
+ """Show function calls `from black/__init__.py` as they happen.
+
+ Register this with `sys.settrace()` in a test you're debugging.
+ """
+ if event != "call":
+ return tracefunc
+
+ stack = len(inspect.stack()) - 19
+ stack *= 2
+ filename = frame.f_code.co_filename
+ lineno = frame.f_lineno
+ func_sig_lineno = lineno - 1
+ funcname = black_source_lines[func_sig_lineno].strip()
+ while funcname.startswith("@"):
+ func_sig_lineno += 1
+ funcname = black_source_lines[func_sig_lineno].strip()
+ if "black/__init__.py" in filename:
+ print(f"{' ' * stack}{lineno}:{funcname}")
+ return tracefunc