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] = {}
105 class FakeParameter(click.Parameter):
106 """A fake click Parameter for when calling functions that need it."""
108 def __init__(self) -> None:
112 class BlackRunner(CliRunner):
113 """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
115 def __init__(self) -> None:
116 super().__init__(mix_stderr=False)
120 args: List[str], exit_code: int = 0, ignore_config: bool = True
122 runner = BlackRunner()
124 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
125 result = runner.invoke(black.main, args, catch_exceptions=False)
126 assert result.stdout_bytes is not None
127 assert result.stderr_bytes is not None
129 f"Failed with args: {args}\n"
130 f"stdout: {result.stdout_bytes.decode()!r}\n"
131 f"stderr: {result.stderr_bytes.decode()!r}\n"
132 f"exception: {result.exception}"
134 assert result.exit_code == exit_code, msg
137 class BlackTestCase(BlackBaseTestCase):
138 invokeBlack = staticmethod(invokeBlack)
140 def test_empty_ff(self) -> None:
142 tmp_file = Path(black.dump_to_file())
144 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
145 with open(tmp_file, encoding="utf8") as f:
149 self.assertFormatEqual(expected, actual)
151 def test_piping(self) -> None:
152 source, expected = read_data("src/black/__init__", data=False)
153 result = BlackRunner().invoke(
155 ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
156 input=BytesIO(source.encode("utf8")),
158 self.assertEqual(result.exit_code, 0)
159 self.assertFormatEqual(expected, result.output)
160 if source != result.output:
161 black.assert_equivalent(source, result.output)
162 black.assert_stable(source, result.output, DEFAULT_MODE)
164 def test_piping_diff(self) -> None:
165 diff_header = re.compile(
166 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
169 source, _ = read_data("expression.py")
170 expected, _ = read_data("expression.diff")
171 config = THIS_DIR / "data" / "empty_pyproject.toml"
175 f"--line-length={black.DEFAULT_LINE_LENGTH}",
177 f"--config={config}",
179 result = BlackRunner().invoke(
180 black.main, args, input=BytesIO(source.encode("utf8"))
182 self.assertEqual(result.exit_code, 0)
183 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
184 actual = actual.rstrip() + "\n" # the diff output has a trailing space
185 self.assertEqual(expected, actual)
187 def test_piping_diff_with_color(self) -> None:
188 source, _ = read_data("expression.py")
189 config = THIS_DIR / "data" / "empty_pyproject.toml"
193 f"--line-length={black.DEFAULT_LINE_LENGTH}",
196 f"--config={config}",
198 result = BlackRunner().invoke(
199 black.main, args, input=BytesIO(source.encode("utf8"))
201 actual = result.output
202 # Again, the contents are checked in a different test, so only look for colors.
203 self.assertIn("\033[1;37m", actual)
204 self.assertIn("\033[36m", actual)
205 self.assertIn("\033[32m", actual)
206 self.assertIn("\033[31m", actual)
207 self.assertIn("\033[0m", actual)
209 @patch("black.dump_to_file", dump_to_stderr)
210 def _test_wip(self) -> None:
211 source, expected = read_data("wip")
212 sys.settrace(tracefunc)
215 experimental_string_processing=False,
216 target_versions={black.TargetVersion.PY38},
218 actual = fs(source, mode=mode)
220 self.assertFormatEqual(expected, actual)
221 black.assert_equivalent(source, actual)
222 black.assert_stable(source, actual, black.FileMode())
224 @unittest.expectedFailure
225 @patch("black.dump_to_file", dump_to_stderr)
226 def test_trailing_comma_optional_parens_stability1(self) -> None:
227 source, _expected = read_data("trailing_comma_optional_parens1")
229 black.assert_stable(source, actual, DEFAULT_MODE)
231 @unittest.expectedFailure
232 @patch("black.dump_to_file", dump_to_stderr)
233 def test_trailing_comma_optional_parens_stability2(self) -> None:
234 source, _expected = read_data("trailing_comma_optional_parens2")
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_stability3(self) -> None:
241 source, _expected = read_data("trailing_comma_optional_parens3")
243 black.assert_stable(source, actual, DEFAULT_MODE)
245 @patch("black.dump_to_file", dump_to_stderr)
246 def test_trailing_comma_optional_parens_stability1_pass2(self) -> None:
247 source, _expected = read_data("trailing_comma_optional_parens1")
248 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
249 black.assert_stable(source, actual, DEFAULT_MODE)
251 @patch("black.dump_to_file", dump_to_stderr)
252 def test_trailing_comma_optional_parens_stability2_pass2(self) -> None:
253 source, _expected = read_data("trailing_comma_optional_parens2")
254 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
255 black.assert_stable(source, actual, DEFAULT_MODE)
257 @patch("black.dump_to_file", dump_to_stderr)
258 def test_trailing_comma_optional_parens_stability3_pass2(self) -> None:
259 source, _expected = read_data("trailing_comma_optional_parens3")
260 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
261 black.assert_stable(source, actual, DEFAULT_MODE)
263 def test_pep_572_version_detection(self) -> None:
264 source, _ = read_data("pep_572")
265 root = black.lib2to3_parse(source)
266 features = black.get_features_used(root)
267 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
268 versions = black.detect_target_versions(root)
269 self.assertIn(black.TargetVersion.PY38, versions)
271 def test_expression_ff(self) -> None:
272 source, expected = read_data("expression")
273 tmp_file = Path(black.dump_to_file(source))
275 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
276 with open(tmp_file, encoding="utf8") as f:
280 self.assertFormatEqual(expected, actual)
281 with patch("black.dump_to_file", dump_to_stderr):
282 black.assert_equivalent(source, actual)
283 black.assert_stable(source, actual, DEFAULT_MODE)
285 def test_expression_diff(self) -> None:
286 source, _ = read_data("expression.py")
287 config = THIS_DIR / "data" / "empty_pyproject.toml"
288 expected, _ = read_data("expression.diff")
289 tmp_file = Path(black.dump_to_file(source))
290 diff_header = re.compile(
291 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
292 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
295 result = BlackRunner().invoke(
296 black.main, ["--diff", str(tmp_file), f"--config={config}"]
298 self.assertEqual(result.exit_code, 0)
301 actual = result.output
302 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
303 if expected != actual:
304 dump = black.dump_to_file(actual)
306 "Expected diff isn't equal to the actual. If you made changes to"
307 " expression.py and this is an anticipated difference, overwrite"
308 f" tests/data/expression.diff with {dump}"
310 self.assertEqual(expected, actual, msg)
312 def test_expression_diff_with_color(self) -> None:
313 source, _ = read_data("expression.py")
314 config = THIS_DIR / "data" / "empty_pyproject.toml"
315 expected, _ = read_data("expression.diff")
316 tmp_file = Path(black.dump_to_file(source))
318 result = BlackRunner().invoke(
319 black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
323 actual = result.output
324 # We check the contents of the diff in `test_expression_diff`. All
325 # we need to check here is that color codes exist in the result.
326 self.assertIn("\033[1;37m", actual)
327 self.assertIn("\033[36m", actual)
328 self.assertIn("\033[32m", actual)
329 self.assertIn("\033[31m", actual)
330 self.assertIn("\033[0m", actual)
332 def test_detect_pos_only_arguments(self) -> None:
333 source, _ = read_data("pep_570")
334 root = black.lib2to3_parse(source)
335 features = black.get_features_used(root)
336 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
337 versions = black.detect_target_versions(root)
338 self.assertIn(black.TargetVersion.PY38, versions)
340 @patch("black.dump_to_file", dump_to_stderr)
341 def test_string_quotes(self) -> None:
342 source, expected = read_data("string_quotes")
343 mode = black.Mode(experimental_string_processing=True)
344 assert_format(source, expected, mode)
345 mode = replace(mode, string_normalization=False)
346 not_normalized = fs(source, mode=mode)
347 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
348 black.assert_equivalent(source, not_normalized)
349 black.assert_stable(source, not_normalized, mode=mode)
351 def test_skip_magic_trailing_comma(self) -> None:
352 source, _ = read_data("expression.py")
353 expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
354 tmp_file = Path(black.dump_to_file(source))
355 diff_header = re.compile(
356 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
357 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
360 result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
361 self.assertEqual(result.exit_code, 0)
364 actual = result.output
365 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
366 actual = actual.rstrip() + "\n" # the diff output has a trailing space
367 if expected != actual:
368 dump = black.dump_to_file(actual)
370 "Expected diff isn't equal to the actual. If you made changes to"
371 " expression.py and this is an anticipated difference, overwrite"
372 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
374 self.assertEqual(expected, actual, msg)
376 @patch("black.dump_to_file", dump_to_stderr)
377 def test_async_as_identifier(self) -> None:
378 source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
379 source, expected = read_data("async_as_identifier")
381 self.assertFormatEqual(expected, actual)
382 major, minor = sys.version_info[:2]
383 if major < 3 or (major <= 3 and minor < 7):
384 black.assert_equivalent(source, actual)
385 black.assert_stable(source, actual, DEFAULT_MODE)
386 # ensure black can parse this when the target is 3.6
387 self.invokeBlack([str(source_path), "--target-version", "py36"])
388 # but not on 3.7, because async/await is no longer an identifier
389 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
391 @patch("black.dump_to_file", dump_to_stderr)
392 def test_python37(self) -> None:
393 source_path = (THIS_DIR / "data" / "python37.py").resolve()
394 source, expected = read_data("python37")
396 self.assertFormatEqual(expected, actual)
397 major, minor = sys.version_info[:2]
398 if major > 3 or (major == 3 and minor >= 7):
399 black.assert_equivalent(source, actual)
400 black.assert_stable(source, actual, DEFAULT_MODE)
401 # ensure black can parse this when the target is 3.7
402 self.invokeBlack([str(source_path), "--target-version", "py37"])
403 # but not on 3.6, because we use async as a reserved keyword
404 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
406 def test_tab_comment_indentation(self) -> None:
407 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
408 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
409 self.assertFormatEqual(contents_spc, fs(contents_spc))
410 self.assertFormatEqual(contents_spc, fs(contents_tab))
412 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
413 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
414 self.assertFormatEqual(contents_spc, fs(contents_spc))
415 self.assertFormatEqual(contents_spc, fs(contents_tab))
417 # mixed tabs and spaces (valid Python 2 code)
418 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
419 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
420 self.assertFormatEqual(contents_spc, fs(contents_spc))
421 self.assertFormatEqual(contents_spc, fs(contents_tab))
423 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
424 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
425 self.assertFormatEqual(contents_spc, fs(contents_spc))
426 self.assertFormatEqual(contents_spc, fs(contents_tab))
428 def test_report_verbose(self) -> None:
429 report = Report(verbose=True)
433 def out(msg: str, **kwargs: Any) -> None:
434 out_lines.append(msg)
436 def err(msg: str, **kwargs: Any) -> None:
437 err_lines.append(msg)
439 with patch("black.output._out", out), patch("black.output._err", err):
440 report.done(Path("f1"), black.Changed.NO)
441 self.assertEqual(len(out_lines), 1)
442 self.assertEqual(len(err_lines), 0)
443 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
444 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
445 self.assertEqual(report.return_code, 0)
446 report.done(Path("f2"), black.Changed.YES)
447 self.assertEqual(len(out_lines), 2)
448 self.assertEqual(len(err_lines), 0)
449 self.assertEqual(out_lines[-1], "reformatted f2")
451 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
453 report.done(Path("f3"), black.Changed.CACHED)
454 self.assertEqual(len(out_lines), 3)
455 self.assertEqual(len(err_lines), 0)
457 out_lines[-1], "f3 wasn't modified on disk since last run."
460 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
462 self.assertEqual(report.return_code, 0)
464 self.assertEqual(report.return_code, 1)
466 report.failed(Path("e1"), "boom")
467 self.assertEqual(len(out_lines), 3)
468 self.assertEqual(len(err_lines), 1)
469 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
471 unstyle(str(report)),
472 "1 file reformatted, 2 files left unchanged, 1 file failed to"
475 self.assertEqual(report.return_code, 123)
476 report.done(Path("f3"), black.Changed.YES)
477 self.assertEqual(len(out_lines), 4)
478 self.assertEqual(len(err_lines), 1)
479 self.assertEqual(out_lines[-1], "reformatted f3")
481 unstyle(str(report)),
482 "2 files reformatted, 2 files left unchanged, 1 file failed to"
485 self.assertEqual(report.return_code, 123)
486 report.failed(Path("e2"), "boom")
487 self.assertEqual(len(out_lines), 4)
488 self.assertEqual(len(err_lines), 2)
489 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
491 unstyle(str(report)),
492 "2 files reformatted, 2 files left unchanged, 2 files failed to"
495 self.assertEqual(report.return_code, 123)
496 report.path_ignored(Path("wat"), "no match")
497 self.assertEqual(len(out_lines), 5)
498 self.assertEqual(len(err_lines), 2)
499 self.assertEqual(out_lines[-1], "wat ignored: no match")
501 unstyle(str(report)),
502 "2 files reformatted, 2 files left unchanged, 2 files failed to"
505 self.assertEqual(report.return_code, 123)
506 report.done(Path("f4"), black.Changed.NO)
507 self.assertEqual(len(out_lines), 6)
508 self.assertEqual(len(err_lines), 2)
509 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
511 unstyle(str(report)),
512 "2 files reformatted, 3 files left unchanged, 2 files failed to"
515 self.assertEqual(report.return_code, 123)
518 unstyle(str(report)),
519 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
520 " would fail to reformat.",
525 unstyle(str(report)),
526 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
527 " would fail to reformat.",
530 def test_report_quiet(self) -> None:
531 report = Report(quiet=True)
535 def out(msg: str, **kwargs: Any) -> None:
536 out_lines.append(msg)
538 def err(msg: str, **kwargs: Any) -> None:
539 err_lines.append(msg)
541 with patch("black.output._out", out), patch("black.output._err", err):
542 report.done(Path("f1"), black.Changed.NO)
543 self.assertEqual(len(out_lines), 0)
544 self.assertEqual(len(err_lines), 0)
545 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
546 self.assertEqual(report.return_code, 0)
547 report.done(Path("f2"), black.Changed.YES)
548 self.assertEqual(len(out_lines), 0)
549 self.assertEqual(len(err_lines), 0)
551 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
553 report.done(Path("f3"), black.Changed.CACHED)
554 self.assertEqual(len(out_lines), 0)
555 self.assertEqual(len(err_lines), 0)
557 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
559 self.assertEqual(report.return_code, 0)
561 self.assertEqual(report.return_code, 1)
563 report.failed(Path("e1"), "boom")
564 self.assertEqual(len(out_lines), 0)
565 self.assertEqual(len(err_lines), 1)
566 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
568 unstyle(str(report)),
569 "1 file reformatted, 2 files left unchanged, 1 file failed to"
572 self.assertEqual(report.return_code, 123)
573 report.done(Path("f3"), black.Changed.YES)
574 self.assertEqual(len(out_lines), 0)
575 self.assertEqual(len(err_lines), 1)
577 unstyle(str(report)),
578 "2 files reformatted, 2 files left unchanged, 1 file failed to"
581 self.assertEqual(report.return_code, 123)
582 report.failed(Path("e2"), "boom")
583 self.assertEqual(len(out_lines), 0)
584 self.assertEqual(len(err_lines), 2)
585 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
587 unstyle(str(report)),
588 "2 files reformatted, 2 files left unchanged, 2 files failed to"
591 self.assertEqual(report.return_code, 123)
592 report.path_ignored(Path("wat"), "no match")
593 self.assertEqual(len(out_lines), 0)
594 self.assertEqual(len(err_lines), 2)
596 unstyle(str(report)),
597 "2 files reformatted, 2 files left unchanged, 2 files failed to"
600 self.assertEqual(report.return_code, 123)
601 report.done(Path("f4"), black.Changed.NO)
602 self.assertEqual(len(out_lines), 0)
603 self.assertEqual(len(err_lines), 2)
605 unstyle(str(report)),
606 "2 files reformatted, 3 files left unchanged, 2 files failed to"
609 self.assertEqual(report.return_code, 123)
612 unstyle(str(report)),
613 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
614 " would fail to reformat.",
619 unstyle(str(report)),
620 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
621 " would fail to reformat.",
624 def test_report_normal(self) -> None:
625 report = black.Report()
629 def out(msg: str, **kwargs: Any) -> None:
630 out_lines.append(msg)
632 def err(msg: str, **kwargs: Any) -> None:
633 err_lines.append(msg)
635 with patch("black.output._out", out), patch("black.output._err", err):
636 report.done(Path("f1"), black.Changed.NO)
637 self.assertEqual(len(out_lines), 0)
638 self.assertEqual(len(err_lines), 0)
639 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
640 self.assertEqual(report.return_code, 0)
641 report.done(Path("f2"), black.Changed.YES)
642 self.assertEqual(len(out_lines), 1)
643 self.assertEqual(len(err_lines), 0)
644 self.assertEqual(out_lines[-1], "reformatted f2")
646 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
648 report.done(Path("f3"), black.Changed.CACHED)
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, 2 files left unchanged."
655 self.assertEqual(report.return_code, 0)
657 self.assertEqual(report.return_code, 1)
659 report.failed(Path("e1"), "boom")
660 self.assertEqual(len(out_lines), 1)
661 self.assertEqual(len(err_lines), 1)
662 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
664 unstyle(str(report)),
665 "1 file reformatted, 2 files left unchanged, 1 file failed to"
668 self.assertEqual(report.return_code, 123)
669 report.done(Path("f3"), black.Changed.YES)
670 self.assertEqual(len(out_lines), 2)
671 self.assertEqual(len(err_lines), 1)
672 self.assertEqual(out_lines[-1], "reformatted f3")
674 unstyle(str(report)),
675 "2 files reformatted, 2 files left unchanged, 1 file failed to"
678 self.assertEqual(report.return_code, 123)
679 report.failed(Path("e2"), "boom")
680 self.assertEqual(len(out_lines), 2)
681 self.assertEqual(len(err_lines), 2)
682 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
684 unstyle(str(report)),
685 "2 files reformatted, 2 files left unchanged, 2 files failed to"
688 self.assertEqual(report.return_code, 123)
689 report.path_ignored(Path("wat"), "no match")
690 self.assertEqual(len(out_lines), 2)
691 self.assertEqual(len(err_lines), 2)
693 unstyle(str(report)),
694 "2 files reformatted, 2 files left unchanged, 2 files failed to"
697 self.assertEqual(report.return_code, 123)
698 report.done(Path("f4"), black.Changed.NO)
699 self.assertEqual(len(out_lines), 2)
700 self.assertEqual(len(err_lines), 2)
702 unstyle(str(report)),
703 "2 files reformatted, 3 files left unchanged, 2 files failed to"
706 self.assertEqual(report.return_code, 123)
709 unstyle(str(report)),
710 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
711 " would fail to reformat.",
716 unstyle(str(report)),
717 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
718 " would fail to reformat.",
721 def test_lib2to3_parse(self) -> None:
722 with self.assertRaises(black.InvalidInput):
723 black.lib2to3_parse("invalid syntax")
726 black.lib2to3_parse(straddling)
727 black.lib2to3_parse(straddling, {TargetVersion.PY27})
728 black.lib2to3_parse(straddling, {TargetVersion.PY36})
729 black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36})
732 black.lib2to3_parse(py2_only)
733 black.lib2to3_parse(py2_only, {TargetVersion.PY27})
734 with self.assertRaises(black.InvalidInput):
735 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
736 with self.assertRaises(black.InvalidInput):
737 black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36})
739 py3_only = "exec(x, end=y)"
740 black.lib2to3_parse(py3_only)
741 with self.assertRaises(black.InvalidInput):
742 black.lib2to3_parse(py3_only, {TargetVersion.PY27})
743 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
744 black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36})
746 def test_get_features_used_decorator(self) -> None:
747 # Test the feature detection of new decorator syntax
748 # since this makes some test cases of test_get_features_used()
749 # fails if it fails, this is tested first so that a useful case
751 simples, relaxed = read_data("decorators")
752 # skip explanation comments at the top of the file
753 for simple_test in simples.split("##")[1:]:
754 node = black.lib2to3_parse(simple_test)
755 decorator = str(node.children[0].children[0]).strip()
757 Feature.RELAXED_DECORATORS,
758 black.get_features_used(node),
760 f"decorator '{decorator}' follows python<=3.8 syntax"
761 "but is detected as 3.9+"
762 # f"The full node is\n{node!r}"
765 # skip the '# output' comment at the top of the output part
766 for relaxed_test in relaxed.split("##")[1:]:
767 node = black.lib2to3_parse(relaxed_test)
768 decorator = str(node.children[0].children[0]).strip()
770 Feature.RELAXED_DECORATORS,
771 black.get_features_used(node),
773 f"decorator '{decorator}' uses python3.9+ syntax"
774 "but is detected as python<=3.8"
775 # f"The full node is\n{node!r}"
779 def test_get_features_used(self) -> None:
780 node = black.lib2to3_parse("def f(*, arg): ...\n")
781 self.assertEqual(black.get_features_used(node), set())
782 node = black.lib2to3_parse("def f(*, arg,): ...\n")
783 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
784 node = black.lib2to3_parse("f(*arg,)\n")
786 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
788 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
789 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
790 node = black.lib2to3_parse("123_456\n")
791 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
792 node = black.lib2to3_parse("123456\n")
793 self.assertEqual(black.get_features_used(node), set())
794 source, expected = read_data("function")
795 node = black.lib2to3_parse(source)
796 expected_features = {
797 Feature.TRAILING_COMMA_IN_CALL,
798 Feature.TRAILING_COMMA_IN_DEF,
801 self.assertEqual(black.get_features_used(node), expected_features)
802 node = black.lib2to3_parse(expected)
803 self.assertEqual(black.get_features_used(node), expected_features)
804 source, expected = read_data("expression")
805 node = black.lib2to3_parse(source)
806 self.assertEqual(black.get_features_used(node), set())
807 node = black.lib2to3_parse(expected)
808 self.assertEqual(black.get_features_used(node), set())
809 node = black.lib2to3_parse("lambda a, /, b: ...")
810 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
811 node = black.lib2to3_parse("def fn(a, /, b): ...")
812 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
814 def test_get_future_imports(self) -> None:
815 node = black.lib2to3_parse("\n")
816 self.assertEqual(set(), black.get_future_imports(node))
817 node = black.lib2to3_parse("from __future__ import black\n")
818 self.assertEqual({"black"}, black.get_future_imports(node))
819 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
820 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
821 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
822 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
823 node = black.lib2to3_parse(
824 "from __future__ import multiple\nfrom __future__ import imports\n"
826 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
827 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
828 self.assertEqual({"black"}, black.get_future_imports(node))
829 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
830 self.assertEqual({"black"}, black.get_future_imports(node))
831 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
832 self.assertEqual(set(), black.get_future_imports(node))
833 node = black.lib2to3_parse("from some.module import black\n")
834 self.assertEqual(set(), black.get_future_imports(node))
835 node = black.lib2to3_parse(
836 "from __future__ import unicode_literals as _unicode_literals"
838 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
839 node = black.lib2to3_parse(
840 "from __future__ import unicode_literals as _lol, print"
842 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
844 @pytest.mark.incompatible_with_mypyc
845 def test_debug_visitor(self) -> None:
846 source, _ = read_data("debug_visitor.py")
847 expected, _ = read_data("debug_visitor.out")
851 def out(msg: str, **kwargs: Any) -> None:
852 out_lines.append(msg)
854 def err(msg: str, **kwargs: Any) -> None:
855 err_lines.append(msg)
857 with patch("black.debug.out", out):
858 DebugVisitor.show(source)
859 actual = "\n".join(out_lines) + "\n"
861 if expected != actual:
862 log_name = black.dump_to_file(*out_lines)
866 f"AST print out is different. Actual version dumped to {log_name}",
869 def test_format_file_contents(self) -> None:
872 with self.assertRaises(black.NothingChanged):
873 black.format_file_contents(empty, mode=mode, fast=False)
875 with self.assertRaises(black.NothingChanged):
876 black.format_file_contents(just_nl, mode=mode, fast=False)
877 same = "j = [1, 2, 3]\n"
878 with self.assertRaises(black.NothingChanged):
879 black.format_file_contents(same, mode=mode, fast=False)
880 different = "j = [1,2,3]"
882 actual = black.format_file_contents(different, mode=mode, fast=False)
883 self.assertEqual(expected, actual)
884 invalid = "return if you can"
885 with self.assertRaises(black.InvalidInput) as e:
886 black.format_file_contents(invalid, mode=mode, fast=False)
887 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
889 def test_endmarker(self) -> None:
890 n = black.lib2to3_parse("\n")
891 self.assertEqual(n.type, black.syms.file_input)
892 self.assertEqual(len(n.children), 1)
893 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
895 @pytest.mark.incompatible_with_mypyc
896 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
897 def test_assertFormatEqual(self) -> None:
901 def out(msg: str, **kwargs: Any) -> None:
902 out_lines.append(msg)
904 def err(msg: str, **kwargs: Any) -> None:
905 err_lines.append(msg)
907 with patch("black.output._out", out), patch("black.output._err", err):
908 with self.assertRaises(AssertionError):
909 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
911 out_str = "".join(out_lines)
912 self.assertTrue("Expected tree:" in out_str)
913 self.assertTrue("Actual tree:" in out_str)
914 self.assertEqual("".join(err_lines), "")
917 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
918 def test_works_in_mono_process_only_environment(self) -> None:
919 with cache_dir() as workspace:
921 (workspace / "one.py").resolve(),
922 (workspace / "two.py").resolve(),
924 f.write_text('print("hello")\n')
925 self.invokeBlack([str(workspace)])
928 def test_check_diff_use_together(self) -> None:
930 # Files which will be reformatted.
931 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
932 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
933 # Files which will not be reformatted.
934 src2 = (THIS_DIR / "data" / "composition.py").resolve()
935 self.invokeBlack([str(src2), "--diff", "--check"])
936 # Multi file command.
937 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
939 def test_no_files(self) -> None:
941 # Without an argument, black exits with error code 0.
944 def test_broken_symlink(self) -> None:
945 with cache_dir() as workspace:
946 symlink = workspace / "broken_link.py"
948 symlink.symlink_to("nonexistent.py")
949 except (OSError, NotImplementedError) as e:
950 self.skipTest(f"Can't create symlinks: {e}")
951 self.invokeBlack([str(workspace.resolve())])
953 def test_single_file_force_pyi(self) -> None:
954 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
955 contents, expected = read_data("force_pyi")
956 with cache_dir() as workspace:
957 path = (workspace / "file.py").resolve()
958 with open(path, "w") as fh:
960 self.invokeBlack([str(path), "--pyi"])
961 with open(path, "r") as fh:
963 # verify cache with --pyi is separate
964 pyi_cache = black.read_cache(pyi_mode)
965 self.assertIn(str(path), pyi_cache)
966 normal_cache = black.read_cache(DEFAULT_MODE)
967 self.assertNotIn(str(path), normal_cache)
968 self.assertFormatEqual(expected, actual)
969 black.assert_equivalent(contents, actual)
970 black.assert_stable(contents, actual, pyi_mode)
973 def test_multi_file_force_pyi(self) -> None:
974 reg_mode = DEFAULT_MODE
975 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
976 contents, expected = read_data("force_pyi")
977 with cache_dir() as workspace:
979 (workspace / "file1.py").resolve(),
980 (workspace / "file2.py").resolve(),
983 with open(path, "w") as fh:
985 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
987 with open(path, "r") as fh:
989 self.assertEqual(actual, expected)
990 # verify cache with --pyi is separate
991 pyi_cache = black.read_cache(pyi_mode)
992 normal_cache = black.read_cache(reg_mode)
994 self.assertIn(str(path), pyi_cache)
995 self.assertNotIn(str(path), normal_cache)
997 def test_pipe_force_pyi(self) -> None:
998 source, expected = read_data("force_pyi")
999 result = CliRunner().invoke(
1000 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1002 self.assertEqual(result.exit_code, 0)
1003 actual = result.output
1004 self.assertFormatEqual(actual, expected)
1006 def test_single_file_force_py36(self) -> None:
1007 reg_mode = DEFAULT_MODE
1008 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1009 source, expected = read_data("force_py36")
1010 with cache_dir() as workspace:
1011 path = (workspace / "file.py").resolve()
1012 with open(path, "w") as fh:
1014 self.invokeBlack([str(path), *PY36_ARGS])
1015 with open(path, "r") as fh:
1017 # verify cache with --target-version is separate
1018 py36_cache = black.read_cache(py36_mode)
1019 self.assertIn(str(path), py36_cache)
1020 normal_cache = black.read_cache(reg_mode)
1021 self.assertNotIn(str(path), normal_cache)
1022 self.assertEqual(actual, expected)
1025 def test_multi_file_force_py36(self) -> None:
1026 reg_mode = DEFAULT_MODE
1027 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1028 source, expected = read_data("force_py36")
1029 with cache_dir() as workspace:
1031 (workspace / "file1.py").resolve(),
1032 (workspace / "file2.py").resolve(),
1035 with open(path, "w") as fh:
1037 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1039 with open(path, "r") as fh:
1041 self.assertEqual(actual, expected)
1042 # verify cache with --target-version is separate
1043 pyi_cache = black.read_cache(py36_mode)
1044 normal_cache = black.read_cache(reg_mode)
1046 self.assertIn(str(path), pyi_cache)
1047 self.assertNotIn(str(path), normal_cache)
1049 def test_pipe_force_py36(self) -> None:
1050 source, expected = read_data("force_py36")
1051 result = CliRunner().invoke(
1053 ["-", "-q", "--target-version=py36"],
1054 input=BytesIO(source.encode("utf8")),
1056 self.assertEqual(result.exit_code, 0)
1057 actual = result.output
1058 self.assertFormatEqual(actual, expected)
1060 @pytest.mark.incompatible_with_mypyc
1061 def test_reformat_one_with_stdin(self) -> None:
1063 "black.format_stdin_to_stdout",
1064 return_value=lambda *args, **kwargs: black.Changed.YES,
1066 report = MagicMock()
1071 write_back=black.WriteBack.YES,
1075 fsts.assert_called_once()
1076 report.done.assert_called_with(path, black.Changed.YES)
1078 @pytest.mark.incompatible_with_mypyc
1079 def test_reformat_one_with_stdin_filename(self) -> None:
1081 "black.format_stdin_to_stdout",
1082 return_value=lambda *args, **kwargs: black.Changed.YES,
1084 report = MagicMock()
1086 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1091 write_back=black.WriteBack.YES,
1095 fsts.assert_called_once_with(
1096 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1098 # __BLACK_STDIN_FILENAME__ should have been stripped
1099 report.done.assert_called_with(expected, black.Changed.YES)
1101 @pytest.mark.incompatible_with_mypyc
1102 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1104 "black.format_stdin_to_stdout",
1105 return_value=lambda *args, **kwargs: black.Changed.YES,
1107 report = MagicMock()
1109 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1114 write_back=black.WriteBack.YES,
1118 fsts.assert_called_once_with(
1120 write_back=black.WriteBack.YES,
1121 mode=replace(DEFAULT_MODE, is_pyi=True),
1123 # __BLACK_STDIN_FILENAME__ should have been stripped
1124 report.done.assert_called_with(expected, black.Changed.YES)
1126 @pytest.mark.incompatible_with_mypyc
1127 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1129 "black.format_stdin_to_stdout",
1130 return_value=lambda *args, **kwargs: black.Changed.YES,
1132 report = MagicMock()
1134 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1139 write_back=black.WriteBack.YES,
1143 fsts.assert_called_once_with(
1145 write_back=black.WriteBack.YES,
1146 mode=replace(DEFAULT_MODE, is_ipynb=True),
1148 # __BLACK_STDIN_FILENAME__ should have been stripped
1149 report.done.assert_called_with(expected, black.Changed.YES)
1151 @pytest.mark.incompatible_with_mypyc
1152 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1154 "black.format_stdin_to_stdout",
1155 return_value=lambda *args, **kwargs: black.Changed.YES,
1157 report = MagicMock()
1158 # Even with an existing file, since we are forcing stdin, black
1159 # should output to stdout and not modify the file inplace
1160 p = Path(str(THIS_DIR / "data/collections.py"))
1161 # Make sure is_file actually returns True
1162 self.assertTrue(p.is_file())
1163 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1168 write_back=black.WriteBack.YES,
1172 fsts.assert_called_once()
1173 # __BLACK_STDIN_FILENAME__ should have been stripped
1174 report.done.assert_called_with(expected, black.Changed.YES)
1176 def test_reformat_one_with_stdin_empty(self) -> None:
1177 output = io.StringIO()
1178 with patch("io.TextIOWrapper", lambda *args, **kwargs: output):
1180 black.format_stdin_to_stdout(
1183 write_back=black.WriteBack.YES,
1186 except io.UnsupportedOperation:
1187 pass # StringIO does not support detach
1188 assert output.getvalue() == ""
1190 def test_invalid_cli_regex(self) -> None:
1191 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1192 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1194 def test_required_version_matches_version(self) -> None:
1196 ["--required-version", black.__version__], exit_code=0, ignore_config=True
1199 def test_required_version_does_not_match_version(self) -> None:
1201 ["--required-version", "20.99b"], exit_code=1, ignore_config=True
1204 def test_preserves_line_endings(self) -> None:
1205 with TemporaryDirectory() as workspace:
1206 test_file = Path(workspace) / "test.py"
1207 for nl in ["\n", "\r\n"]:
1208 contents = nl.join(["def f( ):", " pass"])
1209 test_file.write_bytes(contents.encode())
1210 ff(test_file, write_back=black.WriteBack.YES)
1211 updated_contents: bytes = test_file.read_bytes()
1212 self.assertIn(nl.encode(), updated_contents)
1214 self.assertNotIn(b"\r\n", updated_contents)
1216 def test_preserves_line_endings_via_stdin(self) -> None:
1217 for nl in ["\n", "\r\n"]:
1218 contents = nl.join(["def f( ):", " pass"])
1219 runner = BlackRunner()
1220 result = runner.invoke(
1221 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1223 self.assertEqual(result.exit_code, 0)
1224 output = result.stdout_bytes
1225 self.assertIn(nl.encode("utf8"), output)
1227 self.assertNotIn(b"\r\n", output)
1229 def test_assert_equivalent_different_asts(self) -> None:
1230 with self.assertRaises(AssertionError):
1231 black.assert_equivalent("{}", "None")
1233 def test_shhh_click(self) -> None:
1235 from click import _unicodefun
1236 except ModuleNotFoundError:
1237 self.skipTest("Incompatible Click version")
1238 if not hasattr(_unicodefun, "_verify_python3_env"):
1239 self.skipTest("Incompatible Click version")
1240 # First, let's see if Click is crashing with a preferred ASCII charset.
1241 with patch("locale.getpreferredencoding") as gpe:
1242 gpe.return_value = "ASCII"
1243 with self.assertRaises(RuntimeError):
1244 _unicodefun._verify_python3_env() # type: ignore
1245 # Now, let's silence Click...
1247 # ...and confirm it's silent.
1248 with patch("locale.getpreferredencoding") as gpe:
1249 gpe.return_value = "ASCII"
1251 _unicodefun._verify_python3_env() # type: ignore
1252 except RuntimeError as re:
1253 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1255 def test_root_logger_not_used_directly(self) -> None:
1256 def fail(*args: Any, **kwargs: Any) -> None:
1257 self.fail("Record created with root logger")
1259 with patch.multiple(
1268 ff(THIS_DIR / "util.py")
1270 def test_invalid_config_return_code(self) -> None:
1271 tmp_file = Path(black.dump_to_file())
1273 tmp_config = Path(black.dump_to_file())
1275 args = ["--config", str(tmp_config), str(tmp_file)]
1276 self.invokeBlack(args, exit_code=2, ignore_config=False)
1280 def test_parse_pyproject_toml(self) -> None:
1281 test_toml_file = THIS_DIR / "test.toml"
1282 config = black.parse_pyproject_toml(str(test_toml_file))
1283 self.assertEqual(config["verbose"], 1)
1284 self.assertEqual(config["check"], "no")
1285 self.assertEqual(config["diff"], "y")
1286 self.assertEqual(config["color"], True)
1287 self.assertEqual(config["line_length"], 79)
1288 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1289 self.assertEqual(config["exclude"], r"\.pyi?$")
1290 self.assertEqual(config["include"], r"\.py?$")
1292 def test_read_pyproject_toml(self) -> None:
1293 test_toml_file = THIS_DIR / "test.toml"
1294 fake_ctx = FakeContext()
1295 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1296 config = fake_ctx.default_map
1297 self.assertEqual(config["verbose"], "1")
1298 self.assertEqual(config["check"], "no")
1299 self.assertEqual(config["diff"], "y")
1300 self.assertEqual(config["color"], "True")
1301 self.assertEqual(config["line_length"], "79")
1302 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1303 self.assertEqual(config["exclude"], r"\.pyi?$")
1304 self.assertEqual(config["include"], r"\.py?$")
1306 @pytest.mark.incompatible_with_mypyc
1307 def test_find_project_root(self) -> None:
1308 with TemporaryDirectory() as workspace:
1309 root = Path(workspace)
1310 test_dir = root / "test"
1313 src_dir = root / "src"
1316 root_pyproject = root / "pyproject.toml"
1317 root_pyproject.touch()
1318 src_pyproject = src_dir / "pyproject.toml"
1319 src_pyproject.touch()
1320 src_python = src_dir / "foo.py"
1324 black.find_project_root((src_dir, test_dir)), root.resolve()
1326 self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1327 self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1330 "black.files.find_user_pyproject_toml",
1331 black.files.find_user_pyproject_toml.__wrapped__,
1333 def test_find_user_pyproject_toml_linux(self) -> None:
1334 if system() == "Windows":
1337 # Test if XDG_CONFIG_HOME is checked
1338 with TemporaryDirectory() as workspace:
1339 tmp_user_config = Path(workspace) / "black"
1340 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1342 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1345 # Test fallback for XDG_CONFIG_HOME
1346 with patch.dict("os.environ"):
1347 os.environ.pop("XDG_CONFIG_HOME", None)
1348 fallback_user_config = Path("~/.config").expanduser() / "black"
1350 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1353 def test_find_user_pyproject_toml_windows(self) -> None:
1354 if system() != "Windows":
1357 user_config_path = Path.home() / ".black"
1359 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1362 def test_bpo_33660_workaround(self) -> None:
1363 if system() == "Windows":
1366 # https://bugs.python.org/issue33660
1368 with change_directory(root):
1369 path = Path("workspace") / "project"
1370 report = black.Report(verbose=True)
1371 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1372 self.assertEqual(normalized_path, "workspace/project")
1374 def test_newline_comment_interaction(self) -> None:
1375 source = "class A:\\\r\n# type: ignore\n pass\n"
1376 output = black.format_str(source, mode=DEFAULT_MODE)
1377 black.assert_stable(source, output, mode=DEFAULT_MODE)
1379 def test_bpo_2142_workaround(self) -> None:
1381 # https://bugs.python.org/issue2142
1383 source, _ = read_data("missing_final_newline.py")
1384 # read_data adds a trailing newline
1385 source = source.rstrip()
1386 expected, _ = read_data("missing_final_newline.diff")
1387 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1388 diff_header = re.compile(
1389 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1390 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1393 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1394 self.assertEqual(result.exit_code, 0)
1397 actual = result.output
1398 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1399 self.assertEqual(actual, expected)
1401 @pytest.mark.python2
1402 def test_docstring_reformat_for_py27(self) -> None:
1404 Check that stripping trailing whitespace from Python 2 docstrings
1405 doesn't trigger a "not equivalent to source" error
1408 b'def foo():\r\n """Testing\r\n Testing """\r\n print "Foo"\r\n'
1410 expected = 'def foo():\n """Testing\n Testing"""\n print "Foo"\n'
1412 result = BlackRunner().invoke(
1414 ["-", "-q", "--target-version=py27"],
1415 input=BytesIO(source),
1418 self.assertEqual(result.exit_code, 0)
1419 actual = result.stdout
1420 self.assertFormatEqual(actual, expected)
1423 def compare_results(
1424 result: click.testing.Result, expected_value: str, expected_exit_code: int
1426 """Helper method to test the value and exit code of a click Result."""
1428 result.output == expected_value
1429 ), "The output did not match the expected value."
1430 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1432 def test_code_option(self) -> None:
1433 """Test the code option with no changes."""
1434 code = 'print("Hello world")\n'
1435 args = ["--code", code]
1436 result = CliRunner().invoke(black.main, args)
1438 self.compare_results(result, code, 0)
1440 def test_code_option_changed(self) -> None:
1441 """Test the code option when changes are required."""
1442 code = "print('hello world')"
1443 formatted = black.format_str(code, mode=DEFAULT_MODE)
1445 args = ["--code", code]
1446 result = CliRunner().invoke(black.main, args)
1448 self.compare_results(result, formatted, 0)
1450 def test_code_option_check(self) -> None:
1451 """Test the code option when check is passed."""
1452 args = ["--check", "--code", 'print("Hello world")\n']
1453 result = CliRunner().invoke(black.main, args)
1454 self.compare_results(result, "", 0)
1456 def test_code_option_check_changed(self) -> None:
1457 """Test the code option when changes are required, and check is passed."""
1458 args = ["--check", "--code", "print('hello world')"]
1459 result = CliRunner().invoke(black.main, args)
1460 self.compare_results(result, "", 1)
1462 def test_code_option_diff(self) -> None:
1463 """Test the code option when diff is passed."""
1464 code = "print('hello world')"
1465 formatted = black.format_str(code, mode=DEFAULT_MODE)
1466 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1468 args = ["--diff", "--code", code]
1469 result = CliRunner().invoke(black.main, args)
1471 # Remove time from diff
1472 output = DIFF_TIME.sub("", result.output)
1474 assert output == result_diff, "The output did not match the expected value."
1475 assert result.exit_code == 0, "The exit code is incorrect."
1477 def test_code_option_color_diff(self) -> None:
1478 """Test the code option when color and diff are passed."""
1479 code = "print('hello world')"
1480 formatted = black.format_str(code, mode=DEFAULT_MODE)
1482 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1483 result_diff = color_diff(result_diff)
1485 args = ["--diff", "--color", "--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 @pytest.mark.incompatible_with_mypyc
1495 def test_code_option_safe(self) -> None:
1496 """Test that the code option throws an error when the sanity checks fail."""
1497 # Patch black.assert_equivalent to ensure the sanity checks fail
1498 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1499 code = 'print("Hello world")'
1500 error_msg = f"{code}\nerror: cannot format <string>: \n"
1502 args = ["--safe", "--code", code]
1503 result = CliRunner().invoke(black.main, args)
1505 self.compare_results(result, error_msg, 123)
1507 def test_code_option_fast(self) -> None:
1508 """Test that the code option ignores errors when the sanity checks fail."""
1509 # Patch black.assert_equivalent to ensure the sanity checks fail
1510 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1511 code = 'print("Hello world")'
1512 formatted = black.format_str(code, mode=DEFAULT_MODE)
1514 args = ["--fast", "--code", code]
1515 result = CliRunner().invoke(black.main, args)
1517 self.compare_results(result, formatted, 0)
1519 @pytest.mark.incompatible_with_mypyc
1520 def test_code_option_config(self) -> None:
1522 Test that the code option finds the pyproject.toml in the current directory.
1524 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1525 args = ["--code", "print"]
1526 # This is the only directory known to contain a pyproject.toml
1527 with change_directory(PROJECT_ROOT):
1528 CliRunner().invoke(black.main, args)
1529 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1532 len(parse.mock_calls) >= 1
1533 ), "Expected config parse to be called with the current directory."
1535 _, call_args, _ = parse.mock_calls[0]
1537 call_args[0].lower() == str(pyproject_path).lower()
1538 ), "Incorrect config loaded."
1540 @pytest.mark.incompatible_with_mypyc
1541 def test_code_option_parent_config(self) -> None:
1543 Test that the code option finds the pyproject.toml in the parent directory.
1545 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1546 with change_directory(THIS_DIR):
1547 args = ["--code", "print"]
1548 CliRunner().invoke(black.main, args)
1550 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1552 len(parse.mock_calls) >= 1
1553 ), "Expected config parse to be called with the current directory."
1555 _, call_args, _ = parse.mock_calls[0]
1557 call_args[0].lower() == str(pyproject_path).lower()
1558 ), "Incorrect config loaded."
1560 def test_for_handled_unexpected_eof_error(self) -> None:
1562 Test that an unexpected EOF SyntaxError is nicely presented.
1564 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1565 black.lib2to3_parse("print(", {})
1567 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1571 def test_cache_broken_file(self) -> None:
1573 with cache_dir() as workspace:
1574 cache_file = get_cache_file(mode)
1575 cache_file.write_text("this is not a pickle")
1576 assert black.read_cache(mode) == {}
1577 src = (workspace / "test.py").resolve()
1578 src.write_text("print('hello')")
1579 invokeBlack([str(src)])
1580 cache = black.read_cache(mode)
1581 assert str(src) in cache
1583 def test_cache_single_file_already_cached(self) -> None:
1585 with cache_dir() as workspace:
1586 src = (workspace / "test.py").resolve()
1587 src.write_text("print('hello')")
1588 black.write_cache({}, [src], mode)
1589 invokeBlack([str(src)])
1590 assert src.read_text() == "print('hello')"
1593 def test_cache_multiple_files(self) -> None:
1595 with cache_dir() as workspace, patch(
1596 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1598 one = (workspace / "one.py").resolve()
1599 with one.open("w") as fobj:
1600 fobj.write("print('hello')")
1601 two = (workspace / "two.py").resolve()
1602 with two.open("w") as fobj:
1603 fobj.write("print('hello')")
1604 black.write_cache({}, [one], mode)
1605 invokeBlack([str(workspace)])
1606 with one.open("r") as fobj:
1607 assert fobj.read() == "print('hello')"
1608 with two.open("r") as fobj:
1609 assert fobj.read() == 'print("hello")\n'
1610 cache = black.read_cache(mode)
1611 assert str(one) in cache
1612 assert str(two) in cache
1614 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1615 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1617 with cache_dir() as workspace:
1618 src = (workspace / "test.py").resolve()
1619 with src.open("w") as fobj:
1620 fobj.write("print('hello')")
1621 with patch("black.read_cache") as read_cache, patch(
1624 cmd = [str(src), "--diff"]
1626 cmd.append("--color")
1628 cache_file = get_cache_file(mode)
1629 assert cache_file.exists() is False
1630 write_cache.assert_not_called()
1631 read_cache.assert_not_called()
1633 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1635 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1636 with cache_dir() as workspace:
1637 for tag in range(0, 4):
1638 src = (workspace / f"test{tag}.py").resolve()
1639 with src.open("w") as fobj:
1640 fobj.write("print('hello')")
1641 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1642 cmd = ["--diff", str(workspace)]
1644 cmd.append("--color")
1645 invokeBlack(cmd, exit_code=0)
1646 # this isn't quite doing what we want, but if it _isn't_
1647 # called then we cannot be using the lock it provides
1650 def test_no_cache_when_stdin(self) -> None:
1653 result = CliRunner().invoke(
1654 black.main, ["-"], input=BytesIO(b"print('hello')")
1656 assert not result.exit_code
1657 cache_file = get_cache_file(mode)
1658 assert not cache_file.exists()
1660 def test_read_cache_no_cachefile(self) -> None:
1663 assert black.read_cache(mode) == {}
1665 def test_write_cache_read_cache(self) -> None:
1667 with cache_dir() as workspace:
1668 src = (workspace / "test.py").resolve()
1670 black.write_cache({}, [src], mode)
1671 cache = black.read_cache(mode)
1672 assert str(src) in cache
1673 assert cache[str(src)] == black.get_cache_info(src)
1675 def test_filter_cached(self) -> None:
1676 with TemporaryDirectory() as workspace:
1677 path = Path(workspace)
1678 uncached = (path / "uncached").resolve()
1679 cached = (path / "cached").resolve()
1680 cached_but_changed = (path / "changed").resolve()
1683 cached_but_changed.touch()
1685 str(cached): black.get_cache_info(cached),
1686 str(cached_but_changed): (0.0, 0),
1688 todo, done = black.filter_cached(
1689 cache, {uncached, cached, cached_but_changed}
1691 assert todo == {uncached, cached_but_changed}
1692 assert done == {cached}
1694 def test_write_cache_creates_directory_if_needed(self) -> None:
1696 with cache_dir(exists=False) as workspace:
1697 assert not workspace.exists()
1698 black.write_cache({}, [], mode)
1699 assert workspace.exists()
1702 def test_failed_formatting_does_not_get_cached(self) -> None:
1704 with cache_dir() as workspace, patch(
1705 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1707 failing = (workspace / "failing.py").resolve()
1708 with failing.open("w") as fobj:
1709 fobj.write("not actually python")
1710 clean = (workspace / "clean.py").resolve()
1711 with clean.open("w") as fobj:
1712 fobj.write('print("hello")\n')
1713 invokeBlack([str(workspace)], exit_code=123)
1714 cache = black.read_cache(mode)
1715 assert str(failing) not in cache
1716 assert str(clean) in cache
1718 def test_write_cache_write_fail(self) -> None:
1720 with cache_dir(), patch.object(Path, "open") as mock:
1721 mock.side_effect = OSError
1722 black.write_cache({}, [], mode)
1724 def test_read_cache_line_lengths(self) -> None:
1726 short_mode = replace(DEFAULT_MODE, line_length=1)
1727 with cache_dir() as workspace:
1728 path = (workspace / "file.py").resolve()
1730 black.write_cache({}, [path], mode)
1731 one = black.read_cache(mode)
1732 assert str(path) in one
1733 two = black.read_cache(short_mode)
1734 assert str(path) not in two
1737 def assert_collected_sources(
1738 src: Sequence[Union[str, Path]],
1739 expected: Sequence[Union[str, Path]],
1741 exclude: Optional[str] = None,
1742 include: Optional[str] = None,
1743 extend_exclude: Optional[str] = None,
1744 force_exclude: Optional[str] = None,
1745 stdin_filename: Optional[str] = None,
1747 gs_src = tuple(str(Path(s)) for s in src)
1748 gs_expected = [Path(s) for s in expected]
1749 gs_exclude = None if exclude is None else compile_pattern(exclude)
1750 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
1751 gs_extend_exclude = (
1752 None if extend_exclude is None else compile_pattern(extend_exclude)
1754 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
1755 collected = black.get_sources(
1762 extend_exclude=gs_extend_exclude,
1763 force_exclude=gs_force_exclude,
1764 report=black.Report(),
1765 stdin_filename=stdin_filename,
1767 assert sorted(collected) == sorted(gs_expected)
1770 class TestFileCollection:
1771 def test_include_exclude(self) -> None:
1772 path = THIS_DIR / "data" / "include_exclude_tests"
1775 Path(path / "b/dont_exclude/a.py"),
1776 Path(path / "b/dont_exclude/a.pyi"),
1778 assert_collected_sources(
1782 exclude=r"/exclude/|/\.definitely_exclude/",
1785 def test_gitignore_used_as_default(self) -> None:
1786 base = Path(DATA_DIR / "include_exclude_tests")
1788 base / "b/.definitely_exclude/a.py",
1789 base / "b/.definitely_exclude/a.pyi",
1792 assert_collected_sources(src, expected, extend_exclude=r"/exclude/")
1794 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1795 def test_exclude_for_issue_1572(self) -> None:
1796 # Exclude shouldn't touch files that were explicitly given to Black through the
1797 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1798 # https://github.com/psf/black/issues/1572
1799 path = DATA_DIR / "include_exclude_tests"
1800 src = [path / "b/exclude/a.py"]
1801 expected = [path / "b/exclude/a.py"]
1802 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1804 def test_gitignore_exclude(self) -> None:
1805 path = THIS_DIR / "data" / "include_exclude_tests"
1806 include = re.compile(r"\.pyi?$")
1807 exclude = re.compile(r"")
1808 report = black.Report()
1809 gitignore = PathSpec.from_lines(
1810 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1812 sources: List[Path] = []
1814 Path(path / "b/dont_exclude/a.py"),
1815 Path(path / "b/dont_exclude/a.pyi"),
1817 this_abs = THIS_DIR.resolve()
1819 black.gen_python_files(
1832 assert sorted(expected) == sorted(sources)
1834 def test_nested_gitignore(self) -> None:
1835 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
1836 include = re.compile(r"\.pyi?$")
1837 exclude = re.compile(r"")
1838 root_gitignore = black.files.get_gitignore(path)
1839 report = black.Report()
1840 expected: List[Path] = [
1841 Path(path / "x.py"),
1842 Path(path / "root/b.py"),
1843 Path(path / "root/c.py"),
1844 Path(path / "root/child/c.py"),
1846 this_abs = THIS_DIR.resolve()
1848 black.gen_python_files(
1861 assert sorted(expected) == sorted(sources)
1863 def test_invalid_gitignore(self) -> None:
1864 path = THIS_DIR / "data" / "invalid_gitignore_tests"
1865 empty_config = path / "pyproject.toml"
1866 result = BlackRunner().invoke(
1867 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1869 assert result.exit_code == 1
1870 assert result.stderr_bytes is not None
1872 gitignore = path / ".gitignore"
1873 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1875 def test_invalid_nested_gitignore(self) -> None:
1876 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
1877 empty_config = path / "pyproject.toml"
1878 result = BlackRunner().invoke(
1879 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1881 assert result.exit_code == 1
1882 assert result.stderr_bytes is not None
1884 gitignore = path / "a" / ".gitignore"
1885 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1887 def test_empty_include(self) -> None:
1888 path = DATA_DIR / "include_exclude_tests"
1891 Path(path / "b/exclude/a.pie"),
1892 Path(path / "b/exclude/a.py"),
1893 Path(path / "b/exclude/a.pyi"),
1894 Path(path / "b/dont_exclude/a.pie"),
1895 Path(path / "b/dont_exclude/a.py"),
1896 Path(path / "b/dont_exclude/a.pyi"),
1897 Path(path / "b/.definitely_exclude/a.pie"),
1898 Path(path / "b/.definitely_exclude/a.py"),
1899 Path(path / "b/.definitely_exclude/a.pyi"),
1900 Path(path / ".gitignore"),
1901 Path(path / "pyproject.toml"),
1903 # Setting exclude explicitly to an empty string to block .gitignore usage.
1904 assert_collected_sources(src, expected, include="", exclude="")
1906 def test_extend_exclude(self) -> None:
1907 path = DATA_DIR / "include_exclude_tests"
1910 Path(path / "b/exclude/a.py"),
1911 Path(path / "b/dont_exclude/a.py"),
1913 assert_collected_sources(
1914 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
1917 @pytest.mark.incompatible_with_mypyc
1918 def test_symlink_out_of_root_directory(self) -> None:
1920 root = THIS_DIR.resolve()
1922 include = re.compile(black.DEFAULT_INCLUDES)
1923 exclude = re.compile(black.DEFAULT_EXCLUDES)
1924 report = black.Report()
1925 gitignore = PathSpec.from_lines("gitwildmatch", [])
1926 # `child` should behave like a symlink which resolved path is clearly
1927 # outside of the `root` directory.
1928 path.iterdir.return_value = [child]
1929 child.resolve.return_value = Path("/a/b/c")
1930 child.as_posix.return_value = "/a/b/c"
1931 child.is_symlink.return_value = True
1934 black.gen_python_files(
1947 except ValueError as ve:
1948 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
1949 path.iterdir.assert_called_once()
1950 child.resolve.assert_called_once()
1951 child.is_symlink.assert_called_once()
1952 # `child` should behave like a strange file which resolved path is clearly
1953 # outside of the `root` directory.
1954 child.is_symlink.return_value = False
1955 with pytest.raises(ValueError):
1957 black.gen_python_files(
1970 path.iterdir.assert_called()
1971 assert path.iterdir.call_count == 2
1972 child.resolve.assert_called()
1973 assert child.resolve.call_count == 2
1974 child.is_symlink.assert_called()
1975 assert child.is_symlink.call_count == 2
1977 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1978 def test_get_sources_with_stdin(self) -> None:
1981 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1983 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1984 def test_get_sources_with_stdin_filename(self) -> None:
1986 stdin_filename = str(THIS_DIR / "data/collections.py")
1987 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
1988 assert_collected_sources(
1991 exclude=r"/exclude/a\.py",
1992 stdin_filename=stdin_filename,
1995 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1996 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
1997 # Exclude shouldn't exclude stdin_filename since it is mimicking the
1998 # file being passed directly. This is the same as
1999 # test_exclude_for_issue_1572
2000 path = DATA_DIR / "include_exclude_tests"
2002 stdin_filename = str(path / "b/exclude/a.py")
2003 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2004 assert_collected_sources(
2007 exclude=r"/exclude/|a\.py",
2008 stdin_filename=stdin_filename,
2011 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
2012 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2013 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2014 # file being passed directly. This is the same as
2015 # test_exclude_for_issue_1572
2017 path = THIS_DIR / "data" / "include_exclude_tests"
2018 stdin_filename = str(path / "b/exclude/a.py")
2019 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2020 assert_collected_sources(
2023 extend_exclude=r"/exclude/|a\.py",
2024 stdin_filename=stdin_filename,
2027 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
2028 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2029 # Force exclude should exclude the file when passing it through
2031 path = THIS_DIR / "data" / "include_exclude_tests"
2032 stdin_filename = str(path / "b/exclude/a.py")
2033 assert_collected_sources(
2036 force_exclude=r"/exclude/|a\.py",
2037 stdin_filename=stdin_filename,
2041 @pytest.mark.python2
2042 @pytest.mark.parametrize("explicit", [True, False], ids=["explicit", "autodetection"])
2043 def test_python_2_deprecation_with_target_version(explicit: bool) -> None:
2046 str(THIS_DIR / "empty.toml"),
2047 str(DATA_DIR / "python2.py"),
2051 args.append("--target-version=py27")
2053 result = BlackRunner().invoke(black.main, args)
2054 assert "DEPRECATION: Python 2 support will be removed" in result.stderr
2057 @pytest.mark.python2
2058 def test_python_2_deprecation_autodetection_extended() -> None:
2059 # this test has a similar construction to test_get_features_used_decorator
2060 python2, non_python2 = read_data("python2_detection")
2061 for python2_case in python2.split("###"):
2062 node = black.lib2to3_parse(python2_case)
2063 assert black.detect_target_versions(node) == {TargetVersion.PY27}, python2_case
2064 for non_python2_case in non_python2.split("###"):
2065 node = black.lib2to3_parse(non_python2_case)
2066 assert black.detect_target_versions(node) != {
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}")