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_piping(self) -> None:
154 source, expected = read_data("src/black/__init__", data=False)
155 result = BlackRunner().invoke(
157 ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
158 input=BytesIO(source.encode("utf8")),
160 self.assertEqual(result.exit_code, 0)
161 self.assertFormatEqual(expected, result.output)
162 if source != result.output:
163 black.assert_equivalent(source, result.output)
164 black.assert_stable(source, result.output, DEFAULT_MODE)
166 def test_piping_diff(self) -> None:
167 diff_header = re.compile(
168 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
171 source, _ = read_data("expression.py")
172 expected, _ = read_data("expression.diff")
173 config = THIS_DIR / "data" / "empty_pyproject.toml"
177 f"--line-length={black.DEFAULT_LINE_LENGTH}",
179 f"--config={config}",
181 result = BlackRunner().invoke(
182 black.main, args, input=BytesIO(source.encode("utf8"))
184 self.assertEqual(result.exit_code, 0)
185 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
186 actual = actual.rstrip() + "\n" # the diff output has a trailing space
187 self.assertEqual(expected, actual)
189 def test_piping_diff_with_color(self) -> None:
190 source, _ = read_data("expression.py")
191 config = THIS_DIR / "data" / "empty_pyproject.toml"
195 f"--line-length={black.DEFAULT_LINE_LENGTH}",
198 f"--config={config}",
200 result = BlackRunner().invoke(
201 black.main, args, input=BytesIO(source.encode("utf8"))
203 actual = result.output
204 # Again, the contents are checked in a different test, so only look for colors.
205 self.assertIn("\033[1m", actual)
206 self.assertIn("\033[36m", actual)
207 self.assertIn("\033[32m", actual)
208 self.assertIn("\033[31m", actual)
209 self.assertIn("\033[0m", actual)
211 @patch("black.dump_to_file", dump_to_stderr)
212 def _test_wip(self) -> None:
213 source, expected = read_data("wip")
214 sys.settrace(tracefunc)
217 experimental_string_processing=False,
218 target_versions={black.TargetVersion.PY38},
220 actual = fs(source, mode=mode)
222 self.assertFormatEqual(expected, actual)
223 black.assert_equivalent(source, actual)
224 black.assert_stable(source, actual, black.FileMode())
226 @unittest.expectedFailure
227 @patch("black.dump_to_file", dump_to_stderr)
228 def test_trailing_comma_optional_parens_stability1(self) -> None:
229 source, _expected = read_data("trailing_comma_optional_parens1")
231 black.assert_stable(source, actual, DEFAULT_MODE)
233 @unittest.expectedFailure
234 @patch("black.dump_to_file", dump_to_stderr)
235 def test_trailing_comma_optional_parens_stability2(self) -> None:
236 source, _expected = read_data("trailing_comma_optional_parens2")
238 black.assert_stable(source, actual, DEFAULT_MODE)
240 @unittest.expectedFailure
241 @patch("black.dump_to_file", dump_to_stderr)
242 def test_trailing_comma_optional_parens_stability3(self) -> None:
243 source, _expected = read_data("trailing_comma_optional_parens3")
245 black.assert_stable(source, actual, DEFAULT_MODE)
247 @patch("black.dump_to_file", dump_to_stderr)
248 def test_trailing_comma_optional_parens_stability1_pass2(self) -> None:
249 source, _expected = read_data("trailing_comma_optional_parens1")
250 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
251 black.assert_stable(source, actual, DEFAULT_MODE)
253 @patch("black.dump_to_file", dump_to_stderr)
254 def test_trailing_comma_optional_parens_stability2_pass2(self) -> None:
255 source, _expected = read_data("trailing_comma_optional_parens2")
256 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
257 black.assert_stable(source, actual, DEFAULT_MODE)
259 @patch("black.dump_to_file", dump_to_stderr)
260 def test_trailing_comma_optional_parens_stability3_pass2(self) -> None:
261 source, _expected = read_data("trailing_comma_optional_parens3")
262 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
263 black.assert_stable(source, actual, DEFAULT_MODE)
265 def test_pep_572_version_detection(self) -> None:
266 source, _ = read_data("pep_572")
267 root = black.lib2to3_parse(source)
268 features = black.get_features_used(root)
269 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
270 versions = black.detect_target_versions(root)
271 self.assertIn(black.TargetVersion.PY38, versions)
273 def test_expression_ff(self) -> None:
274 source, expected = read_data("expression")
275 tmp_file = Path(black.dump_to_file(source))
277 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
278 with open(tmp_file, encoding="utf8") as f:
282 self.assertFormatEqual(expected, actual)
283 with patch("black.dump_to_file", dump_to_stderr):
284 black.assert_equivalent(source, actual)
285 black.assert_stable(source, actual, DEFAULT_MODE)
287 def test_expression_diff(self) -> None:
288 source, _ = read_data("expression.py")
289 config = THIS_DIR / "data" / "empty_pyproject.toml"
290 expected, _ = read_data("expression.diff")
291 tmp_file = Path(black.dump_to_file(source))
292 diff_header = re.compile(
293 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
294 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
297 result = BlackRunner().invoke(
298 black.main, ["--diff", str(tmp_file), f"--config={config}"]
300 self.assertEqual(result.exit_code, 0)
303 actual = result.output
304 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
305 if expected != actual:
306 dump = black.dump_to_file(actual)
308 "Expected diff isn't equal to the actual. If you made changes to"
309 " expression.py and this is an anticipated difference, overwrite"
310 f" tests/data/expression.diff with {dump}"
312 self.assertEqual(expected, actual, msg)
314 def test_expression_diff_with_color(self) -> None:
315 source, _ = read_data("expression.py")
316 config = THIS_DIR / "data" / "empty_pyproject.toml"
317 expected, _ = read_data("expression.diff")
318 tmp_file = Path(black.dump_to_file(source))
320 result = BlackRunner().invoke(
321 black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
325 actual = result.output
326 # We check the contents of the diff in `test_expression_diff`. All
327 # we need to check here is that color codes exist in the result.
328 self.assertIn("\033[1m", actual)
329 self.assertIn("\033[36m", actual)
330 self.assertIn("\033[32m", actual)
331 self.assertIn("\033[31m", actual)
332 self.assertIn("\033[0m", actual)
334 def test_detect_pos_only_arguments(self) -> None:
335 source, _ = read_data("pep_570")
336 root = black.lib2to3_parse(source)
337 features = black.get_features_used(root)
338 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
339 versions = black.detect_target_versions(root)
340 self.assertIn(black.TargetVersion.PY38, versions)
342 @patch("black.dump_to_file", dump_to_stderr)
343 def test_string_quotes(self) -> None:
344 source, expected = read_data("string_quotes")
345 mode = black.Mode(experimental_string_processing=True)
346 assert_format(source, expected, mode)
347 mode = replace(mode, string_normalization=False)
348 not_normalized = fs(source, mode=mode)
349 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
350 black.assert_equivalent(source, not_normalized)
351 black.assert_stable(source, not_normalized, mode=mode)
353 def test_skip_magic_trailing_comma(self) -> None:
354 source, _ = read_data("expression.py")
355 expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
356 tmp_file = Path(black.dump_to_file(source))
357 diff_header = re.compile(
358 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
359 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
362 result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
363 self.assertEqual(result.exit_code, 0)
366 actual = result.output
367 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
368 actual = actual.rstrip() + "\n" # the diff output has a trailing space
369 if expected != actual:
370 dump = black.dump_to_file(actual)
372 "Expected diff isn't equal to the actual. If you made changes to"
373 " expression.py and this is an anticipated difference, overwrite"
374 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
376 self.assertEqual(expected, actual, msg)
378 @patch("black.dump_to_file", dump_to_stderr)
379 def test_async_as_identifier(self) -> None:
380 source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
381 source, expected = read_data("async_as_identifier")
383 self.assertFormatEqual(expected, actual)
384 major, minor = sys.version_info[:2]
385 if major < 3 or (major <= 3 and minor < 7):
386 black.assert_equivalent(source, actual)
387 black.assert_stable(source, actual, DEFAULT_MODE)
388 # ensure black can parse this when the target is 3.6
389 self.invokeBlack([str(source_path), "--target-version", "py36"])
390 # but not on 3.7, because async/await is no longer an identifier
391 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
393 @patch("black.dump_to_file", dump_to_stderr)
394 def test_python37(self) -> None:
395 source_path = (THIS_DIR / "data" / "python37.py").resolve()
396 source, expected = read_data("python37")
398 self.assertFormatEqual(expected, actual)
399 major, minor = sys.version_info[:2]
400 if major > 3 or (major == 3 and minor >= 7):
401 black.assert_equivalent(source, actual)
402 black.assert_stable(source, actual, DEFAULT_MODE)
403 # ensure black can parse this when the target is 3.7
404 self.invokeBlack([str(source_path), "--target-version", "py37"])
405 # but not on 3.6, because we use async as a reserved keyword
406 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
408 def test_tab_comment_indentation(self) -> None:
409 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
410 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
411 self.assertFormatEqual(contents_spc, fs(contents_spc))
412 self.assertFormatEqual(contents_spc, fs(contents_tab))
414 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\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 # mixed tabs and spaces (valid Python 2 code)
420 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
421 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
422 self.assertFormatEqual(contents_spc, fs(contents_spc))
423 self.assertFormatEqual(contents_spc, fs(contents_tab))
425 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\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 def test_report_verbose(self) -> None:
431 report = Report(verbose=True)
435 def out(msg: str, **kwargs: Any) -> None:
436 out_lines.append(msg)
438 def err(msg: str, **kwargs: Any) -> None:
439 err_lines.append(msg)
441 with patch("black.output._out", out), patch("black.output._err", err):
442 report.done(Path("f1"), black.Changed.NO)
443 self.assertEqual(len(out_lines), 1)
444 self.assertEqual(len(err_lines), 0)
445 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
446 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
447 self.assertEqual(report.return_code, 0)
448 report.done(Path("f2"), black.Changed.YES)
449 self.assertEqual(len(out_lines), 2)
450 self.assertEqual(len(err_lines), 0)
451 self.assertEqual(out_lines[-1], "reformatted f2")
453 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
455 report.done(Path("f3"), black.Changed.CACHED)
456 self.assertEqual(len(out_lines), 3)
457 self.assertEqual(len(err_lines), 0)
459 out_lines[-1], "f3 wasn't modified on disk since last run."
462 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
464 self.assertEqual(report.return_code, 0)
466 self.assertEqual(report.return_code, 1)
468 report.failed(Path("e1"), "boom")
469 self.assertEqual(len(out_lines), 3)
470 self.assertEqual(len(err_lines), 1)
471 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
473 unstyle(str(report)),
474 "1 file reformatted, 2 files left unchanged, 1 file failed to"
477 self.assertEqual(report.return_code, 123)
478 report.done(Path("f3"), black.Changed.YES)
479 self.assertEqual(len(out_lines), 4)
480 self.assertEqual(len(err_lines), 1)
481 self.assertEqual(out_lines[-1], "reformatted f3")
483 unstyle(str(report)),
484 "2 files reformatted, 2 files left unchanged, 1 file failed to"
487 self.assertEqual(report.return_code, 123)
488 report.failed(Path("e2"), "boom")
489 self.assertEqual(len(out_lines), 4)
490 self.assertEqual(len(err_lines), 2)
491 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
493 unstyle(str(report)),
494 "2 files reformatted, 2 files left unchanged, 2 files failed to"
497 self.assertEqual(report.return_code, 123)
498 report.path_ignored(Path("wat"), "no match")
499 self.assertEqual(len(out_lines), 5)
500 self.assertEqual(len(err_lines), 2)
501 self.assertEqual(out_lines[-1], "wat ignored: no match")
503 unstyle(str(report)),
504 "2 files reformatted, 2 files left unchanged, 2 files failed to"
507 self.assertEqual(report.return_code, 123)
508 report.done(Path("f4"), black.Changed.NO)
509 self.assertEqual(len(out_lines), 6)
510 self.assertEqual(len(err_lines), 2)
511 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
513 unstyle(str(report)),
514 "2 files reformatted, 3 files left unchanged, 2 files failed to"
517 self.assertEqual(report.return_code, 123)
520 unstyle(str(report)),
521 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
522 " would fail to reformat.",
527 unstyle(str(report)),
528 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
529 " would fail to reformat.",
532 def test_report_quiet(self) -> None:
533 report = Report(quiet=True)
537 def out(msg: str, **kwargs: Any) -> None:
538 out_lines.append(msg)
540 def err(msg: str, **kwargs: Any) -> None:
541 err_lines.append(msg)
543 with patch("black.output._out", out), patch("black.output._err", err):
544 report.done(Path("f1"), black.Changed.NO)
545 self.assertEqual(len(out_lines), 0)
546 self.assertEqual(len(err_lines), 0)
547 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
548 self.assertEqual(report.return_code, 0)
549 report.done(Path("f2"), black.Changed.YES)
550 self.assertEqual(len(out_lines), 0)
551 self.assertEqual(len(err_lines), 0)
553 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
555 report.done(Path("f3"), black.Changed.CACHED)
556 self.assertEqual(len(out_lines), 0)
557 self.assertEqual(len(err_lines), 0)
559 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
561 self.assertEqual(report.return_code, 0)
563 self.assertEqual(report.return_code, 1)
565 report.failed(Path("e1"), "boom")
566 self.assertEqual(len(out_lines), 0)
567 self.assertEqual(len(err_lines), 1)
568 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
570 unstyle(str(report)),
571 "1 file reformatted, 2 files left unchanged, 1 file failed to"
574 self.assertEqual(report.return_code, 123)
575 report.done(Path("f3"), black.Changed.YES)
576 self.assertEqual(len(out_lines), 0)
577 self.assertEqual(len(err_lines), 1)
579 unstyle(str(report)),
580 "2 files reformatted, 2 files left unchanged, 1 file failed to"
583 self.assertEqual(report.return_code, 123)
584 report.failed(Path("e2"), "boom")
585 self.assertEqual(len(out_lines), 0)
586 self.assertEqual(len(err_lines), 2)
587 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
589 unstyle(str(report)),
590 "2 files reformatted, 2 files left unchanged, 2 files failed to"
593 self.assertEqual(report.return_code, 123)
594 report.path_ignored(Path("wat"), "no match")
595 self.assertEqual(len(out_lines), 0)
596 self.assertEqual(len(err_lines), 2)
598 unstyle(str(report)),
599 "2 files reformatted, 2 files left unchanged, 2 files failed to"
602 self.assertEqual(report.return_code, 123)
603 report.done(Path("f4"), black.Changed.NO)
604 self.assertEqual(len(out_lines), 0)
605 self.assertEqual(len(err_lines), 2)
607 unstyle(str(report)),
608 "2 files reformatted, 3 files left unchanged, 2 files failed to"
611 self.assertEqual(report.return_code, 123)
614 unstyle(str(report)),
615 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
616 " would fail to reformat.",
621 unstyle(str(report)),
622 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
623 " would fail to reformat.",
626 def test_report_normal(self) -> None:
627 report = black.Report()
631 def out(msg: str, **kwargs: Any) -> None:
632 out_lines.append(msg)
634 def err(msg: str, **kwargs: Any) -> None:
635 err_lines.append(msg)
637 with patch("black.output._out", out), patch("black.output._err", err):
638 report.done(Path("f1"), black.Changed.NO)
639 self.assertEqual(len(out_lines), 0)
640 self.assertEqual(len(err_lines), 0)
641 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
642 self.assertEqual(report.return_code, 0)
643 report.done(Path("f2"), black.Changed.YES)
644 self.assertEqual(len(out_lines), 1)
645 self.assertEqual(len(err_lines), 0)
646 self.assertEqual(out_lines[-1], "reformatted f2")
648 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
650 report.done(Path("f3"), black.Changed.CACHED)
651 self.assertEqual(len(out_lines), 1)
652 self.assertEqual(len(err_lines), 0)
653 self.assertEqual(out_lines[-1], "reformatted f2")
655 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
657 self.assertEqual(report.return_code, 0)
659 self.assertEqual(report.return_code, 1)
661 report.failed(Path("e1"), "boom")
662 self.assertEqual(len(out_lines), 1)
663 self.assertEqual(len(err_lines), 1)
664 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
666 unstyle(str(report)),
667 "1 file reformatted, 2 files left unchanged, 1 file failed to"
670 self.assertEqual(report.return_code, 123)
671 report.done(Path("f3"), black.Changed.YES)
672 self.assertEqual(len(out_lines), 2)
673 self.assertEqual(len(err_lines), 1)
674 self.assertEqual(out_lines[-1], "reformatted f3")
676 unstyle(str(report)),
677 "2 files reformatted, 2 files left unchanged, 1 file failed to"
680 self.assertEqual(report.return_code, 123)
681 report.failed(Path("e2"), "boom")
682 self.assertEqual(len(out_lines), 2)
683 self.assertEqual(len(err_lines), 2)
684 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
686 unstyle(str(report)),
687 "2 files reformatted, 2 files left unchanged, 2 files failed to"
690 self.assertEqual(report.return_code, 123)
691 report.path_ignored(Path("wat"), "no match")
692 self.assertEqual(len(out_lines), 2)
693 self.assertEqual(len(err_lines), 2)
695 unstyle(str(report)),
696 "2 files reformatted, 2 files left unchanged, 2 files failed to"
699 self.assertEqual(report.return_code, 123)
700 report.done(Path("f4"), black.Changed.NO)
701 self.assertEqual(len(out_lines), 2)
702 self.assertEqual(len(err_lines), 2)
704 unstyle(str(report)),
705 "2 files reformatted, 3 files left unchanged, 2 files failed to"
708 self.assertEqual(report.return_code, 123)
711 unstyle(str(report)),
712 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
713 " would fail to reformat.",
718 unstyle(str(report)),
719 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
720 " would fail to reformat.",
723 def test_lib2to3_parse(self) -> None:
724 with self.assertRaises(black.InvalidInput):
725 black.lib2to3_parse("invalid syntax")
728 black.lib2to3_parse(straddling)
729 black.lib2to3_parse(straddling, {TargetVersion.PY36})
732 with self.assertRaises(black.InvalidInput):
733 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
735 py3_only = "exec(x, end=y)"
736 black.lib2to3_parse(py3_only)
737 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
739 def test_get_features_used_decorator(self) -> None:
740 # Test the feature detection of new decorator syntax
741 # since this makes some test cases of test_get_features_used()
742 # fails if it fails, this is tested first so that a useful case
744 simples, relaxed = read_data("decorators")
745 # skip explanation comments at the top of the file
746 for simple_test in simples.split("##")[1:]:
747 node = black.lib2to3_parse(simple_test)
748 decorator = str(node.children[0].children[0]).strip()
750 Feature.RELAXED_DECORATORS,
751 black.get_features_used(node),
753 f"decorator '{decorator}' follows python<=3.8 syntax"
754 "but is detected as 3.9+"
755 # f"The full node is\n{node!r}"
758 # skip the '# output' comment at the top of the output part
759 for relaxed_test in relaxed.split("##")[1:]:
760 node = black.lib2to3_parse(relaxed_test)
761 decorator = str(node.children[0].children[0]).strip()
763 Feature.RELAXED_DECORATORS,
764 black.get_features_used(node),
766 f"decorator '{decorator}' uses python3.9+ syntax"
767 "but is detected as python<=3.8"
768 # f"The full node is\n{node!r}"
772 def test_get_features_used(self) -> None:
773 node = black.lib2to3_parse("def f(*, arg): ...\n")
774 self.assertEqual(black.get_features_used(node), set())
775 node = black.lib2to3_parse("def f(*, arg,): ...\n")
776 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
777 node = black.lib2to3_parse("f(*arg,)\n")
779 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
781 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
782 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
783 node = black.lib2to3_parse("123_456\n")
784 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
785 node = black.lib2to3_parse("123456\n")
786 self.assertEqual(black.get_features_used(node), set())
787 source, expected = read_data("function")
788 node = black.lib2to3_parse(source)
789 expected_features = {
790 Feature.TRAILING_COMMA_IN_CALL,
791 Feature.TRAILING_COMMA_IN_DEF,
794 self.assertEqual(black.get_features_used(node), expected_features)
795 node = black.lib2to3_parse(expected)
796 self.assertEqual(black.get_features_used(node), expected_features)
797 source, expected = read_data("expression")
798 node = black.lib2to3_parse(source)
799 self.assertEqual(black.get_features_used(node), set())
800 node = black.lib2to3_parse(expected)
801 self.assertEqual(black.get_features_used(node), set())
802 node = black.lib2to3_parse("lambda a, /, b: ...")
803 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
804 node = black.lib2to3_parse("def fn(a, /, b): ...")
805 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
806 node = black.lib2to3_parse("def fn(): yield a, b")
807 self.assertEqual(black.get_features_used(node), set())
808 node = black.lib2to3_parse("def fn(): return a, b")
809 self.assertEqual(black.get_features_used(node), set())
810 node = black.lib2to3_parse("def fn(): yield *b, c")
811 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
812 node = black.lib2to3_parse("def fn(): return a, *b, c")
813 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
814 node = black.lib2to3_parse("x = a, *b, c")
815 self.assertEqual(black.get_features_used(node), set())
816 node = black.lib2to3_parse("x: Any = regular")
817 self.assertEqual(black.get_features_used(node), set())
818 node = black.lib2to3_parse("x: Any = (regular, regular)")
819 self.assertEqual(black.get_features_used(node), set())
820 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
821 self.assertEqual(black.get_features_used(node), set())
822 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
824 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
827 def test_get_features_used_for_future_flags(self) -> None:
828 for src, features in [
829 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
831 "from __future__ import (other, annotations)",
832 {Feature.FUTURE_ANNOTATIONS},
834 ("a = 1 + 2\nfrom something import annotations", set()),
835 ("from __future__ import x, y", set()),
837 with self.subTest(src=src, features=features):
838 node = black.lib2to3_parse(src)
839 future_imports = black.get_future_imports(node)
841 black.get_features_used(node, future_imports=future_imports),
845 def test_get_future_imports(self) -> None:
846 node = black.lib2to3_parse("\n")
847 self.assertEqual(set(), black.get_future_imports(node))
848 node = black.lib2to3_parse("from __future__ import black\n")
849 self.assertEqual({"black"}, black.get_future_imports(node))
850 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
851 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
852 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
853 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
854 node = black.lib2to3_parse(
855 "from __future__ import multiple\nfrom __future__ import imports\n"
857 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
858 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
859 self.assertEqual({"black"}, black.get_future_imports(node))
860 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
861 self.assertEqual({"black"}, black.get_future_imports(node))
862 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
863 self.assertEqual(set(), black.get_future_imports(node))
864 node = black.lib2to3_parse("from some.module import black\n")
865 self.assertEqual(set(), black.get_future_imports(node))
866 node = black.lib2to3_parse(
867 "from __future__ import unicode_literals as _unicode_literals"
869 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
870 node = black.lib2to3_parse(
871 "from __future__ import unicode_literals as _lol, print"
873 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
875 @pytest.mark.incompatible_with_mypyc
876 def test_debug_visitor(self) -> None:
877 source, _ = read_data("debug_visitor.py")
878 expected, _ = read_data("debug_visitor.out")
882 def out(msg: str, **kwargs: Any) -> None:
883 out_lines.append(msg)
885 def err(msg: str, **kwargs: Any) -> None:
886 err_lines.append(msg)
888 with patch("black.debug.out", out):
889 DebugVisitor.show(source)
890 actual = "\n".join(out_lines) + "\n"
892 if expected != actual:
893 log_name = black.dump_to_file(*out_lines)
897 f"AST print out is different. Actual version dumped to {log_name}",
900 def test_format_file_contents(self) -> None:
903 with self.assertRaises(black.NothingChanged):
904 black.format_file_contents(empty, mode=mode, fast=False)
906 with self.assertRaises(black.NothingChanged):
907 black.format_file_contents(just_nl, mode=mode, fast=False)
908 same = "j = [1, 2, 3]\n"
909 with self.assertRaises(black.NothingChanged):
910 black.format_file_contents(same, mode=mode, fast=False)
911 different = "j = [1,2,3]"
913 actual = black.format_file_contents(different, mode=mode, fast=False)
914 self.assertEqual(expected, actual)
915 invalid = "return if you can"
916 with self.assertRaises(black.InvalidInput) as e:
917 black.format_file_contents(invalid, mode=mode, fast=False)
918 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
920 def test_endmarker(self) -> None:
921 n = black.lib2to3_parse("\n")
922 self.assertEqual(n.type, black.syms.file_input)
923 self.assertEqual(len(n.children), 1)
924 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
926 @pytest.mark.incompatible_with_mypyc
927 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
928 def test_assertFormatEqual(self) -> None:
932 def out(msg: str, **kwargs: Any) -> None:
933 out_lines.append(msg)
935 def err(msg: str, **kwargs: Any) -> None:
936 err_lines.append(msg)
938 with patch("black.output._out", out), patch("black.output._err", err):
939 with self.assertRaises(AssertionError):
940 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
942 out_str = "".join(out_lines)
943 self.assertIn("Expected tree:", out_str)
944 self.assertIn("Actual tree:", out_str)
945 self.assertEqual("".join(err_lines), "")
948 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
949 def test_works_in_mono_process_only_environment(self) -> None:
950 with cache_dir() as workspace:
952 (workspace / "one.py").resolve(),
953 (workspace / "two.py").resolve(),
955 f.write_text('print("hello")\n')
956 self.invokeBlack([str(workspace)])
959 def test_check_diff_use_together(self) -> None:
961 # Files which will be reformatted.
962 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
963 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
964 # Files which will not be reformatted.
965 src2 = (THIS_DIR / "data" / "composition.py").resolve()
966 self.invokeBlack([str(src2), "--diff", "--check"])
967 # Multi file command.
968 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
970 def test_no_files(self) -> None:
972 # Without an argument, black exits with error code 0.
975 def test_broken_symlink(self) -> None:
976 with cache_dir() as workspace:
977 symlink = workspace / "broken_link.py"
979 symlink.symlink_to("nonexistent.py")
980 except (OSError, NotImplementedError) as e:
981 self.skipTest(f"Can't create symlinks: {e}")
982 self.invokeBlack([str(workspace.resolve())])
984 def test_single_file_force_pyi(self) -> None:
985 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
986 contents, expected = read_data("force_pyi")
987 with cache_dir() as workspace:
988 path = (workspace / "file.py").resolve()
989 with open(path, "w") as fh:
991 self.invokeBlack([str(path), "--pyi"])
992 with open(path, "r") as fh:
994 # verify cache with --pyi is separate
995 pyi_cache = black.read_cache(pyi_mode)
996 self.assertIn(str(path), pyi_cache)
997 normal_cache = black.read_cache(DEFAULT_MODE)
998 self.assertNotIn(str(path), normal_cache)
999 self.assertFormatEqual(expected, actual)
1000 black.assert_equivalent(contents, actual)
1001 black.assert_stable(contents, actual, pyi_mode)
1004 def test_multi_file_force_pyi(self) -> None:
1005 reg_mode = DEFAULT_MODE
1006 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1007 contents, expected = read_data("force_pyi")
1008 with cache_dir() as workspace:
1010 (workspace / "file1.py").resolve(),
1011 (workspace / "file2.py").resolve(),
1014 with open(path, "w") as fh:
1016 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1018 with open(path, "r") as fh:
1020 self.assertEqual(actual, expected)
1021 # verify cache with --pyi is separate
1022 pyi_cache = black.read_cache(pyi_mode)
1023 normal_cache = black.read_cache(reg_mode)
1025 self.assertIn(str(path), pyi_cache)
1026 self.assertNotIn(str(path), normal_cache)
1028 def test_pipe_force_pyi(self) -> None:
1029 source, expected = read_data("force_pyi")
1030 result = CliRunner().invoke(
1031 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1033 self.assertEqual(result.exit_code, 0)
1034 actual = result.output
1035 self.assertFormatEqual(actual, expected)
1037 def test_single_file_force_py36(self) -> None:
1038 reg_mode = DEFAULT_MODE
1039 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1040 source, expected = read_data("force_py36")
1041 with cache_dir() as workspace:
1042 path = (workspace / "file.py").resolve()
1043 with open(path, "w") as fh:
1045 self.invokeBlack([str(path), *PY36_ARGS])
1046 with open(path, "r") as fh:
1048 # verify cache with --target-version is separate
1049 py36_cache = black.read_cache(py36_mode)
1050 self.assertIn(str(path), py36_cache)
1051 normal_cache = black.read_cache(reg_mode)
1052 self.assertNotIn(str(path), normal_cache)
1053 self.assertEqual(actual, expected)
1056 def test_multi_file_force_py36(self) -> None:
1057 reg_mode = DEFAULT_MODE
1058 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1059 source, expected = read_data("force_py36")
1060 with cache_dir() as workspace:
1062 (workspace / "file1.py").resolve(),
1063 (workspace / "file2.py").resolve(),
1066 with open(path, "w") as fh:
1068 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1070 with open(path, "r") as fh:
1072 self.assertEqual(actual, expected)
1073 # verify cache with --target-version is separate
1074 pyi_cache = black.read_cache(py36_mode)
1075 normal_cache = black.read_cache(reg_mode)
1077 self.assertIn(str(path), pyi_cache)
1078 self.assertNotIn(str(path), normal_cache)
1080 def test_pipe_force_py36(self) -> None:
1081 source, expected = read_data("force_py36")
1082 result = CliRunner().invoke(
1084 ["-", "-q", "--target-version=py36"],
1085 input=BytesIO(source.encode("utf8")),
1087 self.assertEqual(result.exit_code, 0)
1088 actual = result.output
1089 self.assertFormatEqual(actual, expected)
1091 @pytest.mark.incompatible_with_mypyc
1092 def test_reformat_one_with_stdin(self) -> None:
1094 "black.format_stdin_to_stdout",
1095 return_value=lambda *args, **kwargs: black.Changed.YES,
1097 report = MagicMock()
1102 write_back=black.WriteBack.YES,
1106 fsts.assert_called_once()
1107 report.done.assert_called_with(path, black.Changed.YES)
1109 @pytest.mark.incompatible_with_mypyc
1110 def test_reformat_one_with_stdin_filename(self) -> None:
1112 "black.format_stdin_to_stdout",
1113 return_value=lambda *args, **kwargs: black.Changed.YES,
1115 report = MagicMock()
1117 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1122 write_back=black.WriteBack.YES,
1126 fsts.assert_called_once_with(
1127 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1129 # __BLACK_STDIN_FILENAME__ should have been stripped
1130 report.done.assert_called_with(expected, black.Changed.YES)
1132 @pytest.mark.incompatible_with_mypyc
1133 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1135 "black.format_stdin_to_stdout",
1136 return_value=lambda *args, **kwargs: black.Changed.YES,
1138 report = MagicMock()
1140 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1145 write_back=black.WriteBack.YES,
1149 fsts.assert_called_once_with(
1151 write_back=black.WriteBack.YES,
1152 mode=replace(DEFAULT_MODE, is_pyi=True),
1154 # __BLACK_STDIN_FILENAME__ should have been stripped
1155 report.done.assert_called_with(expected, black.Changed.YES)
1157 @pytest.mark.incompatible_with_mypyc
1158 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1160 "black.format_stdin_to_stdout",
1161 return_value=lambda *args, **kwargs: black.Changed.YES,
1163 report = MagicMock()
1165 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1170 write_back=black.WriteBack.YES,
1174 fsts.assert_called_once_with(
1176 write_back=black.WriteBack.YES,
1177 mode=replace(DEFAULT_MODE, is_ipynb=True),
1179 # __BLACK_STDIN_FILENAME__ should have been stripped
1180 report.done.assert_called_with(expected, black.Changed.YES)
1182 @pytest.mark.incompatible_with_mypyc
1183 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1185 "black.format_stdin_to_stdout",
1186 return_value=lambda *args, **kwargs: black.Changed.YES,
1188 report = MagicMock()
1189 # Even with an existing file, since we are forcing stdin, black
1190 # should output to stdout and not modify the file inplace
1191 p = Path(str(THIS_DIR / "data/collections.py"))
1192 # Make sure is_file actually returns True
1193 self.assertTrue(p.is_file())
1194 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1199 write_back=black.WriteBack.YES,
1203 fsts.assert_called_once()
1204 # __BLACK_STDIN_FILENAME__ should have been stripped
1205 report.done.assert_called_with(expected, black.Changed.YES)
1207 def test_reformat_one_with_stdin_empty(self) -> None:
1208 output = io.StringIO()
1209 with patch("io.TextIOWrapper", lambda *args, **kwargs: output):
1211 black.format_stdin_to_stdout(
1214 write_back=black.WriteBack.YES,
1217 except io.UnsupportedOperation:
1218 pass # StringIO does not support detach
1219 assert output.getvalue() == ""
1221 def test_invalid_cli_regex(self) -> None:
1222 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1223 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1225 def test_required_version_matches_version(self) -> None:
1227 ["--required-version", black.__version__], exit_code=0, ignore_config=True
1230 def test_required_version_does_not_match_version(self) -> None:
1232 ["--required-version", "20.99b"], exit_code=1, ignore_config=True
1235 def test_preserves_line_endings(self) -> None:
1236 with TemporaryDirectory() as workspace:
1237 test_file = Path(workspace) / "test.py"
1238 for nl in ["\n", "\r\n"]:
1239 contents = nl.join(["def f( ):", " pass"])
1240 test_file.write_bytes(contents.encode())
1241 ff(test_file, write_back=black.WriteBack.YES)
1242 updated_contents: bytes = test_file.read_bytes()
1243 self.assertIn(nl.encode(), updated_contents)
1245 self.assertNotIn(b"\r\n", updated_contents)
1247 def test_preserves_line_endings_via_stdin(self) -> None:
1248 for nl in ["\n", "\r\n"]:
1249 contents = nl.join(["def f( ):", " pass"])
1250 runner = BlackRunner()
1251 result = runner.invoke(
1252 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1254 self.assertEqual(result.exit_code, 0)
1255 output = result.stdout_bytes
1256 self.assertIn(nl.encode("utf8"), output)
1258 self.assertNotIn(b"\r\n", output)
1260 def test_assert_equivalent_different_asts(self) -> None:
1261 with self.assertRaises(AssertionError):
1262 black.assert_equivalent("{}", "None")
1264 def test_shhh_click(self) -> None:
1266 from click import _unicodefun
1267 except ModuleNotFoundError:
1268 self.skipTest("Incompatible Click version")
1269 if not hasattr(_unicodefun, "_verify_python3_env"):
1270 self.skipTest("Incompatible Click version")
1271 # First, let's see if Click is crashing with a preferred ASCII charset.
1272 with patch("locale.getpreferredencoding") as gpe:
1273 gpe.return_value = "ASCII"
1274 with self.assertRaises(RuntimeError):
1275 _unicodefun._verify_python3_env() # type: ignore
1276 # Now, let's silence Click...
1278 # ...and confirm it's silent.
1279 with patch("locale.getpreferredencoding") as gpe:
1280 gpe.return_value = "ASCII"
1282 _unicodefun._verify_python3_env() # type: ignore
1283 except RuntimeError as re:
1284 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1286 def test_root_logger_not_used_directly(self) -> None:
1287 def fail(*args: Any, **kwargs: Any) -> None:
1288 self.fail("Record created with root logger")
1290 with patch.multiple(
1299 ff(THIS_DIR / "util.py")
1301 def test_invalid_config_return_code(self) -> None:
1302 tmp_file = Path(black.dump_to_file())
1304 tmp_config = Path(black.dump_to_file())
1306 args = ["--config", str(tmp_config), str(tmp_file)]
1307 self.invokeBlack(args, exit_code=2, ignore_config=False)
1311 def test_parse_pyproject_toml(self) -> None:
1312 test_toml_file = THIS_DIR / "test.toml"
1313 config = black.parse_pyproject_toml(str(test_toml_file))
1314 self.assertEqual(config["verbose"], 1)
1315 self.assertEqual(config["check"], "no")
1316 self.assertEqual(config["diff"], "y")
1317 self.assertEqual(config["color"], True)
1318 self.assertEqual(config["line_length"], 79)
1319 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1320 self.assertEqual(config["exclude"], r"\.pyi?$")
1321 self.assertEqual(config["include"], r"\.py?$")
1323 def test_read_pyproject_toml(self) -> None:
1324 test_toml_file = THIS_DIR / "test.toml"
1325 fake_ctx = FakeContext()
1326 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1327 config = fake_ctx.default_map
1328 self.assertEqual(config["verbose"], "1")
1329 self.assertEqual(config["check"], "no")
1330 self.assertEqual(config["diff"], "y")
1331 self.assertEqual(config["color"], "True")
1332 self.assertEqual(config["line_length"], "79")
1333 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1334 self.assertEqual(config["exclude"], r"\.pyi?$")
1335 self.assertEqual(config["include"], r"\.py?$")
1337 @pytest.mark.incompatible_with_mypyc
1338 def test_find_project_root(self) -> None:
1339 with TemporaryDirectory() as workspace:
1340 root = Path(workspace)
1341 test_dir = root / "test"
1344 src_dir = root / "src"
1347 root_pyproject = root / "pyproject.toml"
1348 root_pyproject.touch()
1349 src_pyproject = src_dir / "pyproject.toml"
1350 src_pyproject.touch()
1351 src_python = src_dir / "foo.py"
1355 black.find_project_root((src_dir, test_dir)),
1356 (root.resolve(), "pyproject.toml"),
1359 black.find_project_root((src_dir,)),
1360 (src_dir.resolve(), "pyproject.toml"),
1363 black.find_project_root((src_python,)),
1364 (src_dir.resolve(), "pyproject.toml"),
1368 "black.files.find_user_pyproject_toml",
1369 black.files.find_user_pyproject_toml.__wrapped__,
1371 def test_find_user_pyproject_toml_linux(self) -> None:
1372 if system() == "Windows":
1375 # Test if XDG_CONFIG_HOME is checked
1376 with TemporaryDirectory() as workspace:
1377 tmp_user_config = Path(workspace) / "black"
1378 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1380 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1383 # Test fallback for XDG_CONFIG_HOME
1384 with patch.dict("os.environ"):
1385 os.environ.pop("XDG_CONFIG_HOME", None)
1386 fallback_user_config = Path("~/.config").expanduser() / "black"
1388 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1391 def test_find_user_pyproject_toml_windows(self) -> None:
1392 if system() != "Windows":
1395 user_config_path = Path.home() / ".black"
1397 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1400 def test_bpo_33660_workaround(self) -> None:
1401 if system() == "Windows":
1404 # https://bugs.python.org/issue33660
1406 with change_directory(root):
1407 path = Path("workspace") / "project"
1408 report = black.Report(verbose=True)
1409 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1410 self.assertEqual(normalized_path, "workspace/project")
1412 def test_newline_comment_interaction(self) -> None:
1413 source = "class A:\\\r\n# type: ignore\n pass\n"
1414 output = black.format_str(source, mode=DEFAULT_MODE)
1415 black.assert_stable(source, output, mode=DEFAULT_MODE)
1417 def test_bpo_2142_workaround(self) -> None:
1419 # https://bugs.python.org/issue2142
1421 source, _ = read_data("missing_final_newline.py")
1422 # read_data adds a trailing newline
1423 source = source.rstrip()
1424 expected, _ = read_data("missing_final_newline.diff")
1425 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1426 diff_header = re.compile(
1427 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1428 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1431 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1432 self.assertEqual(result.exit_code, 0)
1435 actual = result.output
1436 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1437 self.assertEqual(actual, expected)
1440 def compare_results(
1441 result: click.testing.Result, expected_value: str, expected_exit_code: int
1443 """Helper method to test the value and exit code of a click Result."""
1445 result.output == expected_value
1446 ), "The output did not match the expected value."
1447 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1449 def test_code_option(self) -> None:
1450 """Test the code option with no changes."""
1451 code = 'print("Hello world")\n'
1452 args = ["--code", code]
1453 result = CliRunner().invoke(black.main, args)
1455 self.compare_results(result, code, 0)
1457 def test_code_option_changed(self) -> None:
1458 """Test the code option when changes are required."""
1459 code = "print('hello world')"
1460 formatted = black.format_str(code, mode=DEFAULT_MODE)
1462 args = ["--code", code]
1463 result = CliRunner().invoke(black.main, args)
1465 self.compare_results(result, formatted, 0)
1467 def test_code_option_check(self) -> None:
1468 """Test the code option when check is passed."""
1469 args = ["--check", "--code", 'print("Hello world")\n']
1470 result = CliRunner().invoke(black.main, args)
1471 self.compare_results(result, "", 0)
1473 def test_code_option_check_changed(self) -> None:
1474 """Test the code option when changes are required, and check is passed."""
1475 args = ["--check", "--code", "print('hello world')"]
1476 result = CliRunner().invoke(black.main, args)
1477 self.compare_results(result, "", 1)
1479 def test_code_option_diff(self) -> None:
1480 """Test the code option when diff is passed."""
1481 code = "print('hello world')"
1482 formatted = black.format_str(code, mode=DEFAULT_MODE)
1483 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1485 args = ["--diff", "--code", code]
1486 result = CliRunner().invoke(black.main, args)
1488 # Remove time from diff
1489 output = DIFF_TIME.sub("", result.output)
1491 assert output == result_diff, "The output did not match the expected value."
1492 assert result.exit_code == 0, "The exit code is incorrect."
1494 def test_code_option_color_diff(self) -> None:
1495 """Test the code option when color and diff are passed."""
1496 code = "print('hello world')"
1497 formatted = black.format_str(code, mode=DEFAULT_MODE)
1499 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1500 result_diff = color_diff(result_diff)
1502 args = ["--diff", "--color", "--code", code]
1503 result = CliRunner().invoke(black.main, args)
1505 # Remove time from diff
1506 output = DIFF_TIME.sub("", result.output)
1508 assert output == result_diff, "The output did not match the expected value."
1509 assert result.exit_code == 0, "The exit code is incorrect."
1511 @pytest.mark.incompatible_with_mypyc
1512 def test_code_option_safe(self) -> None:
1513 """Test that the code option throws an error when the sanity checks fail."""
1514 # Patch black.assert_equivalent to ensure the sanity checks fail
1515 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1516 code = 'print("Hello world")'
1517 error_msg = f"{code}\nerror: cannot format <string>: \n"
1519 args = ["--safe", "--code", code]
1520 result = CliRunner().invoke(black.main, args)
1522 self.compare_results(result, error_msg, 123)
1524 def test_code_option_fast(self) -> None:
1525 """Test that the code option ignores errors when the sanity checks fail."""
1526 # Patch black.assert_equivalent to ensure the sanity checks fail
1527 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1528 code = 'print("Hello world")'
1529 formatted = black.format_str(code, mode=DEFAULT_MODE)
1531 args = ["--fast", "--code", code]
1532 result = CliRunner().invoke(black.main, args)
1534 self.compare_results(result, formatted, 0)
1536 @pytest.mark.incompatible_with_mypyc
1537 def test_code_option_config(self) -> None:
1539 Test that the code option finds the pyproject.toml in the current directory.
1541 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1542 args = ["--code", "print"]
1543 # This is the only directory known to contain a pyproject.toml
1544 with change_directory(PROJECT_ROOT):
1545 CliRunner().invoke(black.main, args)
1546 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1549 len(parse.mock_calls) >= 1
1550 ), "Expected config parse to be called with the current directory."
1552 _, call_args, _ = parse.mock_calls[0]
1554 call_args[0].lower() == str(pyproject_path).lower()
1555 ), "Incorrect config loaded."
1557 @pytest.mark.incompatible_with_mypyc
1558 def test_code_option_parent_config(self) -> None:
1560 Test that the code option finds the pyproject.toml in the parent directory.
1562 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1563 with change_directory(THIS_DIR):
1564 args = ["--code", "print"]
1565 CliRunner().invoke(black.main, args)
1567 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1569 len(parse.mock_calls) >= 1
1570 ), "Expected config parse to be called with the current directory."
1572 _, call_args, _ = parse.mock_calls[0]
1574 call_args[0].lower() == str(pyproject_path).lower()
1575 ), "Incorrect config loaded."
1577 def test_for_handled_unexpected_eof_error(self) -> None:
1579 Test that an unexpected EOF SyntaxError is nicely presented.
1581 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1582 black.lib2to3_parse("print(", {})
1584 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1586 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1587 with pytest.raises(AssertionError) as err:
1588 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1591 # Unfortunately the SyntaxError message has changed in newer versions so we
1592 # can't match it directly.
1593 err.match("invalid character")
1594 err.match(r"\(<unknown>, line 1\)")
1598 def test_cache_broken_file(self) -> None:
1600 with cache_dir() as workspace:
1601 cache_file = get_cache_file(mode)
1602 cache_file.write_text("this is not a pickle")
1603 assert black.read_cache(mode) == {}
1604 src = (workspace / "test.py").resolve()
1605 src.write_text("print('hello')")
1606 invokeBlack([str(src)])
1607 cache = black.read_cache(mode)
1608 assert str(src) in cache
1610 def test_cache_single_file_already_cached(self) -> None:
1612 with cache_dir() as workspace:
1613 src = (workspace / "test.py").resolve()
1614 src.write_text("print('hello')")
1615 black.write_cache({}, [src], mode)
1616 invokeBlack([str(src)])
1617 assert src.read_text() == "print('hello')"
1620 def test_cache_multiple_files(self) -> None:
1622 with cache_dir() as workspace, patch(
1623 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1625 one = (workspace / "one.py").resolve()
1626 with one.open("w") as fobj:
1627 fobj.write("print('hello')")
1628 two = (workspace / "two.py").resolve()
1629 with two.open("w") as fobj:
1630 fobj.write("print('hello')")
1631 black.write_cache({}, [one], mode)
1632 invokeBlack([str(workspace)])
1633 with one.open("r") as fobj:
1634 assert fobj.read() == "print('hello')"
1635 with two.open("r") as fobj:
1636 assert fobj.read() == 'print("hello")\n'
1637 cache = black.read_cache(mode)
1638 assert str(one) in cache
1639 assert str(two) in cache
1641 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1642 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1644 with cache_dir() as workspace:
1645 src = (workspace / "test.py").resolve()
1646 with src.open("w") as fobj:
1647 fobj.write("print('hello')")
1648 with patch("black.read_cache") as read_cache, patch(
1651 cmd = [str(src), "--diff"]
1653 cmd.append("--color")
1655 cache_file = get_cache_file(mode)
1656 assert cache_file.exists() is False
1657 write_cache.assert_not_called()
1658 read_cache.assert_not_called()
1660 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1662 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1663 with cache_dir() as workspace:
1664 for tag in range(0, 4):
1665 src = (workspace / f"test{tag}.py").resolve()
1666 with src.open("w") as fobj:
1667 fobj.write("print('hello')")
1668 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1669 cmd = ["--diff", str(workspace)]
1671 cmd.append("--color")
1672 invokeBlack(cmd, exit_code=0)
1673 # this isn't quite doing what we want, but if it _isn't_
1674 # called then we cannot be using the lock it provides
1677 def test_no_cache_when_stdin(self) -> None:
1680 result = CliRunner().invoke(
1681 black.main, ["-"], input=BytesIO(b"print('hello')")
1683 assert not result.exit_code
1684 cache_file = get_cache_file(mode)
1685 assert not cache_file.exists()
1687 def test_read_cache_no_cachefile(self) -> None:
1690 assert black.read_cache(mode) == {}
1692 def test_write_cache_read_cache(self) -> None:
1694 with cache_dir() as workspace:
1695 src = (workspace / "test.py").resolve()
1697 black.write_cache({}, [src], mode)
1698 cache = black.read_cache(mode)
1699 assert str(src) in cache
1700 assert cache[str(src)] == black.get_cache_info(src)
1702 def test_filter_cached(self) -> None:
1703 with TemporaryDirectory() as workspace:
1704 path = Path(workspace)
1705 uncached = (path / "uncached").resolve()
1706 cached = (path / "cached").resolve()
1707 cached_but_changed = (path / "changed").resolve()
1710 cached_but_changed.touch()
1712 str(cached): black.get_cache_info(cached),
1713 str(cached_but_changed): (0.0, 0),
1715 todo, done = black.filter_cached(
1716 cache, {uncached, cached, cached_but_changed}
1718 assert todo == {uncached, cached_but_changed}
1719 assert done == {cached}
1721 def test_write_cache_creates_directory_if_needed(self) -> None:
1723 with cache_dir(exists=False) as workspace:
1724 assert not workspace.exists()
1725 black.write_cache({}, [], mode)
1726 assert workspace.exists()
1729 def test_failed_formatting_does_not_get_cached(self) -> None:
1731 with cache_dir() as workspace, patch(
1732 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1734 failing = (workspace / "failing.py").resolve()
1735 with failing.open("w") as fobj:
1736 fobj.write("not actually python")
1737 clean = (workspace / "clean.py").resolve()
1738 with clean.open("w") as fobj:
1739 fobj.write('print("hello")\n')
1740 invokeBlack([str(workspace)], exit_code=123)
1741 cache = black.read_cache(mode)
1742 assert str(failing) not in cache
1743 assert str(clean) in cache
1745 def test_write_cache_write_fail(self) -> None:
1747 with cache_dir(), patch.object(Path, "open") as mock:
1748 mock.side_effect = OSError
1749 black.write_cache({}, [], mode)
1751 def test_read_cache_line_lengths(self) -> None:
1753 short_mode = replace(DEFAULT_MODE, line_length=1)
1754 with cache_dir() as workspace:
1755 path = (workspace / "file.py").resolve()
1757 black.write_cache({}, [path], mode)
1758 one = black.read_cache(mode)
1759 assert str(path) in one
1760 two = black.read_cache(short_mode)
1761 assert str(path) not in two
1764 def assert_collected_sources(
1765 src: Sequence[Union[str, Path]],
1766 expected: Sequence[Union[str, Path]],
1768 ctx: Optional[FakeContext] = None,
1769 exclude: Optional[str] = None,
1770 include: Optional[str] = None,
1771 extend_exclude: Optional[str] = None,
1772 force_exclude: Optional[str] = None,
1773 stdin_filename: Optional[str] = None,
1775 gs_src = tuple(str(Path(s)) for s in src)
1776 gs_expected = [Path(s) for s in expected]
1777 gs_exclude = None if exclude is None else compile_pattern(exclude)
1778 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
1779 gs_extend_exclude = (
1780 None if extend_exclude is None else compile_pattern(extend_exclude)
1782 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
1783 collected = black.get_sources(
1784 ctx=ctx or FakeContext(),
1790 extend_exclude=gs_extend_exclude,
1791 force_exclude=gs_force_exclude,
1792 report=black.Report(),
1793 stdin_filename=stdin_filename,
1795 assert sorted(collected) == sorted(gs_expected)
1798 class TestFileCollection:
1799 def test_include_exclude(self) -> None:
1800 path = THIS_DIR / "data" / "include_exclude_tests"
1803 Path(path / "b/dont_exclude/a.py"),
1804 Path(path / "b/dont_exclude/a.pyi"),
1806 assert_collected_sources(
1810 exclude=r"/exclude/|/\.definitely_exclude/",
1813 def test_gitignore_used_as_default(self) -> None:
1814 base = Path(DATA_DIR / "include_exclude_tests")
1816 base / "b/.definitely_exclude/a.py",
1817 base / "b/.definitely_exclude/a.pyi",
1821 ctx.obj["root"] = base
1822 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
1824 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
1825 def test_exclude_for_issue_1572(self) -> None:
1826 # Exclude shouldn't touch files that were explicitly given to Black through the
1827 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1828 # https://github.com/psf/black/issues/1572
1829 path = DATA_DIR / "include_exclude_tests"
1830 src = [path / "b/exclude/a.py"]
1831 expected = [path / "b/exclude/a.py"]
1832 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1834 def test_gitignore_exclude(self) -> None:
1835 path = THIS_DIR / "data" / "include_exclude_tests"
1836 include = re.compile(r"\.pyi?$")
1837 exclude = re.compile(r"")
1838 report = black.Report()
1839 gitignore = PathSpec.from_lines(
1840 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1842 sources: List[Path] = []
1844 Path(path / "b/dont_exclude/a.py"),
1845 Path(path / "b/dont_exclude/a.pyi"),
1847 this_abs = THIS_DIR.resolve()
1849 black.gen_python_files(
1862 assert sorted(expected) == sorted(sources)
1864 def test_nested_gitignore(self) -> None:
1865 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
1866 include = re.compile(r"\.pyi?$")
1867 exclude = re.compile(r"")
1868 root_gitignore = black.files.get_gitignore(path)
1869 report = black.Report()
1870 expected: List[Path] = [
1871 Path(path / "x.py"),
1872 Path(path / "root/b.py"),
1873 Path(path / "root/c.py"),
1874 Path(path / "root/child/c.py"),
1876 this_abs = THIS_DIR.resolve()
1878 black.gen_python_files(
1891 assert sorted(expected) == sorted(sources)
1893 def test_invalid_gitignore(self) -> None:
1894 path = THIS_DIR / "data" / "invalid_gitignore_tests"
1895 empty_config = path / "pyproject.toml"
1896 result = BlackRunner().invoke(
1897 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1899 assert result.exit_code == 1
1900 assert result.stderr_bytes is not None
1902 gitignore = path / ".gitignore"
1903 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1905 def test_invalid_nested_gitignore(self) -> None:
1906 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
1907 empty_config = path / "pyproject.toml"
1908 result = BlackRunner().invoke(
1909 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1911 assert result.exit_code == 1
1912 assert result.stderr_bytes is not None
1914 gitignore = path / "a" / ".gitignore"
1915 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1917 def test_empty_include(self) -> None:
1918 path = DATA_DIR / "include_exclude_tests"
1921 Path(path / "b/exclude/a.pie"),
1922 Path(path / "b/exclude/a.py"),
1923 Path(path / "b/exclude/a.pyi"),
1924 Path(path / "b/dont_exclude/a.pie"),
1925 Path(path / "b/dont_exclude/a.py"),
1926 Path(path / "b/dont_exclude/a.pyi"),
1927 Path(path / "b/.definitely_exclude/a.pie"),
1928 Path(path / "b/.definitely_exclude/a.py"),
1929 Path(path / "b/.definitely_exclude/a.pyi"),
1930 Path(path / ".gitignore"),
1931 Path(path / "pyproject.toml"),
1933 # Setting exclude explicitly to an empty string to block .gitignore usage.
1934 assert_collected_sources(src, expected, include="", exclude="")
1936 def test_extend_exclude(self) -> None:
1937 path = DATA_DIR / "include_exclude_tests"
1940 Path(path / "b/exclude/a.py"),
1941 Path(path / "b/dont_exclude/a.py"),
1943 assert_collected_sources(
1944 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
1947 @pytest.mark.incompatible_with_mypyc
1948 def test_symlink_out_of_root_directory(self) -> None:
1950 root = THIS_DIR.resolve()
1952 include = re.compile(black.DEFAULT_INCLUDES)
1953 exclude = re.compile(black.DEFAULT_EXCLUDES)
1954 report = black.Report()
1955 gitignore = PathSpec.from_lines("gitwildmatch", [])
1956 # `child` should behave like a symlink which resolved path is clearly
1957 # outside of the `root` directory.
1958 path.iterdir.return_value = [child]
1959 child.resolve.return_value = Path("/a/b/c")
1960 child.as_posix.return_value = "/a/b/c"
1961 child.is_symlink.return_value = True
1964 black.gen_python_files(
1977 except ValueError as ve:
1978 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
1979 path.iterdir.assert_called_once()
1980 child.resolve.assert_called_once()
1981 child.is_symlink.assert_called_once()
1982 # `child` should behave like a strange file which resolved path is clearly
1983 # outside of the `root` directory.
1984 child.is_symlink.return_value = False
1985 with pytest.raises(ValueError):
1987 black.gen_python_files(
2000 path.iterdir.assert_called()
2001 assert path.iterdir.call_count == 2
2002 child.resolve.assert_called()
2003 assert child.resolve.call_count == 2
2004 child.is_symlink.assert_called()
2005 assert child.is_symlink.call_count == 2
2007 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2008 def test_get_sources_with_stdin(self) -> None:
2011 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2013 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2014 def test_get_sources_with_stdin_filename(self) -> None:
2016 stdin_filename = str(THIS_DIR / "data/collections.py")
2017 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2018 assert_collected_sources(
2021 exclude=r"/exclude/a\.py",
2022 stdin_filename=stdin_filename,
2025 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2026 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2027 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2028 # file being passed directly. This is the same as
2029 # test_exclude_for_issue_1572
2030 path = DATA_DIR / "include_exclude_tests"
2032 stdin_filename = str(path / "b/exclude/a.py")
2033 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2034 assert_collected_sources(
2037 exclude=r"/exclude/|a\.py",
2038 stdin_filename=stdin_filename,
2041 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2042 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2043 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2044 # file being passed directly. This is the same as
2045 # test_exclude_for_issue_1572
2047 path = THIS_DIR / "data" / "include_exclude_tests"
2048 stdin_filename = str(path / "b/exclude/a.py")
2049 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2050 assert_collected_sources(
2053 extend_exclude=r"/exclude/|a\.py",
2054 stdin_filename=stdin_filename,
2057 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2058 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2059 # Force exclude should exclude the file when passing it through
2061 path = THIS_DIR / "data" / "include_exclude_tests"
2062 stdin_filename = str(path / "b/exclude/a.py")
2063 assert_collected_sources(
2066 force_exclude=r"/exclude/|a\.py",
2067 stdin_filename=stdin_filename,
2072 with open(black.__file__, "r", encoding="utf-8") as _bf:
2073 black_source_lines = _bf.readlines()
2074 except UnicodeDecodeError:
2075 if not black.COMPILED:
2080 frame: types.FrameType, event: str, arg: Any
2081 ) -> Callable[[types.FrameType, str, Any], Any]:
2082 """Show function calls `from black/__init__.py` as they happen.
2084 Register this with `sys.settrace()` in a test you're debugging.
2089 stack = len(inspect.stack()) - 19
2091 filename = frame.f_code.co_filename
2092 lineno = frame.f_lineno
2093 func_sig_lineno = lineno - 1
2094 funcname = black_source_lines[func_sig_lineno].strip()
2095 while funcname.startswith("@"):
2096 func_sig_lineno += 1
2097 funcname = black_source_lines[func_sig_lineno].strip()
2098 if "black/__init__.py" in filename:
2099 print(f"{' ' * stack}{lineno}:{funcname}")