All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
12 from concurrent.futures import ThreadPoolExecutor
13 from contextlib import contextmanager
14 from dataclasses import replace
15 from io import BytesIO
16 from pathlib import Path
17 from platform import system
18 from tempfile import TemporaryDirectory
30 from unittest.mock import MagicMock, patch
35 from click import unstyle
36 from click.testing import CliRunner
37 from pathspec import PathSpec
41 from black import Feature, TargetVersion
42 from black import re_compile_maybe_verbose as compile_pattern
43 from black.cache import get_cache_file
44 from black.debug import DebugVisitor
45 from black.output import color_diff, diff
46 from black.report import Report
48 # Import other test classes
49 from tests.util import (
65 THIS_FILE = Path(__file__)
66 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
67 DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
68 DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
72 # Match the time output in a diff, but nothing else
73 DIFF_TIME = re.compile(r"\t[\d\-:+\. ]+")
77 def cache_dir(exists: bool = True) -> Iterator[Path]:
78 with TemporaryDirectory() as workspace:
79 cache_dir = Path(workspace)
81 cache_dir = cache_dir / "new"
82 with patch("black.cache.CACHE_DIR", cache_dir):
87 def event_loop() -> Iterator[None]:
88 policy = asyncio.get_event_loop_policy()
89 loop = policy.new_event_loop()
90 asyncio.set_event_loop(loop)
98 class FakeContext(click.Context):
99 """A fake click Context for when calling functions that need it."""
101 def __init__(self) -> None:
102 self.default_map: Dict[str, Any] = {}
103 # Dummy root, since most of the tests don't care about it
104 self.obj: Dict[str, Any] = {"root": PROJECT_ROOT}
107 class FakeParameter(click.Parameter):
108 """A fake click Parameter for when calling functions that need it."""
110 def __init__(self) -> None:
114 class BlackRunner(CliRunner):
115 """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
117 def __init__(self) -> None:
118 super().__init__(mix_stderr=False)
122 args: List[str], exit_code: int = 0, ignore_config: bool = True
124 runner = BlackRunner()
126 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
127 result = runner.invoke(black.main, args, catch_exceptions=False)
128 assert result.stdout_bytes is not None
129 assert result.stderr_bytes is not None
131 f"Failed with args: {args}\n"
132 f"stdout: {result.stdout_bytes.decode()!r}\n"
133 f"stderr: {result.stderr_bytes.decode()!r}\n"
134 f"exception: {result.exception}"
136 assert result.exit_code == exit_code, msg
139 class BlackTestCase(BlackBaseTestCase):
140 invokeBlack = staticmethod(invokeBlack)
142 def test_empty_ff(self) -> None:
144 tmp_file = Path(black.dump_to_file())
146 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
147 with open(tmp_file, encoding="utf8") as f:
151 self.assertFormatEqual(expected, actual)
153 def test_experimental_string_processing_warns(self) -> None:
155 black.mode.Deprecated, black.Mode, experimental_string_processing=True
158 def test_piping(self) -> None:
159 source, expected = read_data("src/black/__init__", data=False)
160 result = BlackRunner().invoke(
162 ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
163 input=BytesIO(source.encode("utf8")),
165 self.assertEqual(result.exit_code, 0)
166 self.assertFormatEqual(expected, result.output)
167 if source != result.output:
168 black.assert_equivalent(source, result.output)
169 black.assert_stable(source, result.output, DEFAULT_MODE)
171 def test_piping_diff(self) -> None:
172 diff_header = re.compile(
173 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
176 source, _ = read_data("expression.py")
177 expected, _ = read_data("expression.diff")
178 config = THIS_DIR / "data" / "empty_pyproject.toml"
182 f"--line-length={black.DEFAULT_LINE_LENGTH}",
184 f"--config={config}",
186 result = BlackRunner().invoke(
187 black.main, args, input=BytesIO(source.encode("utf8"))
189 self.assertEqual(result.exit_code, 0)
190 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
191 actual = actual.rstrip() + "\n" # the diff output has a trailing space
192 self.assertEqual(expected, actual)
194 def test_piping_diff_with_color(self) -> None:
195 source, _ = read_data("expression.py")
196 config = THIS_DIR / "data" / "empty_pyproject.toml"
200 f"--line-length={black.DEFAULT_LINE_LENGTH}",
203 f"--config={config}",
205 result = BlackRunner().invoke(
206 black.main, args, input=BytesIO(source.encode("utf8"))
208 actual = result.output
209 # Again, the contents are checked in a different test, so only look for colors.
210 self.assertIn("\033[1m", actual)
211 self.assertIn("\033[36m", actual)
212 self.assertIn("\033[32m", actual)
213 self.assertIn("\033[31m", actual)
214 self.assertIn("\033[0m", actual)
216 @patch("black.dump_to_file", dump_to_stderr)
217 def _test_wip(self) -> None:
218 source, expected = read_data("wip")
219 sys.settrace(tracefunc)
222 experimental_string_processing=False,
223 target_versions={black.TargetVersion.PY38},
225 actual = fs(source, mode=mode)
227 self.assertFormatEqual(expected, actual)
228 black.assert_equivalent(source, actual)
229 black.assert_stable(source, actual, black.FileMode())
231 @unittest.expectedFailure
232 @patch("black.dump_to_file", dump_to_stderr)
233 def test_trailing_comma_optional_parens_stability1(self) -> None:
234 source, _expected = read_data("trailing_comma_optional_parens1")
236 black.assert_stable(source, actual, DEFAULT_MODE)
238 @unittest.expectedFailure
239 @patch("black.dump_to_file", dump_to_stderr)
240 def test_trailing_comma_optional_parens_stability2(self) -> None:
241 source, _expected = read_data("trailing_comma_optional_parens2")
243 black.assert_stable(source, actual, DEFAULT_MODE)
245 @unittest.expectedFailure
246 @patch("black.dump_to_file", dump_to_stderr)
247 def test_trailing_comma_optional_parens_stability3(self) -> None:
248 source, _expected = read_data("trailing_comma_optional_parens3")
250 black.assert_stable(source, actual, DEFAULT_MODE)
252 @patch("black.dump_to_file", dump_to_stderr)
253 def test_trailing_comma_optional_parens_stability1_pass2(self) -> None:
254 source, _expected = read_data("trailing_comma_optional_parens1")
255 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
256 black.assert_stable(source, actual, DEFAULT_MODE)
258 @patch("black.dump_to_file", dump_to_stderr)
259 def test_trailing_comma_optional_parens_stability2_pass2(self) -> None:
260 source, _expected = read_data("trailing_comma_optional_parens2")
261 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
262 black.assert_stable(source, actual, DEFAULT_MODE)
264 @patch("black.dump_to_file", dump_to_stderr)
265 def test_trailing_comma_optional_parens_stability3_pass2(self) -> None:
266 source, _expected = read_data("trailing_comma_optional_parens3")
267 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
268 black.assert_stable(source, actual, DEFAULT_MODE)
270 def test_pep_572_version_detection(self) -> None:
271 source, _ = read_data("pep_572")
272 root = black.lib2to3_parse(source)
273 features = black.get_features_used(root)
274 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
275 versions = black.detect_target_versions(root)
276 self.assertIn(black.TargetVersion.PY38, versions)
278 def test_expression_ff(self) -> None:
279 source, expected = read_data("expression")
280 tmp_file = Path(black.dump_to_file(source))
282 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
283 with open(tmp_file, encoding="utf8") as f:
287 self.assertFormatEqual(expected, actual)
288 with patch("black.dump_to_file", dump_to_stderr):
289 black.assert_equivalent(source, actual)
290 black.assert_stable(source, actual, DEFAULT_MODE)
292 def test_expression_diff(self) -> None:
293 source, _ = read_data("expression.py")
294 config = THIS_DIR / "data" / "empty_pyproject.toml"
295 expected, _ = read_data("expression.diff")
296 tmp_file = Path(black.dump_to_file(source))
297 diff_header = re.compile(
298 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
299 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
302 result = BlackRunner().invoke(
303 black.main, ["--diff", str(tmp_file), f"--config={config}"]
305 self.assertEqual(result.exit_code, 0)
308 actual = result.output
309 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
310 if expected != actual:
311 dump = black.dump_to_file(actual)
313 "Expected diff isn't equal to the actual. If you made changes to"
314 " expression.py and this is an anticipated difference, overwrite"
315 f" tests/data/expression.diff with {dump}"
317 self.assertEqual(expected, actual, msg)
319 def test_expression_diff_with_color(self) -> None:
320 source, _ = read_data("expression.py")
321 config = THIS_DIR / "data" / "empty_pyproject.toml"
322 expected, _ = read_data("expression.diff")
323 tmp_file = Path(black.dump_to_file(source))
325 result = BlackRunner().invoke(
326 black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
330 actual = result.output
331 # We check the contents of the diff in `test_expression_diff`. All
332 # we need to check here is that color codes exist in the result.
333 self.assertIn("\033[1m", actual)
334 self.assertIn("\033[36m", actual)
335 self.assertIn("\033[32m", actual)
336 self.assertIn("\033[31m", actual)
337 self.assertIn("\033[0m", actual)
339 def test_detect_pos_only_arguments(self) -> None:
340 source, _ = read_data("pep_570")
341 root = black.lib2to3_parse(source)
342 features = black.get_features_used(root)
343 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
344 versions = black.detect_target_versions(root)
345 self.assertIn(black.TargetVersion.PY38, versions)
347 @patch("black.dump_to_file", dump_to_stderr)
348 def test_string_quotes(self) -> None:
349 source, expected = read_data("string_quotes")
350 mode = black.Mode(preview=True)
351 assert_format(source, expected, mode)
352 mode = replace(mode, string_normalization=False)
353 not_normalized = fs(source, mode=mode)
354 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
355 black.assert_equivalent(source, not_normalized)
356 black.assert_stable(source, not_normalized, mode=mode)
358 def test_skip_magic_trailing_comma(self) -> None:
359 source, _ = read_data("expression.py")
360 expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
361 tmp_file = Path(black.dump_to_file(source))
362 diff_header = re.compile(
363 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
364 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
367 result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
368 self.assertEqual(result.exit_code, 0)
371 actual = result.output
372 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
373 actual = actual.rstrip() + "\n" # the diff output has a trailing space
374 if expected != actual:
375 dump = black.dump_to_file(actual)
377 "Expected diff isn't equal to the actual. If you made changes to"
378 " expression.py and this is an anticipated difference, overwrite"
379 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
381 self.assertEqual(expected, actual, msg)
383 @patch("black.dump_to_file", dump_to_stderr)
384 def test_async_as_identifier(self) -> None:
385 source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
386 source, expected = read_data("async_as_identifier")
388 self.assertFormatEqual(expected, actual)
389 major, minor = sys.version_info[:2]
390 if major < 3 or (major <= 3 and minor < 7):
391 black.assert_equivalent(source, actual)
392 black.assert_stable(source, actual, DEFAULT_MODE)
393 # ensure black can parse this when the target is 3.6
394 self.invokeBlack([str(source_path), "--target-version", "py36"])
395 # but not on 3.7, because async/await is no longer an identifier
396 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
398 @patch("black.dump_to_file", dump_to_stderr)
399 def test_python37(self) -> None:
400 source_path = (THIS_DIR / "data" / "python37.py").resolve()
401 source, expected = read_data("python37")
403 self.assertFormatEqual(expected, actual)
404 major, minor = sys.version_info[:2]
405 if major > 3 or (major == 3 and minor >= 7):
406 black.assert_equivalent(source, actual)
407 black.assert_stable(source, actual, DEFAULT_MODE)
408 # ensure black can parse this when the target is 3.7
409 self.invokeBlack([str(source_path), "--target-version", "py37"])
410 # but not on 3.6, because we use async as a reserved keyword
411 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
413 def test_tab_comment_indentation(self) -> None:
414 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
415 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
416 self.assertFormatEqual(contents_spc, fs(contents_spc))
417 self.assertFormatEqual(contents_spc, fs(contents_tab))
419 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
420 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
421 self.assertFormatEqual(contents_spc, fs(contents_spc))
422 self.assertFormatEqual(contents_spc, fs(contents_tab))
424 # mixed tabs and spaces (valid Python 2 code)
425 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
426 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
427 self.assertFormatEqual(contents_spc, fs(contents_spc))
428 self.assertFormatEqual(contents_spc, fs(contents_tab))
430 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
431 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
432 self.assertFormatEqual(contents_spc, fs(contents_spc))
433 self.assertFormatEqual(contents_spc, fs(contents_tab))
435 def test_report_verbose(self) -> None:
436 report = Report(verbose=True)
440 def out(msg: str, **kwargs: Any) -> None:
441 out_lines.append(msg)
443 def err(msg: str, **kwargs: Any) -> None:
444 err_lines.append(msg)
446 with patch("black.output._out", out), patch("black.output._err", err):
447 report.done(Path("f1"), black.Changed.NO)
448 self.assertEqual(len(out_lines), 1)
449 self.assertEqual(len(err_lines), 0)
450 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
451 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
452 self.assertEqual(report.return_code, 0)
453 report.done(Path("f2"), black.Changed.YES)
454 self.assertEqual(len(out_lines), 2)
455 self.assertEqual(len(err_lines), 0)
456 self.assertEqual(out_lines[-1], "reformatted f2")
458 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
460 report.done(Path("f3"), black.Changed.CACHED)
461 self.assertEqual(len(out_lines), 3)
462 self.assertEqual(len(err_lines), 0)
464 out_lines[-1], "f3 wasn't modified on disk since last run."
467 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
469 self.assertEqual(report.return_code, 0)
471 self.assertEqual(report.return_code, 1)
473 report.failed(Path("e1"), "boom")
474 self.assertEqual(len(out_lines), 3)
475 self.assertEqual(len(err_lines), 1)
476 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
478 unstyle(str(report)),
479 "1 file reformatted, 2 files left unchanged, 1 file failed to"
482 self.assertEqual(report.return_code, 123)
483 report.done(Path("f3"), black.Changed.YES)
484 self.assertEqual(len(out_lines), 4)
485 self.assertEqual(len(err_lines), 1)
486 self.assertEqual(out_lines[-1], "reformatted f3")
488 unstyle(str(report)),
489 "2 files reformatted, 2 files left unchanged, 1 file failed to"
492 self.assertEqual(report.return_code, 123)
493 report.failed(Path("e2"), "boom")
494 self.assertEqual(len(out_lines), 4)
495 self.assertEqual(len(err_lines), 2)
496 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
498 unstyle(str(report)),
499 "2 files reformatted, 2 files left unchanged, 2 files failed to"
502 self.assertEqual(report.return_code, 123)
503 report.path_ignored(Path("wat"), "no match")
504 self.assertEqual(len(out_lines), 5)
505 self.assertEqual(len(err_lines), 2)
506 self.assertEqual(out_lines[-1], "wat ignored: no match")
508 unstyle(str(report)),
509 "2 files reformatted, 2 files left unchanged, 2 files failed to"
512 self.assertEqual(report.return_code, 123)
513 report.done(Path("f4"), black.Changed.NO)
514 self.assertEqual(len(out_lines), 6)
515 self.assertEqual(len(err_lines), 2)
516 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
518 unstyle(str(report)),
519 "2 files reformatted, 3 files left unchanged, 2 files failed to"
522 self.assertEqual(report.return_code, 123)
525 unstyle(str(report)),
526 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
527 " would fail to reformat.",
532 unstyle(str(report)),
533 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
534 " would fail to reformat.",
537 def test_report_quiet(self) -> None:
538 report = Report(quiet=True)
542 def out(msg: str, **kwargs: Any) -> None:
543 out_lines.append(msg)
545 def err(msg: str, **kwargs: Any) -> None:
546 err_lines.append(msg)
548 with patch("black.output._out", out), patch("black.output._err", err):
549 report.done(Path("f1"), black.Changed.NO)
550 self.assertEqual(len(out_lines), 0)
551 self.assertEqual(len(err_lines), 0)
552 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
553 self.assertEqual(report.return_code, 0)
554 report.done(Path("f2"), black.Changed.YES)
555 self.assertEqual(len(out_lines), 0)
556 self.assertEqual(len(err_lines), 0)
558 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
560 report.done(Path("f3"), black.Changed.CACHED)
561 self.assertEqual(len(out_lines), 0)
562 self.assertEqual(len(err_lines), 0)
564 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
566 self.assertEqual(report.return_code, 0)
568 self.assertEqual(report.return_code, 1)
570 report.failed(Path("e1"), "boom")
571 self.assertEqual(len(out_lines), 0)
572 self.assertEqual(len(err_lines), 1)
573 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
575 unstyle(str(report)),
576 "1 file reformatted, 2 files left unchanged, 1 file failed to"
579 self.assertEqual(report.return_code, 123)
580 report.done(Path("f3"), black.Changed.YES)
581 self.assertEqual(len(out_lines), 0)
582 self.assertEqual(len(err_lines), 1)
584 unstyle(str(report)),
585 "2 files reformatted, 2 files left unchanged, 1 file failed to"
588 self.assertEqual(report.return_code, 123)
589 report.failed(Path("e2"), "boom")
590 self.assertEqual(len(out_lines), 0)
591 self.assertEqual(len(err_lines), 2)
592 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
594 unstyle(str(report)),
595 "2 files reformatted, 2 files left unchanged, 2 files failed to"
598 self.assertEqual(report.return_code, 123)
599 report.path_ignored(Path("wat"), "no match")
600 self.assertEqual(len(out_lines), 0)
601 self.assertEqual(len(err_lines), 2)
603 unstyle(str(report)),
604 "2 files reformatted, 2 files left unchanged, 2 files failed to"
607 self.assertEqual(report.return_code, 123)
608 report.done(Path("f4"), black.Changed.NO)
609 self.assertEqual(len(out_lines), 0)
610 self.assertEqual(len(err_lines), 2)
612 unstyle(str(report)),
613 "2 files reformatted, 3 files left unchanged, 2 files failed to"
616 self.assertEqual(report.return_code, 123)
619 unstyle(str(report)),
620 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
621 " would fail to reformat.",
626 unstyle(str(report)),
627 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
628 " would fail to reformat.",
631 def test_report_normal(self) -> None:
632 report = black.Report()
636 def out(msg: str, **kwargs: Any) -> None:
637 out_lines.append(msg)
639 def err(msg: str, **kwargs: Any) -> None:
640 err_lines.append(msg)
642 with patch("black.output._out", out), patch("black.output._err", err):
643 report.done(Path("f1"), black.Changed.NO)
644 self.assertEqual(len(out_lines), 0)
645 self.assertEqual(len(err_lines), 0)
646 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
647 self.assertEqual(report.return_code, 0)
648 report.done(Path("f2"), black.Changed.YES)
649 self.assertEqual(len(out_lines), 1)
650 self.assertEqual(len(err_lines), 0)
651 self.assertEqual(out_lines[-1], "reformatted f2")
653 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
655 report.done(Path("f3"), black.Changed.CACHED)
656 self.assertEqual(len(out_lines), 1)
657 self.assertEqual(len(err_lines), 0)
658 self.assertEqual(out_lines[-1], "reformatted f2")
660 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
662 self.assertEqual(report.return_code, 0)
664 self.assertEqual(report.return_code, 1)
666 report.failed(Path("e1"), "boom")
667 self.assertEqual(len(out_lines), 1)
668 self.assertEqual(len(err_lines), 1)
669 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
671 unstyle(str(report)),
672 "1 file reformatted, 2 files left unchanged, 1 file failed to"
675 self.assertEqual(report.return_code, 123)
676 report.done(Path("f3"), black.Changed.YES)
677 self.assertEqual(len(out_lines), 2)
678 self.assertEqual(len(err_lines), 1)
679 self.assertEqual(out_lines[-1], "reformatted f3")
681 unstyle(str(report)),
682 "2 files reformatted, 2 files left unchanged, 1 file failed to"
685 self.assertEqual(report.return_code, 123)
686 report.failed(Path("e2"), "boom")
687 self.assertEqual(len(out_lines), 2)
688 self.assertEqual(len(err_lines), 2)
689 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
691 unstyle(str(report)),
692 "2 files reformatted, 2 files left unchanged, 2 files failed to"
695 self.assertEqual(report.return_code, 123)
696 report.path_ignored(Path("wat"), "no match")
697 self.assertEqual(len(out_lines), 2)
698 self.assertEqual(len(err_lines), 2)
700 unstyle(str(report)),
701 "2 files reformatted, 2 files left unchanged, 2 files failed to"
704 self.assertEqual(report.return_code, 123)
705 report.done(Path("f4"), black.Changed.NO)
706 self.assertEqual(len(out_lines), 2)
707 self.assertEqual(len(err_lines), 2)
709 unstyle(str(report)),
710 "2 files reformatted, 3 files left unchanged, 2 files failed to"
713 self.assertEqual(report.return_code, 123)
716 unstyle(str(report)),
717 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
718 " would fail to reformat.",
723 unstyle(str(report)),
724 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
725 " would fail to reformat.",
728 def test_lib2to3_parse(self) -> None:
729 with self.assertRaises(black.InvalidInput):
730 black.lib2to3_parse("invalid syntax")
733 black.lib2to3_parse(straddling)
734 black.lib2to3_parse(straddling, {TargetVersion.PY36})
737 with self.assertRaises(black.InvalidInput):
738 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
740 py3_only = "exec(x, end=y)"
741 black.lib2to3_parse(py3_only)
742 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
744 def test_get_features_used_decorator(self) -> None:
745 # Test the feature detection of new decorator syntax
746 # since this makes some test cases of test_get_features_used()
747 # fails if it fails, this is tested first so that a useful case
749 simples, relaxed = read_data("decorators")
750 # skip explanation comments at the top of the file
751 for simple_test in simples.split("##")[1:]:
752 node = black.lib2to3_parse(simple_test)
753 decorator = str(node.children[0].children[0]).strip()
755 Feature.RELAXED_DECORATORS,
756 black.get_features_used(node),
758 f"decorator '{decorator}' follows python<=3.8 syntax"
759 "but is detected as 3.9+"
760 # f"The full node is\n{node!r}"
763 # skip the '# output' comment at the top of the output part
764 for relaxed_test in relaxed.split("##")[1:]:
765 node = black.lib2to3_parse(relaxed_test)
766 decorator = str(node.children[0].children[0]).strip()
768 Feature.RELAXED_DECORATORS,
769 black.get_features_used(node),
771 f"decorator '{decorator}' uses python3.9+ syntax"
772 "but is detected as python<=3.8"
773 # f"The full node is\n{node!r}"
777 def test_get_features_used(self) -> None:
778 node = black.lib2to3_parse("def f(*, arg): ...\n")
779 self.assertEqual(black.get_features_used(node), set())
780 node = black.lib2to3_parse("def f(*, arg,): ...\n")
781 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
782 node = black.lib2to3_parse("f(*arg,)\n")
784 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
786 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
787 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
788 node = black.lib2to3_parse("123_456\n")
789 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
790 node = black.lib2to3_parse("123456\n")
791 self.assertEqual(black.get_features_used(node), set())
792 source, expected = read_data("function")
793 node = black.lib2to3_parse(source)
794 expected_features = {
795 Feature.TRAILING_COMMA_IN_CALL,
796 Feature.TRAILING_COMMA_IN_DEF,
799 self.assertEqual(black.get_features_used(node), expected_features)
800 node = black.lib2to3_parse(expected)
801 self.assertEqual(black.get_features_used(node), expected_features)
802 source, expected = read_data("expression")
803 node = black.lib2to3_parse(source)
804 self.assertEqual(black.get_features_used(node), set())
805 node = black.lib2to3_parse(expected)
806 self.assertEqual(black.get_features_used(node), set())
807 node = black.lib2to3_parse("lambda a, /, b: ...")
808 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
809 node = black.lib2to3_parse("def fn(a, /, b): ...")
810 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
811 node = black.lib2to3_parse("def fn(): yield a, b")
812 self.assertEqual(black.get_features_used(node), set())
813 node = black.lib2to3_parse("def fn(): return a, b")
814 self.assertEqual(black.get_features_used(node), set())
815 node = black.lib2to3_parse("def fn(): yield *b, c")
816 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
817 node = black.lib2to3_parse("def fn(): return a, *b, c")
818 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
819 node = black.lib2to3_parse("x = a, *b, c")
820 self.assertEqual(black.get_features_used(node), set())
821 node = black.lib2to3_parse("x: Any = regular")
822 self.assertEqual(black.get_features_used(node), set())
823 node = black.lib2to3_parse("x: Any = (regular, regular)")
824 self.assertEqual(black.get_features_used(node), set())
825 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
826 self.assertEqual(black.get_features_used(node), set())
827 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
829 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
832 def test_get_features_used_for_future_flags(self) -> None:
833 for src, features in [
834 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
836 "from __future__ import (other, annotations)",
837 {Feature.FUTURE_ANNOTATIONS},
839 ("a = 1 + 2\nfrom something import annotations", set()),
840 ("from __future__ import x, y", set()),
842 with self.subTest(src=src, features=features):
843 node = black.lib2to3_parse(src)
844 future_imports = black.get_future_imports(node)
846 black.get_features_used(node, future_imports=future_imports),
850 def test_get_future_imports(self) -> None:
851 node = black.lib2to3_parse("\n")
852 self.assertEqual(set(), black.get_future_imports(node))
853 node = black.lib2to3_parse("from __future__ import black\n")
854 self.assertEqual({"black"}, black.get_future_imports(node))
855 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
856 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
857 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
858 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
859 node = black.lib2to3_parse(
860 "from __future__ import multiple\nfrom __future__ import imports\n"
862 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
863 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
864 self.assertEqual({"black"}, black.get_future_imports(node))
865 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
866 self.assertEqual({"black"}, black.get_future_imports(node))
867 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
868 self.assertEqual(set(), black.get_future_imports(node))
869 node = black.lib2to3_parse("from some.module import black\n")
870 self.assertEqual(set(), black.get_future_imports(node))
871 node = black.lib2to3_parse(
872 "from __future__ import unicode_literals as _unicode_literals"
874 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
875 node = black.lib2to3_parse(
876 "from __future__ import unicode_literals as _lol, print"
878 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
880 @pytest.mark.incompatible_with_mypyc
881 def test_debug_visitor(self) -> None:
882 source, _ = read_data("debug_visitor.py")
883 expected, _ = read_data("debug_visitor.out")
887 def out(msg: str, **kwargs: Any) -> None:
888 out_lines.append(msg)
890 def err(msg: str, **kwargs: Any) -> None:
891 err_lines.append(msg)
893 with patch("black.debug.out", out):
894 DebugVisitor.show(source)
895 actual = "\n".join(out_lines) + "\n"
897 if expected != actual:
898 log_name = black.dump_to_file(*out_lines)
902 f"AST print out is different. Actual version dumped to {log_name}",
905 def test_format_file_contents(self) -> None:
908 with self.assertRaises(black.NothingChanged):
909 black.format_file_contents(empty, mode=mode, fast=False)
911 with self.assertRaises(black.NothingChanged):
912 black.format_file_contents(just_nl, mode=mode, fast=False)
913 same = "j = [1, 2, 3]\n"
914 with self.assertRaises(black.NothingChanged):
915 black.format_file_contents(same, mode=mode, fast=False)
916 different = "j = [1,2,3]"
918 actual = black.format_file_contents(different, mode=mode, fast=False)
919 self.assertEqual(expected, actual)
920 invalid = "return if you can"
921 with self.assertRaises(black.InvalidInput) as e:
922 black.format_file_contents(invalid, mode=mode, fast=False)
923 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
925 def test_endmarker(self) -> None:
926 n = black.lib2to3_parse("\n")
927 self.assertEqual(n.type, black.syms.file_input)
928 self.assertEqual(len(n.children), 1)
929 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
931 @pytest.mark.incompatible_with_mypyc
932 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
933 def test_assertFormatEqual(self) -> None:
937 def out(msg: str, **kwargs: Any) -> None:
938 out_lines.append(msg)
940 def err(msg: str, **kwargs: Any) -> None:
941 err_lines.append(msg)
943 with patch("black.output._out", out), patch("black.output._err", err):
944 with self.assertRaises(AssertionError):
945 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
947 out_str = "".join(out_lines)
948 self.assertIn("Expected tree:", out_str)
949 self.assertIn("Actual tree:", out_str)
950 self.assertEqual("".join(err_lines), "")
953 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
954 def test_works_in_mono_process_only_environment(self) -> None:
955 with cache_dir() as workspace:
957 (workspace / "one.py").resolve(),
958 (workspace / "two.py").resolve(),
960 f.write_text('print("hello")\n')
961 self.invokeBlack([str(workspace)])
964 def test_check_diff_use_together(self) -> None:
966 # Files which will be reformatted.
967 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
968 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
969 # Files which will not be reformatted.
970 src2 = (THIS_DIR / "data" / "composition.py").resolve()
971 self.invokeBlack([str(src2), "--diff", "--check"])
972 # Multi file command.
973 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
975 def test_no_files(self) -> None:
977 # Without an argument, black exits with error code 0.
980 def test_broken_symlink(self) -> None:
981 with cache_dir() as workspace:
982 symlink = workspace / "broken_link.py"
984 symlink.symlink_to("nonexistent.py")
985 except (OSError, NotImplementedError) as e:
986 self.skipTest(f"Can't create symlinks: {e}")
987 self.invokeBlack([str(workspace.resolve())])
989 def test_single_file_force_pyi(self) -> None:
990 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
991 contents, expected = read_data("force_pyi")
992 with cache_dir() as workspace:
993 path = (workspace / "file.py").resolve()
994 with open(path, "w") as fh:
996 self.invokeBlack([str(path), "--pyi"])
997 with open(path, "r") as fh:
999 # verify cache with --pyi is separate
1000 pyi_cache = black.read_cache(pyi_mode)
1001 self.assertIn(str(path), pyi_cache)
1002 normal_cache = black.read_cache(DEFAULT_MODE)
1003 self.assertNotIn(str(path), normal_cache)
1004 self.assertFormatEqual(expected, actual)
1005 black.assert_equivalent(contents, actual)
1006 black.assert_stable(contents, actual, pyi_mode)
1009 def test_multi_file_force_pyi(self) -> None:
1010 reg_mode = DEFAULT_MODE
1011 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1012 contents, expected = read_data("force_pyi")
1013 with cache_dir() as workspace:
1015 (workspace / "file1.py").resolve(),
1016 (workspace / "file2.py").resolve(),
1019 with open(path, "w") as fh:
1021 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1023 with open(path, "r") as fh:
1025 self.assertEqual(actual, expected)
1026 # verify cache with --pyi is separate
1027 pyi_cache = black.read_cache(pyi_mode)
1028 normal_cache = black.read_cache(reg_mode)
1030 self.assertIn(str(path), pyi_cache)
1031 self.assertNotIn(str(path), normal_cache)
1033 def test_pipe_force_pyi(self) -> None:
1034 source, expected = read_data("force_pyi")
1035 result = CliRunner().invoke(
1036 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1038 self.assertEqual(result.exit_code, 0)
1039 actual = result.output
1040 self.assertFormatEqual(actual, expected)
1042 def test_single_file_force_py36(self) -> None:
1043 reg_mode = DEFAULT_MODE
1044 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1045 source, expected = read_data("force_py36")
1046 with cache_dir() as workspace:
1047 path = (workspace / "file.py").resolve()
1048 with open(path, "w") as fh:
1050 self.invokeBlack([str(path), *PY36_ARGS])
1051 with open(path, "r") as fh:
1053 # verify cache with --target-version is separate
1054 py36_cache = black.read_cache(py36_mode)
1055 self.assertIn(str(path), py36_cache)
1056 normal_cache = black.read_cache(reg_mode)
1057 self.assertNotIn(str(path), normal_cache)
1058 self.assertEqual(actual, expected)
1061 def test_multi_file_force_py36(self) -> None:
1062 reg_mode = DEFAULT_MODE
1063 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1064 source, expected = read_data("force_py36")
1065 with cache_dir() as workspace:
1067 (workspace / "file1.py").resolve(),
1068 (workspace / "file2.py").resolve(),
1071 with open(path, "w") as fh:
1073 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1075 with open(path, "r") as fh:
1077 self.assertEqual(actual, expected)
1078 # verify cache with --target-version is separate
1079 pyi_cache = black.read_cache(py36_mode)
1080 normal_cache = black.read_cache(reg_mode)
1082 self.assertIn(str(path), pyi_cache)
1083 self.assertNotIn(str(path), normal_cache)
1085 def test_pipe_force_py36(self) -> None:
1086 source, expected = read_data("force_py36")
1087 result = CliRunner().invoke(
1089 ["-", "-q", "--target-version=py36"],
1090 input=BytesIO(source.encode("utf8")),
1092 self.assertEqual(result.exit_code, 0)
1093 actual = result.output
1094 self.assertFormatEqual(actual, expected)
1096 @pytest.mark.incompatible_with_mypyc
1097 def test_reformat_one_with_stdin(self) -> None:
1099 "black.format_stdin_to_stdout",
1100 return_value=lambda *args, **kwargs: black.Changed.YES,
1102 report = MagicMock()
1107 write_back=black.WriteBack.YES,
1111 fsts.assert_called_once()
1112 report.done.assert_called_with(path, black.Changed.YES)
1114 @pytest.mark.incompatible_with_mypyc
1115 def test_reformat_one_with_stdin_filename(self) -> None:
1117 "black.format_stdin_to_stdout",
1118 return_value=lambda *args, **kwargs: black.Changed.YES,
1120 report = MagicMock()
1122 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1127 write_back=black.WriteBack.YES,
1131 fsts.assert_called_once_with(
1132 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1134 # __BLACK_STDIN_FILENAME__ should have been stripped
1135 report.done.assert_called_with(expected, black.Changed.YES)
1137 @pytest.mark.incompatible_with_mypyc
1138 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1140 "black.format_stdin_to_stdout",
1141 return_value=lambda *args, **kwargs: black.Changed.YES,
1143 report = MagicMock()
1145 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1150 write_back=black.WriteBack.YES,
1154 fsts.assert_called_once_with(
1156 write_back=black.WriteBack.YES,
1157 mode=replace(DEFAULT_MODE, is_pyi=True),
1159 # __BLACK_STDIN_FILENAME__ should have been stripped
1160 report.done.assert_called_with(expected, black.Changed.YES)
1162 @pytest.mark.incompatible_with_mypyc
1163 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1165 "black.format_stdin_to_stdout",
1166 return_value=lambda *args, **kwargs: black.Changed.YES,
1168 report = MagicMock()
1170 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1175 write_back=black.WriteBack.YES,
1179 fsts.assert_called_once_with(
1181 write_back=black.WriteBack.YES,
1182 mode=replace(DEFAULT_MODE, is_ipynb=True),
1184 # __BLACK_STDIN_FILENAME__ should have been stripped
1185 report.done.assert_called_with(expected, black.Changed.YES)
1187 @pytest.mark.incompatible_with_mypyc
1188 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1190 "black.format_stdin_to_stdout",
1191 return_value=lambda *args, **kwargs: black.Changed.YES,
1193 report = MagicMock()
1194 # Even with an existing file, since we are forcing stdin, black
1195 # should output to stdout and not modify the file inplace
1196 p = Path(str(THIS_DIR / "data/collections.py"))
1197 # Make sure is_file actually returns True
1198 self.assertTrue(p.is_file())
1199 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1204 write_back=black.WriteBack.YES,
1208 fsts.assert_called_once()
1209 # __BLACK_STDIN_FILENAME__ should have been stripped
1210 report.done.assert_called_with(expected, black.Changed.YES)
1212 def test_reformat_one_with_stdin_empty(self) -> None:
1213 output = io.StringIO()
1214 with patch("io.TextIOWrapper", lambda *args, **kwargs: output):
1216 black.format_stdin_to_stdout(
1219 write_back=black.WriteBack.YES,
1222 except io.UnsupportedOperation:
1223 pass # StringIO does not support detach
1224 assert output.getvalue() == ""
1226 def test_invalid_cli_regex(self) -> None:
1227 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1228 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1230 def test_required_version_matches_version(self) -> None:
1232 ["--required-version", black.__version__], exit_code=0, ignore_config=True
1235 def test_required_version_does_not_match_version(self) -> None:
1237 ["--required-version", "20.99b"], exit_code=1, ignore_config=True
1240 def test_preserves_line_endings(self) -> None:
1241 with TemporaryDirectory() as workspace:
1242 test_file = Path(workspace) / "test.py"
1243 for nl in ["\n", "\r\n"]:
1244 contents = nl.join(["def f( ):", " pass"])
1245 test_file.write_bytes(contents.encode())
1246 ff(test_file, write_back=black.WriteBack.YES)
1247 updated_contents: bytes = test_file.read_bytes()
1248 self.assertIn(nl.encode(), updated_contents)
1250 self.assertNotIn(b"\r\n", updated_contents)
1252 def test_preserves_line_endings_via_stdin(self) -> None:
1253 for nl in ["\n", "\r\n"]:
1254 contents = nl.join(["def f( ):", " pass"])
1255 runner = BlackRunner()
1256 result = runner.invoke(
1257 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1259 self.assertEqual(result.exit_code, 0)
1260 output = result.stdout_bytes
1261 self.assertIn(nl.encode("utf8"), output)
1263 self.assertNotIn(b"\r\n", output)
1265 def test_assert_equivalent_different_asts(self) -> None:
1266 with self.assertRaises(AssertionError):
1267 black.assert_equivalent("{}", "None")
1269 def test_shhh_click(self) -> None:
1271 from click import _unicodefun
1272 except ModuleNotFoundError:
1273 self.skipTest("Incompatible Click version")
1274 if not hasattr(_unicodefun, "_verify_python3_env"):
1275 self.skipTest("Incompatible Click version")
1276 # First, let's see if Click is crashing with a preferred ASCII charset.
1277 with patch("locale.getpreferredencoding") as gpe:
1278 gpe.return_value = "ASCII"
1279 with self.assertRaises(RuntimeError):
1280 _unicodefun._verify_python3_env() # type: ignore
1281 # Now, let's silence Click...
1283 # ...and confirm it's silent.
1284 with patch("locale.getpreferredencoding") as gpe:
1285 gpe.return_value = "ASCII"
1287 _unicodefun._verify_python3_env() # type: ignore
1288 except RuntimeError as re:
1289 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1291 def test_root_logger_not_used_directly(self) -> None:
1292 def fail(*args: Any, **kwargs: Any) -> None:
1293 self.fail("Record created with root logger")
1295 with patch.multiple(
1304 ff(THIS_DIR / "util.py")
1306 def test_invalid_config_return_code(self) -> None:
1307 tmp_file = Path(black.dump_to_file())
1309 tmp_config = Path(black.dump_to_file())
1311 args = ["--config", str(tmp_config), str(tmp_file)]
1312 self.invokeBlack(args, exit_code=2, ignore_config=False)
1316 def test_parse_pyproject_toml(self) -> None:
1317 test_toml_file = THIS_DIR / "test.toml"
1318 config = black.parse_pyproject_toml(str(test_toml_file))
1319 self.assertEqual(config["verbose"], 1)
1320 self.assertEqual(config["check"], "no")
1321 self.assertEqual(config["diff"], "y")
1322 self.assertEqual(config["color"], True)
1323 self.assertEqual(config["line_length"], 79)
1324 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1325 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1326 self.assertEqual(config["exclude"], r"\.pyi?$")
1327 self.assertEqual(config["include"], r"\.py?$")
1329 def test_read_pyproject_toml(self) -> None:
1330 test_toml_file = THIS_DIR / "test.toml"
1331 fake_ctx = FakeContext()
1332 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1333 config = fake_ctx.default_map
1334 self.assertEqual(config["verbose"], "1")
1335 self.assertEqual(config["check"], "no")
1336 self.assertEqual(config["diff"], "y")
1337 self.assertEqual(config["color"], "True")
1338 self.assertEqual(config["line_length"], "79")
1339 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1340 self.assertEqual(config["exclude"], r"\.pyi?$")
1341 self.assertEqual(config["include"], r"\.py?$")
1343 @pytest.mark.incompatible_with_mypyc
1344 def test_find_project_root(self) -> None:
1345 with TemporaryDirectory() as workspace:
1346 root = Path(workspace)
1347 test_dir = root / "test"
1350 src_dir = root / "src"
1353 root_pyproject = root / "pyproject.toml"
1354 root_pyproject.touch()
1355 src_pyproject = src_dir / "pyproject.toml"
1356 src_pyproject.touch()
1357 src_python = src_dir / "foo.py"
1361 black.find_project_root((src_dir, test_dir)),
1362 (root.resolve(), "pyproject.toml"),
1365 black.find_project_root((src_dir,)),
1366 (src_dir.resolve(), "pyproject.toml"),
1369 black.find_project_root((src_python,)),
1370 (src_dir.resolve(), "pyproject.toml"),
1374 "black.files.find_user_pyproject_toml",
1375 black.files.find_user_pyproject_toml.__wrapped__,
1377 def test_find_user_pyproject_toml_linux(self) -> None:
1378 if system() == "Windows":
1381 # Test if XDG_CONFIG_HOME is checked
1382 with TemporaryDirectory() as workspace:
1383 tmp_user_config = Path(workspace) / "black"
1384 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1386 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1389 # Test fallback for XDG_CONFIG_HOME
1390 with patch.dict("os.environ"):
1391 os.environ.pop("XDG_CONFIG_HOME", None)
1392 fallback_user_config = Path("~/.config").expanduser() / "black"
1394 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1397 def test_find_user_pyproject_toml_windows(self) -> None:
1398 if system() != "Windows":
1401 user_config_path = Path.home() / ".black"
1403 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1406 def test_bpo_33660_workaround(self) -> None:
1407 if system() == "Windows":
1410 # https://bugs.python.org/issue33660
1412 with change_directory(root):
1413 path = Path("workspace") / "project"
1414 report = black.Report(verbose=True)
1415 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1416 self.assertEqual(normalized_path, "workspace/project")
1418 def test_newline_comment_interaction(self) -> None:
1419 source = "class A:\\\r\n# type: ignore\n pass\n"
1420 output = black.format_str(source, mode=DEFAULT_MODE)
1421 black.assert_stable(source, output, mode=DEFAULT_MODE)
1423 def test_bpo_2142_workaround(self) -> None:
1425 # https://bugs.python.org/issue2142
1427 source, _ = read_data("missing_final_newline.py")
1428 # read_data adds a trailing newline
1429 source = source.rstrip()
1430 expected, _ = read_data("missing_final_newline.diff")
1431 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1432 diff_header = re.compile(
1433 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1434 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1437 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1438 self.assertEqual(result.exit_code, 0)
1441 actual = result.output
1442 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1443 self.assertEqual(actual, expected)
1446 def compare_results(
1447 result: click.testing.Result, expected_value: str, expected_exit_code: int
1449 """Helper method to test the value and exit code of a click Result."""
1451 result.output == expected_value
1452 ), "The output did not match the expected value."
1453 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1455 def test_code_option(self) -> None:
1456 """Test the code option with no changes."""
1457 code = 'print("Hello world")\n'
1458 args = ["--code", code]
1459 result = CliRunner().invoke(black.main, args)
1461 self.compare_results(result, code, 0)
1463 def test_code_option_changed(self) -> None:
1464 """Test the code option when changes are required."""
1465 code = "print('hello world')"
1466 formatted = black.format_str(code, mode=DEFAULT_MODE)
1468 args = ["--code", code]
1469 result = CliRunner().invoke(black.main, args)
1471 self.compare_results(result, formatted, 0)
1473 def test_code_option_check(self) -> None:
1474 """Test the code option when check is passed."""
1475 args = ["--check", "--code", 'print("Hello world")\n']
1476 result = CliRunner().invoke(black.main, args)
1477 self.compare_results(result, "", 0)
1479 def test_code_option_check_changed(self) -> None:
1480 """Test the code option when changes are required, and check is passed."""
1481 args = ["--check", "--code", "print('hello world')"]
1482 result = CliRunner().invoke(black.main, args)
1483 self.compare_results(result, "", 1)
1485 def test_code_option_diff(self) -> None:
1486 """Test the code option when diff is passed."""
1487 code = "print('hello world')"
1488 formatted = black.format_str(code, mode=DEFAULT_MODE)
1489 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1491 args = ["--diff", "--code", code]
1492 result = CliRunner().invoke(black.main, args)
1494 # Remove time from diff
1495 output = DIFF_TIME.sub("", result.output)
1497 assert output == result_diff, "The output did not match the expected value."
1498 assert result.exit_code == 0, "The exit code is incorrect."
1500 def test_code_option_color_diff(self) -> None:
1501 """Test the code option when color and diff are passed."""
1502 code = "print('hello world')"
1503 formatted = black.format_str(code, mode=DEFAULT_MODE)
1505 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1506 result_diff = color_diff(result_diff)
1508 args = ["--diff", "--color", "--code", code]
1509 result = CliRunner().invoke(black.main, args)
1511 # Remove time from diff
1512 output = DIFF_TIME.sub("", result.output)
1514 assert output == result_diff, "The output did not match the expected value."
1515 assert result.exit_code == 0, "The exit code is incorrect."
1517 @pytest.mark.incompatible_with_mypyc
1518 def test_code_option_safe(self) -> None:
1519 """Test that the code option throws an error when the sanity checks fail."""
1520 # Patch black.assert_equivalent to ensure the sanity checks fail
1521 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1522 code = 'print("Hello world")'
1523 error_msg = f"{code}\nerror: cannot format <string>: \n"
1525 args = ["--safe", "--code", code]
1526 result = CliRunner().invoke(black.main, args)
1528 self.compare_results(result, error_msg, 123)
1530 def test_code_option_fast(self) -> None:
1531 """Test that the code option ignores errors when the sanity checks fail."""
1532 # Patch black.assert_equivalent to ensure the sanity checks fail
1533 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1534 code = 'print("Hello world")'
1535 formatted = black.format_str(code, mode=DEFAULT_MODE)
1537 args = ["--fast", "--code", code]
1538 result = CliRunner().invoke(black.main, args)
1540 self.compare_results(result, formatted, 0)
1542 @pytest.mark.incompatible_with_mypyc
1543 def test_code_option_config(self) -> None:
1545 Test that the code option finds the pyproject.toml in the current directory.
1547 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1548 args = ["--code", "print"]
1549 # This is the only directory known to contain a pyproject.toml
1550 with change_directory(PROJECT_ROOT):
1551 CliRunner().invoke(black.main, args)
1552 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1555 len(parse.mock_calls) >= 1
1556 ), "Expected config parse to be called with the current directory."
1558 _, call_args, _ = parse.mock_calls[0]
1560 call_args[0].lower() == str(pyproject_path).lower()
1561 ), "Incorrect config loaded."
1563 @pytest.mark.incompatible_with_mypyc
1564 def test_code_option_parent_config(self) -> None:
1566 Test that the code option finds the pyproject.toml in the parent directory.
1568 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1569 with change_directory(THIS_DIR):
1570 args = ["--code", "print"]
1571 CliRunner().invoke(black.main, args)
1573 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1575 len(parse.mock_calls) >= 1
1576 ), "Expected config parse to be called with the current directory."
1578 _, call_args, _ = parse.mock_calls[0]
1580 call_args[0].lower() == str(pyproject_path).lower()
1581 ), "Incorrect config loaded."
1583 def test_for_handled_unexpected_eof_error(self) -> None:
1585 Test that an unexpected EOF SyntaxError is nicely presented.
1587 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1588 black.lib2to3_parse("print(", {})
1590 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1592 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1593 with pytest.raises(AssertionError) as err:
1594 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1597 # Unfortunately the SyntaxError message has changed in newer versions so we
1598 # can't match it directly.
1599 err.match("invalid character")
1600 err.match(r"\(<unknown>, line 1\)")
1604 def test_cache_broken_file(self) -> None:
1606 with cache_dir() as workspace:
1607 cache_file = get_cache_file(mode)
1608 cache_file.write_text("this is not a pickle")
1609 assert black.read_cache(mode) == {}
1610 src = (workspace / "test.py").resolve()
1611 src.write_text("print('hello')")
1612 invokeBlack([str(src)])
1613 cache = black.read_cache(mode)
1614 assert str(src) in cache
1616 def test_cache_single_file_already_cached(self) -> None:
1618 with cache_dir() as workspace:
1619 src = (workspace / "test.py").resolve()
1620 src.write_text("print('hello')")
1621 black.write_cache({}, [src], mode)
1622 invokeBlack([str(src)])
1623 assert src.read_text() == "print('hello')"
1626 def test_cache_multiple_files(self) -> None:
1628 with cache_dir() as workspace, patch(
1629 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1631 one = (workspace / "one.py").resolve()
1632 with one.open("w") as fobj:
1633 fobj.write("print('hello')")
1634 two = (workspace / "two.py").resolve()
1635 with two.open("w") as fobj:
1636 fobj.write("print('hello')")
1637 black.write_cache({}, [one], mode)
1638 invokeBlack([str(workspace)])
1639 with one.open("r") as fobj:
1640 assert fobj.read() == "print('hello')"
1641 with two.open("r") as fobj:
1642 assert fobj.read() == 'print("hello")\n'
1643 cache = black.read_cache(mode)
1644 assert str(one) in cache
1645 assert str(two) in cache
1647 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1648 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1650 with cache_dir() as workspace:
1651 src = (workspace / "test.py").resolve()
1652 with src.open("w") as fobj:
1653 fobj.write("print('hello')")
1654 with patch("black.read_cache") as read_cache, patch(
1657 cmd = [str(src), "--diff"]
1659 cmd.append("--color")
1661 cache_file = get_cache_file(mode)
1662 assert cache_file.exists() is False
1663 write_cache.assert_not_called()
1664 read_cache.assert_not_called()
1666 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1668 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1669 with cache_dir() as workspace:
1670 for tag in range(0, 4):
1671 src = (workspace / f"test{tag}.py").resolve()
1672 with src.open("w") as fobj:
1673 fobj.write("print('hello')")
1674 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1675 cmd = ["--diff", str(workspace)]
1677 cmd.append("--color")
1678 invokeBlack(cmd, exit_code=0)
1679 # this isn't quite doing what we want, but if it _isn't_
1680 # called then we cannot be using the lock it provides
1683 def test_no_cache_when_stdin(self) -> None:
1686 result = CliRunner().invoke(
1687 black.main, ["-"], input=BytesIO(b"print('hello')")
1689 assert not result.exit_code
1690 cache_file = get_cache_file(mode)
1691 assert not cache_file.exists()
1693 def test_read_cache_no_cachefile(self) -> None:
1696 assert black.read_cache(mode) == {}
1698 def test_write_cache_read_cache(self) -> None:
1700 with cache_dir() as workspace:
1701 src = (workspace / "test.py").resolve()
1703 black.write_cache({}, [src], mode)
1704 cache = black.read_cache(mode)
1705 assert str(src) in cache
1706 assert cache[str(src)] == black.get_cache_info(src)
1708 def test_filter_cached(self) -> None:
1709 with TemporaryDirectory() as workspace:
1710 path = Path(workspace)
1711 uncached = (path / "uncached").resolve()
1712 cached = (path / "cached").resolve()
1713 cached_but_changed = (path / "changed").resolve()
1716 cached_but_changed.touch()
1718 str(cached): black.get_cache_info(cached),
1719 str(cached_but_changed): (0.0, 0),
1721 todo, done = black.filter_cached(
1722 cache, {uncached, cached, cached_but_changed}
1724 assert todo == {uncached, cached_but_changed}
1725 assert done == {cached}
1727 def test_write_cache_creates_directory_if_needed(self) -> None:
1729 with cache_dir(exists=False) as workspace:
1730 assert not workspace.exists()
1731 black.write_cache({}, [], mode)
1732 assert workspace.exists()
1735 def test_failed_formatting_does_not_get_cached(self) -> None:
1737 with cache_dir() as workspace, patch(
1738 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1740 failing = (workspace / "failing.py").resolve()
1741 with failing.open("w") as fobj:
1742 fobj.write("not actually python")
1743 clean = (workspace / "clean.py").resolve()
1744 with clean.open("w") as fobj:
1745 fobj.write('print("hello")\n')
1746 invokeBlack([str(workspace)], exit_code=123)
1747 cache = black.read_cache(mode)
1748 assert str(failing) not in cache
1749 assert str(clean) in cache
1751 def test_write_cache_write_fail(self) -> None:
1753 with cache_dir(), patch.object(Path, "open") as mock:
1754 mock.side_effect = OSError
1755 black.write_cache({}, [], mode)
1757 def test_read_cache_line_lengths(self) -> None:
1759 short_mode = replace(DEFAULT_MODE, line_length=1)
1760 with cache_dir() as workspace:
1761 path = (workspace / "file.py").resolve()
1763 black.write_cache({}, [path], mode)
1764 one = black.read_cache(mode)
1765 assert str(path) in one
1766 two = black.read_cache(short_mode)
1767 assert str(path) not in two
1770 def assert_collected_sources(
1771 src: Sequence[Union[str, Path]],
1772 expected: Sequence[Union[str, Path]],
1774 ctx: Optional[FakeContext] = None,
1775 exclude: Optional[str] = None,
1776 include: Optional[str] = None,
1777 extend_exclude: Optional[str] = None,
1778 force_exclude: Optional[str] = None,
1779 stdin_filename: Optional[str] = None,
1781 gs_src = tuple(str(Path(s)) for s in src)
1782 gs_expected = [Path(s) for s in expected]
1783 gs_exclude = None if exclude is None else compile_pattern(exclude)
1784 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
1785 gs_extend_exclude = (
1786 None if extend_exclude is None else compile_pattern(extend_exclude)
1788 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
1789 collected = black.get_sources(
1790 ctx=ctx or FakeContext(),
1796 extend_exclude=gs_extend_exclude,
1797 force_exclude=gs_force_exclude,
1798 report=black.Report(),
1799 stdin_filename=stdin_filename,
1801 assert sorted(collected) == sorted(gs_expected)
1804 class TestFileCollection:
1805 def test_include_exclude(self) -> None:
1806 path = THIS_DIR / "data" / "include_exclude_tests"
1809 Path(path / "b/dont_exclude/a.py"),
1810 Path(path / "b/dont_exclude/a.pyi"),
1812 assert_collected_sources(
1816 exclude=r"/exclude/|/\.definitely_exclude/",
1819 def test_gitignore_used_as_default(self) -> None:
1820 base = Path(DATA_DIR / "include_exclude_tests")
1822 base / "b/.definitely_exclude/a.py",
1823 base / "b/.definitely_exclude/a.pyi",
1827 ctx.obj["root"] = base
1828 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
1830 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
1831 def test_exclude_for_issue_1572(self) -> None:
1832 # Exclude shouldn't touch files that were explicitly given to Black through the
1833 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1834 # https://github.com/psf/black/issues/1572
1835 path = DATA_DIR / "include_exclude_tests"
1836 src = [path / "b/exclude/a.py"]
1837 expected = [path / "b/exclude/a.py"]
1838 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1840 def test_gitignore_exclude(self) -> None:
1841 path = THIS_DIR / "data" / "include_exclude_tests"
1842 include = re.compile(r"\.pyi?$")
1843 exclude = re.compile(r"")
1844 report = black.Report()
1845 gitignore = PathSpec.from_lines(
1846 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1848 sources: List[Path] = []
1850 Path(path / "b/dont_exclude/a.py"),
1851 Path(path / "b/dont_exclude/a.pyi"),
1853 this_abs = THIS_DIR.resolve()
1855 black.gen_python_files(
1868 assert sorted(expected) == sorted(sources)
1870 def test_nested_gitignore(self) -> None:
1871 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
1872 include = re.compile(r"\.pyi?$")
1873 exclude = re.compile(r"")
1874 root_gitignore = black.files.get_gitignore(path)
1875 report = black.Report()
1876 expected: List[Path] = [
1877 Path(path / "x.py"),
1878 Path(path / "root/b.py"),
1879 Path(path / "root/c.py"),
1880 Path(path / "root/child/c.py"),
1882 this_abs = THIS_DIR.resolve()
1884 black.gen_python_files(
1897 assert sorted(expected) == sorted(sources)
1899 def test_invalid_gitignore(self) -> None:
1900 path = THIS_DIR / "data" / "invalid_gitignore_tests"
1901 empty_config = path / "pyproject.toml"
1902 result = BlackRunner().invoke(
1903 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1905 assert result.exit_code == 1
1906 assert result.stderr_bytes is not None
1908 gitignore = path / ".gitignore"
1909 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1911 def test_invalid_nested_gitignore(self) -> None:
1912 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
1913 empty_config = path / "pyproject.toml"
1914 result = BlackRunner().invoke(
1915 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1917 assert result.exit_code == 1
1918 assert result.stderr_bytes is not None
1920 gitignore = path / "a" / ".gitignore"
1921 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1923 def test_empty_include(self) -> None:
1924 path = DATA_DIR / "include_exclude_tests"
1927 Path(path / "b/exclude/a.pie"),
1928 Path(path / "b/exclude/a.py"),
1929 Path(path / "b/exclude/a.pyi"),
1930 Path(path / "b/dont_exclude/a.pie"),
1931 Path(path / "b/dont_exclude/a.py"),
1932 Path(path / "b/dont_exclude/a.pyi"),
1933 Path(path / "b/.definitely_exclude/a.pie"),
1934 Path(path / "b/.definitely_exclude/a.py"),
1935 Path(path / "b/.definitely_exclude/a.pyi"),
1936 Path(path / ".gitignore"),
1937 Path(path / "pyproject.toml"),
1939 # Setting exclude explicitly to an empty string to block .gitignore usage.
1940 assert_collected_sources(src, expected, include="", exclude="")
1942 def test_extend_exclude(self) -> None:
1943 path = DATA_DIR / "include_exclude_tests"
1946 Path(path / "b/exclude/a.py"),
1947 Path(path / "b/dont_exclude/a.py"),
1949 assert_collected_sources(
1950 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
1953 @pytest.mark.incompatible_with_mypyc
1954 def test_symlink_out_of_root_directory(self) -> None:
1956 root = THIS_DIR.resolve()
1958 include = re.compile(black.DEFAULT_INCLUDES)
1959 exclude = re.compile(black.DEFAULT_EXCLUDES)
1960 report = black.Report()
1961 gitignore = PathSpec.from_lines("gitwildmatch", [])
1962 # `child` should behave like a symlink which resolved path is clearly
1963 # outside of the `root` directory.
1964 path.iterdir.return_value = [child]
1965 child.resolve.return_value = Path("/a/b/c")
1966 child.as_posix.return_value = "/a/b/c"
1967 child.is_symlink.return_value = True
1970 black.gen_python_files(
1983 except ValueError as ve:
1984 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
1985 path.iterdir.assert_called_once()
1986 child.resolve.assert_called_once()
1987 child.is_symlink.assert_called_once()
1988 # `child` should behave like a strange file which resolved path is clearly
1989 # outside of the `root` directory.
1990 child.is_symlink.return_value = False
1991 with pytest.raises(ValueError):
1993 black.gen_python_files(
2006 path.iterdir.assert_called()
2007 assert path.iterdir.call_count == 2
2008 child.resolve.assert_called()
2009 assert child.resolve.call_count == 2
2010 child.is_symlink.assert_called()
2011 assert child.is_symlink.call_count == 2
2013 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2014 def test_get_sources_with_stdin(self) -> None:
2017 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2019 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2020 def test_get_sources_with_stdin_filename(self) -> None:
2022 stdin_filename = str(THIS_DIR / "data/collections.py")
2023 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2024 assert_collected_sources(
2027 exclude=r"/exclude/a\.py",
2028 stdin_filename=stdin_filename,
2031 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2032 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2033 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2034 # file being passed directly. This is the same as
2035 # test_exclude_for_issue_1572
2036 path = DATA_DIR / "include_exclude_tests"
2038 stdin_filename = str(path / "b/exclude/a.py")
2039 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2040 assert_collected_sources(
2043 exclude=r"/exclude/|a\.py",
2044 stdin_filename=stdin_filename,
2047 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2048 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2049 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2050 # file being passed directly. This is the same as
2051 # test_exclude_for_issue_1572
2053 path = THIS_DIR / "data" / "include_exclude_tests"
2054 stdin_filename = str(path / "b/exclude/a.py")
2055 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2056 assert_collected_sources(
2059 extend_exclude=r"/exclude/|a\.py",
2060 stdin_filename=stdin_filename,
2063 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2064 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2065 # Force exclude should exclude the file when passing it through
2067 path = THIS_DIR / "data" / "include_exclude_tests"
2068 stdin_filename = str(path / "b/exclude/a.py")
2069 assert_collected_sources(
2072 force_exclude=r"/exclude/|a\.py",
2073 stdin_filename=stdin_filename,
2078 with open(black.__file__, "r", encoding="utf-8") as _bf:
2079 black_source_lines = _bf.readlines()
2080 except UnicodeDecodeError:
2081 if not black.COMPILED:
2086 frame: types.FrameType, event: str, arg: Any
2087 ) -> Callable[[types.FrameType, str, Any], Any]:
2088 """Show function calls `from black/__init__.py` as they happen.
2090 Register this with `sys.settrace()` in a test you're debugging.
2095 stack = len(inspect.stack()) - 19
2097 filename = frame.f_code.co_filename
2098 lineno = frame.f_lineno
2099 func_sig_lineno = lineno - 1
2100 funcname = black_source_lines[func_sig_lineno].strip()
2101 while funcname.startswith("@"):
2102 func_sig_lineno += 1
2103 funcname = black_source_lines[func_sig_lineno].strip()
2104 if "black/__init__.py" in filename:
2105 print(f"{' ' * stack}{lineno}:{funcname}")