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_dir, get_cache_file
44 from black.debug import DebugVisitor
45 from black.output import color_diff, diff
46 from black.report import Report
48 # Import other test classes
49 from tests.util import (
65 THIS_FILE = Path(__file__)
66 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
67 DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
68 DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
72 # Match the time output in a diff, but nothing else
73 DIFF_TIME = re.compile(r"\t[\d\-:+\. ]+")
77 def cache_dir(exists: bool = True) -> Iterator[Path]:
78 with TemporaryDirectory() as workspace:
79 cache_dir = Path(workspace)
81 cache_dir = cache_dir / "new"
82 with patch("black.cache.CACHE_DIR", cache_dir):
87 def event_loop() -> Iterator[None]:
88 policy = asyncio.get_event_loop_policy()
89 loop = policy.new_event_loop()
90 asyncio.set_event_loop(loop)
98 class FakeContext(click.Context):
99 """A fake click Context for when calling functions that need it."""
101 def __init__(self) -> None:
102 self.default_map: Dict[str, Any] = {}
103 # Dummy root, since most of the tests don't care about it
104 self.obj: Dict[str, Any] = {"root": PROJECT_ROOT}
107 class FakeParameter(click.Parameter):
108 """A fake click Parameter for when calling functions that need it."""
110 def __init__(self) -> None:
114 class BlackRunner(CliRunner):
115 """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
117 def __init__(self) -> None:
118 super().__init__(mix_stderr=False)
122 args: List[str], exit_code: int = 0, ignore_config: bool = True
124 runner = BlackRunner()
126 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
127 result = runner.invoke(black.main, args, catch_exceptions=False)
128 assert result.stdout_bytes is not None
129 assert result.stderr_bytes is not None
131 f"Failed with args: {args}\n"
132 f"stdout: {result.stdout_bytes.decode()!r}\n"
133 f"stderr: {result.stderr_bytes.decode()!r}\n"
134 f"exception: {result.exception}"
136 assert result.exit_code == exit_code, msg
139 class BlackTestCase(BlackBaseTestCase):
140 invokeBlack = staticmethod(invokeBlack)
142 def test_empty_ff(self) -> None:
144 tmp_file = Path(black.dump_to_file())
146 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
147 with open(tmp_file, encoding="utf8") as f:
151 self.assertFormatEqual(expected, actual)
153 def test_experimental_string_processing_warns(self) -> None:
155 black.mode.Deprecated, black.Mode, experimental_string_processing=True
158 def test_piping(self) -> None:
159 source, expected = read_data("src/black/__init__", data=False)
160 result = BlackRunner().invoke(
162 ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
163 input=BytesIO(source.encode("utf8")),
165 self.assertEqual(result.exit_code, 0)
166 self.assertFormatEqual(expected, result.output)
167 if source != result.output:
168 black.assert_equivalent(source, result.output)
169 black.assert_stable(source, result.output, DEFAULT_MODE)
171 def test_piping_diff(self) -> None:
172 diff_header = re.compile(
173 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
176 source, _ = read_data("expression.py")
177 expected, _ = read_data("expression.diff")
178 config = THIS_DIR / "data" / "empty_pyproject.toml"
182 f"--line-length={black.DEFAULT_LINE_LENGTH}",
184 f"--config={config}",
186 result = BlackRunner().invoke(
187 black.main, args, input=BytesIO(source.encode("utf8"))
189 self.assertEqual(result.exit_code, 0)
190 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
191 actual = actual.rstrip() + "\n" # the diff output has a trailing space
192 self.assertEqual(expected, actual)
194 def test_piping_diff_with_color(self) -> None:
195 source, _ = read_data("expression.py")
196 config = THIS_DIR / "data" / "empty_pyproject.toml"
200 f"--line-length={black.DEFAULT_LINE_LENGTH}",
203 f"--config={config}",
205 result = BlackRunner().invoke(
206 black.main, args, input=BytesIO(source.encode("utf8"))
208 actual = result.output
209 # Again, the contents are checked in a different test, so only look for colors.
210 self.assertIn("\033[1m", actual)
211 self.assertIn("\033[36m", actual)
212 self.assertIn("\033[32m", actual)
213 self.assertIn("\033[31m", actual)
214 self.assertIn("\033[0m", actual)
216 @patch("black.dump_to_file", dump_to_stderr)
217 def _test_wip(self) -> None:
218 source, expected = read_data("wip")
219 sys.settrace(tracefunc)
222 experimental_string_processing=False,
223 target_versions={black.TargetVersion.PY38},
225 actual = fs(source, mode=mode)
227 self.assertFormatEqual(expected, actual)
228 black.assert_equivalent(source, actual)
229 black.assert_stable(source, actual, black.FileMode())
231 def test_pep_572_version_detection(self) -> None:
232 source, _ = read_data("pep_572")
233 root = black.lib2to3_parse(source)
234 features = black.get_features_used(root)
235 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
236 versions = black.detect_target_versions(root)
237 self.assertIn(black.TargetVersion.PY38, versions)
239 def test_expression_ff(self) -> None:
240 source, expected = read_data("expression")
241 tmp_file = Path(black.dump_to_file(source))
243 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
244 with open(tmp_file, encoding="utf8") as f:
248 self.assertFormatEqual(expected, actual)
249 with patch("black.dump_to_file", dump_to_stderr):
250 black.assert_equivalent(source, actual)
251 black.assert_stable(source, actual, DEFAULT_MODE)
253 def test_expression_diff(self) -> None:
254 source, _ = read_data("expression.py")
255 config = THIS_DIR / "data" / "empty_pyproject.toml"
256 expected, _ = read_data("expression.diff")
257 tmp_file = Path(black.dump_to_file(source))
258 diff_header = re.compile(
259 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
260 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
263 result = BlackRunner().invoke(
264 black.main, ["--diff", str(tmp_file), f"--config={config}"]
266 self.assertEqual(result.exit_code, 0)
269 actual = result.output
270 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
271 if expected != actual:
272 dump = black.dump_to_file(actual)
274 "Expected diff isn't equal to the actual. If you made changes to"
275 " expression.py and this is an anticipated difference, overwrite"
276 f" tests/data/expression.diff with {dump}"
278 self.assertEqual(expected, actual, msg)
280 def test_expression_diff_with_color(self) -> None:
281 source, _ = read_data("expression.py")
282 config = THIS_DIR / "data" / "empty_pyproject.toml"
283 expected, _ = read_data("expression.diff")
284 tmp_file = Path(black.dump_to_file(source))
286 result = BlackRunner().invoke(
287 black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
291 actual = result.output
292 # We check the contents of the diff in `test_expression_diff`. All
293 # we need to check here is that color codes exist in the result.
294 self.assertIn("\033[1m", actual)
295 self.assertIn("\033[36m", actual)
296 self.assertIn("\033[32m", actual)
297 self.assertIn("\033[31m", actual)
298 self.assertIn("\033[0m", actual)
300 def test_detect_pos_only_arguments(self) -> None:
301 source, _ = read_data("pep_570")
302 root = black.lib2to3_parse(source)
303 features = black.get_features_used(root)
304 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
305 versions = black.detect_target_versions(root)
306 self.assertIn(black.TargetVersion.PY38, versions)
308 @patch("black.dump_to_file", dump_to_stderr)
309 def test_string_quotes(self) -> None:
310 source, expected = read_data("string_quotes")
311 mode = black.Mode(preview=True)
312 assert_format(source, expected, mode)
313 mode = replace(mode, string_normalization=False)
314 not_normalized = fs(source, mode=mode)
315 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
316 black.assert_equivalent(source, not_normalized)
317 black.assert_stable(source, not_normalized, mode=mode)
319 def test_skip_magic_trailing_comma(self) -> None:
320 source, _ = read_data("expression.py")
321 expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
322 tmp_file = Path(black.dump_to_file(source))
323 diff_header = re.compile(
324 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
325 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
328 result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
329 self.assertEqual(result.exit_code, 0)
332 actual = result.output
333 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
334 actual = actual.rstrip() + "\n" # the diff output has a trailing space
335 if expected != actual:
336 dump = black.dump_to_file(actual)
338 "Expected diff isn't equal to the actual. If you made changes to"
339 " expression.py and this is an anticipated difference, overwrite"
340 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
342 self.assertEqual(expected, actual, msg)
344 @patch("black.dump_to_file", dump_to_stderr)
345 def test_async_as_identifier(self) -> None:
346 source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
347 source, expected = read_data("async_as_identifier")
349 self.assertFormatEqual(expected, actual)
350 major, minor = sys.version_info[:2]
351 if major < 3 or (major <= 3 and minor < 7):
352 black.assert_equivalent(source, actual)
353 black.assert_stable(source, actual, DEFAULT_MODE)
354 # ensure black can parse this when the target is 3.6
355 self.invokeBlack([str(source_path), "--target-version", "py36"])
356 # but not on 3.7, because async/await is no longer an identifier
357 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
359 @patch("black.dump_to_file", dump_to_stderr)
360 def test_python37(self) -> None:
361 source_path = (THIS_DIR / "data" / "python37.py").resolve()
362 source, expected = read_data("python37")
364 self.assertFormatEqual(expected, actual)
365 major, minor = sys.version_info[:2]
366 if major > 3 or (major == 3 and minor >= 7):
367 black.assert_equivalent(source, actual)
368 black.assert_stable(source, actual, DEFAULT_MODE)
369 # ensure black can parse this when the target is 3.7
370 self.invokeBlack([str(source_path), "--target-version", "py37"])
371 # but not on 3.6, because we use async as a reserved keyword
372 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
374 def test_tab_comment_indentation(self) -> None:
375 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
376 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
377 self.assertFormatEqual(contents_spc, fs(contents_spc))
378 self.assertFormatEqual(contents_spc, fs(contents_tab))
380 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
381 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
382 self.assertFormatEqual(contents_spc, fs(contents_spc))
383 self.assertFormatEqual(contents_spc, fs(contents_tab))
385 # mixed tabs and spaces (valid Python 2 code)
386 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
387 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
388 self.assertFormatEqual(contents_spc, fs(contents_spc))
389 self.assertFormatEqual(contents_spc, fs(contents_tab))
391 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
392 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
393 self.assertFormatEqual(contents_spc, fs(contents_spc))
394 self.assertFormatEqual(contents_spc, fs(contents_tab))
396 def test_report_verbose(self) -> None:
397 report = Report(verbose=True)
401 def out(msg: str, **kwargs: Any) -> None:
402 out_lines.append(msg)
404 def err(msg: str, **kwargs: Any) -> None:
405 err_lines.append(msg)
407 with patch("black.output._out", out), patch("black.output._err", err):
408 report.done(Path("f1"), black.Changed.NO)
409 self.assertEqual(len(out_lines), 1)
410 self.assertEqual(len(err_lines), 0)
411 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
412 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
413 self.assertEqual(report.return_code, 0)
414 report.done(Path("f2"), black.Changed.YES)
415 self.assertEqual(len(out_lines), 2)
416 self.assertEqual(len(err_lines), 0)
417 self.assertEqual(out_lines[-1], "reformatted f2")
419 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
421 report.done(Path("f3"), black.Changed.CACHED)
422 self.assertEqual(len(out_lines), 3)
423 self.assertEqual(len(err_lines), 0)
425 out_lines[-1], "f3 wasn't modified on disk since last run."
428 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
430 self.assertEqual(report.return_code, 0)
432 self.assertEqual(report.return_code, 1)
434 report.failed(Path("e1"), "boom")
435 self.assertEqual(len(out_lines), 3)
436 self.assertEqual(len(err_lines), 1)
437 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
439 unstyle(str(report)),
440 "1 file reformatted, 2 files left unchanged, 1 file failed to"
443 self.assertEqual(report.return_code, 123)
444 report.done(Path("f3"), black.Changed.YES)
445 self.assertEqual(len(out_lines), 4)
446 self.assertEqual(len(err_lines), 1)
447 self.assertEqual(out_lines[-1], "reformatted f3")
449 unstyle(str(report)),
450 "2 files reformatted, 2 files left unchanged, 1 file failed to"
453 self.assertEqual(report.return_code, 123)
454 report.failed(Path("e2"), "boom")
455 self.assertEqual(len(out_lines), 4)
456 self.assertEqual(len(err_lines), 2)
457 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
459 unstyle(str(report)),
460 "2 files reformatted, 2 files left unchanged, 2 files failed to"
463 self.assertEqual(report.return_code, 123)
464 report.path_ignored(Path("wat"), "no match")
465 self.assertEqual(len(out_lines), 5)
466 self.assertEqual(len(err_lines), 2)
467 self.assertEqual(out_lines[-1], "wat ignored: no match")
469 unstyle(str(report)),
470 "2 files reformatted, 2 files left unchanged, 2 files failed to"
473 self.assertEqual(report.return_code, 123)
474 report.done(Path("f4"), black.Changed.NO)
475 self.assertEqual(len(out_lines), 6)
476 self.assertEqual(len(err_lines), 2)
477 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
479 unstyle(str(report)),
480 "2 files reformatted, 3 files left unchanged, 2 files failed to"
483 self.assertEqual(report.return_code, 123)
486 unstyle(str(report)),
487 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
488 " would fail to reformat.",
493 unstyle(str(report)),
494 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
495 " would fail to reformat.",
498 def test_report_quiet(self) -> None:
499 report = Report(quiet=True)
503 def out(msg: str, **kwargs: Any) -> None:
504 out_lines.append(msg)
506 def err(msg: str, **kwargs: Any) -> None:
507 err_lines.append(msg)
509 with patch("black.output._out", out), patch("black.output._err", err):
510 report.done(Path("f1"), black.Changed.NO)
511 self.assertEqual(len(out_lines), 0)
512 self.assertEqual(len(err_lines), 0)
513 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
514 self.assertEqual(report.return_code, 0)
515 report.done(Path("f2"), black.Changed.YES)
516 self.assertEqual(len(out_lines), 0)
517 self.assertEqual(len(err_lines), 0)
519 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
521 report.done(Path("f3"), black.Changed.CACHED)
522 self.assertEqual(len(out_lines), 0)
523 self.assertEqual(len(err_lines), 0)
525 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
527 self.assertEqual(report.return_code, 0)
529 self.assertEqual(report.return_code, 1)
531 report.failed(Path("e1"), "boom")
532 self.assertEqual(len(out_lines), 0)
533 self.assertEqual(len(err_lines), 1)
534 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
536 unstyle(str(report)),
537 "1 file reformatted, 2 files left unchanged, 1 file failed to"
540 self.assertEqual(report.return_code, 123)
541 report.done(Path("f3"), black.Changed.YES)
542 self.assertEqual(len(out_lines), 0)
543 self.assertEqual(len(err_lines), 1)
545 unstyle(str(report)),
546 "2 files reformatted, 2 files left unchanged, 1 file failed to"
549 self.assertEqual(report.return_code, 123)
550 report.failed(Path("e2"), "boom")
551 self.assertEqual(len(out_lines), 0)
552 self.assertEqual(len(err_lines), 2)
553 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
555 unstyle(str(report)),
556 "2 files reformatted, 2 files left unchanged, 2 files failed to"
559 self.assertEqual(report.return_code, 123)
560 report.path_ignored(Path("wat"), "no match")
561 self.assertEqual(len(out_lines), 0)
562 self.assertEqual(len(err_lines), 2)
564 unstyle(str(report)),
565 "2 files reformatted, 2 files left unchanged, 2 files failed to"
568 self.assertEqual(report.return_code, 123)
569 report.done(Path("f4"), black.Changed.NO)
570 self.assertEqual(len(out_lines), 0)
571 self.assertEqual(len(err_lines), 2)
573 unstyle(str(report)),
574 "2 files reformatted, 3 files left unchanged, 2 files failed to"
577 self.assertEqual(report.return_code, 123)
580 unstyle(str(report)),
581 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
582 " would fail to reformat.",
587 unstyle(str(report)),
588 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
589 " would fail to reformat.",
592 def test_report_normal(self) -> None:
593 report = black.Report()
597 def out(msg: str, **kwargs: Any) -> None:
598 out_lines.append(msg)
600 def err(msg: str, **kwargs: Any) -> None:
601 err_lines.append(msg)
603 with patch("black.output._out", out), patch("black.output._err", err):
604 report.done(Path("f1"), black.Changed.NO)
605 self.assertEqual(len(out_lines), 0)
606 self.assertEqual(len(err_lines), 0)
607 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
608 self.assertEqual(report.return_code, 0)
609 report.done(Path("f2"), black.Changed.YES)
610 self.assertEqual(len(out_lines), 1)
611 self.assertEqual(len(err_lines), 0)
612 self.assertEqual(out_lines[-1], "reformatted f2")
614 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
616 report.done(Path("f3"), black.Changed.CACHED)
617 self.assertEqual(len(out_lines), 1)
618 self.assertEqual(len(err_lines), 0)
619 self.assertEqual(out_lines[-1], "reformatted f2")
621 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
623 self.assertEqual(report.return_code, 0)
625 self.assertEqual(report.return_code, 1)
627 report.failed(Path("e1"), "boom")
628 self.assertEqual(len(out_lines), 1)
629 self.assertEqual(len(err_lines), 1)
630 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
632 unstyle(str(report)),
633 "1 file reformatted, 2 files left unchanged, 1 file failed to"
636 self.assertEqual(report.return_code, 123)
637 report.done(Path("f3"), black.Changed.YES)
638 self.assertEqual(len(out_lines), 2)
639 self.assertEqual(len(err_lines), 1)
640 self.assertEqual(out_lines[-1], "reformatted f3")
642 unstyle(str(report)),
643 "2 files reformatted, 2 files left unchanged, 1 file failed to"
646 self.assertEqual(report.return_code, 123)
647 report.failed(Path("e2"), "boom")
648 self.assertEqual(len(out_lines), 2)
649 self.assertEqual(len(err_lines), 2)
650 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
652 unstyle(str(report)),
653 "2 files reformatted, 2 files left unchanged, 2 files failed to"
656 self.assertEqual(report.return_code, 123)
657 report.path_ignored(Path("wat"), "no match")
658 self.assertEqual(len(out_lines), 2)
659 self.assertEqual(len(err_lines), 2)
661 unstyle(str(report)),
662 "2 files reformatted, 2 files left unchanged, 2 files failed to"
665 self.assertEqual(report.return_code, 123)
666 report.done(Path("f4"), black.Changed.NO)
667 self.assertEqual(len(out_lines), 2)
668 self.assertEqual(len(err_lines), 2)
670 unstyle(str(report)),
671 "2 files reformatted, 3 files left unchanged, 2 files failed to"
674 self.assertEqual(report.return_code, 123)
677 unstyle(str(report)),
678 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
679 " would fail to reformat.",
684 unstyle(str(report)),
685 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
686 " would fail to reformat.",
689 def test_lib2to3_parse(self) -> None:
690 with self.assertRaises(black.InvalidInput):
691 black.lib2to3_parse("invalid syntax")
694 black.lib2to3_parse(straddling)
695 black.lib2to3_parse(straddling, {TargetVersion.PY36})
698 with self.assertRaises(black.InvalidInput):
699 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
701 py3_only = "exec(x, end=y)"
702 black.lib2to3_parse(py3_only)
703 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
705 def test_get_features_used_decorator(self) -> None:
706 # Test the feature detection of new decorator syntax
707 # since this makes some test cases of test_get_features_used()
708 # fails if it fails, this is tested first so that a useful case
710 simples, relaxed = read_data("decorators")
711 # skip explanation comments at the top of the file
712 for simple_test in simples.split("##")[1:]:
713 node = black.lib2to3_parse(simple_test)
714 decorator = str(node.children[0].children[0]).strip()
716 Feature.RELAXED_DECORATORS,
717 black.get_features_used(node),
719 f"decorator '{decorator}' follows python<=3.8 syntax"
720 "but is detected as 3.9+"
721 # f"The full node is\n{node!r}"
724 # skip the '# output' comment at the top of the output part
725 for relaxed_test in relaxed.split("##")[1:]:
726 node = black.lib2to3_parse(relaxed_test)
727 decorator = str(node.children[0].children[0]).strip()
729 Feature.RELAXED_DECORATORS,
730 black.get_features_used(node),
732 f"decorator '{decorator}' uses python3.9+ syntax"
733 "but is detected as python<=3.8"
734 # f"The full node is\n{node!r}"
738 def test_get_features_used(self) -> None:
739 node = black.lib2to3_parse("def f(*, arg): ...\n")
740 self.assertEqual(black.get_features_used(node), set())
741 node = black.lib2to3_parse("def f(*, arg,): ...\n")
742 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
743 node = black.lib2to3_parse("f(*arg,)\n")
745 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
747 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
748 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
749 node = black.lib2to3_parse("123_456\n")
750 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
751 node = black.lib2to3_parse("123456\n")
752 self.assertEqual(black.get_features_used(node), set())
753 source, expected = read_data("function")
754 node = black.lib2to3_parse(source)
755 expected_features = {
756 Feature.TRAILING_COMMA_IN_CALL,
757 Feature.TRAILING_COMMA_IN_DEF,
760 self.assertEqual(black.get_features_used(node), expected_features)
761 node = black.lib2to3_parse(expected)
762 self.assertEqual(black.get_features_used(node), expected_features)
763 source, expected = read_data("expression")
764 node = black.lib2to3_parse(source)
765 self.assertEqual(black.get_features_used(node), set())
766 node = black.lib2to3_parse(expected)
767 self.assertEqual(black.get_features_used(node), set())
768 node = black.lib2to3_parse("lambda a, /, b: ...")
769 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
770 node = black.lib2to3_parse("def fn(a, /, b): ...")
771 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
772 node = black.lib2to3_parse("def fn(): yield a, b")
773 self.assertEqual(black.get_features_used(node), set())
774 node = black.lib2to3_parse("def fn(): return a, b")
775 self.assertEqual(black.get_features_used(node), set())
776 node = black.lib2to3_parse("def fn(): yield *b, c")
777 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
778 node = black.lib2to3_parse("def fn(): return a, *b, c")
779 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
780 node = black.lib2to3_parse("x = a, *b, c")
781 self.assertEqual(black.get_features_used(node), set())
782 node = black.lib2to3_parse("x: Any = regular")
783 self.assertEqual(black.get_features_used(node), set())
784 node = black.lib2to3_parse("x: Any = (regular, regular)")
785 self.assertEqual(black.get_features_used(node), set())
786 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
787 self.assertEqual(black.get_features_used(node), set())
788 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
790 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
793 def test_get_features_used_for_future_flags(self) -> None:
794 for src, features in [
795 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
797 "from __future__ import (other, annotations)",
798 {Feature.FUTURE_ANNOTATIONS},
800 ("a = 1 + 2\nfrom something import annotations", set()),
801 ("from __future__ import x, y", set()),
803 with self.subTest(src=src, features=features):
804 node = black.lib2to3_parse(src)
805 future_imports = black.get_future_imports(node)
807 black.get_features_used(node, future_imports=future_imports),
811 def test_get_future_imports(self) -> None:
812 node = black.lib2to3_parse("\n")
813 self.assertEqual(set(), black.get_future_imports(node))
814 node = black.lib2to3_parse("from __future__ import black\n")
815 self.assertEqual({"black"}, black.get_future_imports(node))
816 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
817 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
818 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
819 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
820 node = black.lib2to3_parse(
821 "from __future__ import multiple\nfrom __future__ import imports\n"
823 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
824 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
825 self.assertEqual({"black"}, black.get_future_imports(node))
826 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
827 self.assertEqual({"black"}, black.get_future_imports(node))
828 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
829 self.assertEqual(set(), black.get_future_imports(node))
830 node = black.lib2to3_parse("from some.module import black\n")
831 self.assertEqual(set(), black.get_future_imports(node))
832 node = black.lib2to3_parse(
833 "from __future__ import unicode_literals as _unicode_literals"
835 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
836 node = black.lib2to3_parse(
837 "from __future__ import unicode_literals as _lol, print"
839 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
841 @pytest.mark.incompatible_with_mypyc
842 def test_debug_visitor(self) -> None:
843 source, _ = read_data("debug_visitor.py")
844 expected, _ = read_data("debug_visitor.out")
848 def out(msg: str, **kwargs: Any) -> None:
849 out_lines.append(msg)
851 def err(msg: str, **kwargs: Any) -> None:
852 err_lines.append(msg)
854 with patch("black.debug.out", out):
855 DebugVisitor.show(source)
856 actual = "\n".join(out_lines) + "\n"
858 if expected != actual:
859 log_name = black.dump_to_file(*out_lines)
863 f"AST print out is different. Actual version dumped to {log_name}",
866 def test_format_file_contents(self) -> None:
869 with self.assertRaises(black.NothingChanged):
870 black.format_file_contents(empty, mode=mode, fast=False)
872 with self.assertRaises(black.NothingChanged):
873 black.format_file_contents(just_nl, mode=mode, fast=False)
874 same = "j = [1, 2, 3]\n"
875 with self.assertRaises(black.NothingChanged):
876 black.format_file_contents(same, mode=mode, fast=False)
877 different = "j = [1,2,3]"
879 actual = black.format_file_contents(different, mode=mode, fast=False)
880 self.assertEqual(expected, actual)
881 invalid = "return if you can"
882 with self.assertRaises(black.InvalidInput) as e:
883 black.format_file_contents(invalid, mode=mode, fast=False)
884 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
886 def test_endmarker(self) -> None:
887 n = black.lib2to3_parse("\n")
888 self.assertEqual(n.type, black.syms.file_input)
889 self.assertEqual(len(n.children), 1)
890 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
892 @pytest.mark.incompatible_with_mypyc
893 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
894 def test_assertFormatEqual(self) -> None:
898 def out(msg: str, **kwargs: Any) -> None:
899 out_lines.append(msg)
901 def err(msg: str, **kwargs: Any) -> None:
902 err_lines.append(msg)
904 with patch("black.output._out", out), patch("black.output._err", err):
905 with self.assertRaises(AssertionError):
906 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
908 out_str = "".join(out_lines)
909 self.assertIn("Expected tree:", out_str)
910 self.assertIn("Actual tree:", out_str)
911 self.assertEqual("".join(err_lines), "")
914 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
915 def test_works_in_mono_process_only_environment(self) -> None:
916 with cache_dir() as workspace:
918 (workspace / "one.py").resolve(),
919 (workspace / "two.py").resolve(),
921 f.write_text('print("hello")\n')
922 self.invokeBlack([str(workspace)])
925 def test_check_diff_use_together(self) -> None:
927 # Files which will be reformatted.
928 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
929 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
930 # Files which will not be reformatted.
931 src2 = (THIS_DIR / "data" / "composition.py").resolve()
932 self.invokeBlack([str(src2), "--diff", "--check"])
933 # Multi file command.
934 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
936 def test_no_src_fails(self) -> None:
938 self.invokeBlack([], exit_code=1)
940 def test_src_and_code_fails(self) -> None:
942 self.invokeBlack([".", "-c", "0"], exit_code=1)
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__, "-c", "0"],
1201 def test_required_version_matches_partial_version(self) -> None:
1203 ["--required-version", black.__version__.split(".")[0], "-c", "0"],
1208 def test_required_version_does_not_match_on_minor_version(self) -> None:
1210 ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
1215 def test_required_version_does_not_match_version(self) -> None:
1216 result = BlackRunner().invoke(
1218 ["--required-version", "20.99b", "-c", "0"],
1220 self.assertEqual(result.exit_code, 1)
1221 self.assertIn("required version", result.stderr)
1223 def test_preserves_line_endings(self) -> None:
1224 with TemporaryDirectory() as workspace:
1225 test_file = Path(workspace) / "test.py"
1226 for nl in ["\n", "\r\n"]:
1227 contents = nl.join(["def f( ):", " pass"])
1228 test_file.write_bytes(contents.encode())
1229 ff(test_file, write_back=black.WriteBack.YES)
1230 updated_contents: bytes = test_file.read_bytes()
1231 self.assertIn(nl.encode(), updated_contents)
1233 self.assertNotIn(b"\r\n", updated_contents)
1235 def test_preserves_line_endings_via_stdin(self) -> None:
1236 for nl in ["\n", "\r\n"]:
1237 contents = nl.join(["def f( ):", " pass"])
1238 runner = BlackRunner()
1239 result = runner.invoke(
1240 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1242 self.assertEqual(result.exit_code, 0)
1243 output = result.stdout_bytes
1244 self.assertIn(nl.encode("utf8"), output)
1246 self.assertNotIn(b"\r\n", output)
1248 def test_assert_equivalent_different_asts(self) -> None:
1249 with self.assertRaises(AssertionError):
1250 black.assert_equivalent("{}", "None")
1252 def test_shhh_click(self) -> None:
1254 from click import _unicodefun
1255 except ModuleNotFoundError:
1256 self.skipTest("Incompatible Click version")
1257 if not hasattr(_unicodefun, "_verify_python3_env"):
1258 self.skipTest("Incompatible Click version")
1259 # First, let's see if Click is crashing with a preferred ASCII charset.
1260 with patch("locale.getpreferredencoding") as gpe:
1261 gpe.return_value = "ASCII"
1262 with self.assertRaises(RuntimeError):
1263 _unicodefun._verify_python3_env() # type: ignore
1264 # Now, let's silence Click...
1266 # ...and confirm it's silent.
1267 with patch("locale.getpreferredencoding") as gpe:
1268 gpe.return_value = "ASCII"
1270 _unicodefun._verify_python3_env() # type: ignore
1271 except RuntimeError as re:
1272 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1274 def test_root_logger_not_used_directly(self) -> None:
1275 def fail(*args: Any, **kwargs: Any) -> None:
1276 self.fail("Record created with root logger")
1278 with patch.multiple(
1287 ff(THIS_DIR / "util.py")
1289 def test_invalid_config_return_code(self) -> None:
1290 tmp_file = Path(black.dump_to_file())
1292 tmp_config = Path(black.dump_to_file())
1294 args = ["--config", str(tmp_config), str(tmp_file)]
1295 self.invokeBlack(args, exit_code=2, ignore_config=False)
1299 def test_parse_pyproject_toml(self) -> None:
1300 test_toml_file = THIS_DIR / "test.toml"
1301 config = black.parse_pyproject_toml(str(test_toml_file))
1302 self.assertEqual(config["verbose"], 1)
1303 self.assertEqual(config["check"], "no")
1304 self.assertEqual(config["diff"], "y")
1305 self.assertEqual(config["color"], True)
1306 self.assertEqual(config["line_length"], 79)
1307 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1308 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1309 self.assertEqual(config["exclude"], r"\.pyi?$")
1310 self.assertEqual(config["include"], r"\.py?$")
1312 def test_read_pyproject_toml(self) -> None:
1313 test_toml_file = THIS_DIR / "test.toml"
1314 fake_ctx = FakeContext()
1315 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1316 config = fake_ctx.default_map
1317 self.assertEqual(config["verbose"], "1")
1318 self.assertEqual(config["check"], "no")
1319 self.assertEqual(config["diff"], "y")
1320 self.assertEqual(config["color"], "True")
1321 self.assertEqual(config["line_length"], "79")
1322 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1323 self.assertEqual(config["exclude"], r"\.pyi?$")
1324 self.assertEqual(config["include"], r"\.py?$")
1326 @pytest.mark.incompatible_with_mypyc
1327 def test_find_project_root(self) -> None:
1328 with TemporaryDirectory() as workspace:
1329 root = Path(workspace)
1330 test_dir = root / "test"
1333 src_dir = root / "src"
1336 root_pyproject = root / "pyproject.toml"
1337 root_pyproject.touch()
1338 src_pyproject = src_dir / "pyproject.toml"
1339 src_pyproject.touch()
1340 src_python = src_dir / "foo.py"
1344 black.find_project_root((src_dir, test_dir)),
1345 (root.resolve(), "pyproject.toml"),
1348 black.find_project_root((src_dir,)),
1349 (src_dir.resolve(), "pyproject.toml"),
1352 black.find_project_root((src_python,)),
1353 (src_dir.resolve(), "pyproject.toml"),
1357 "black.files.find_user_pyproject_toml",
1358 black.files.find_user_pyproject_toml.__wrapped__,
1360 def test_find_user_pyproject_toml_linux(self) -> None:
1361 if system() == "Windows":
1364 # Test if XDG_CONFIG_HOME is checked
1365 with TemporaryDirectory() as workspace:
1366 tmp_user_config = Path(workspace) / "black"
1367 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1369 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1372 # Test fallback for XDG_CONFIG_HOME
1373 with patch.dict("os.environ"):
1374 os.environ.pop("XDG_CONFIG_HOME", None)
1375 fallback_user_config = Path("~/.config").expanduser() / "black"
1377 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1380 def test_find_user_pyproject_toml_windows(self) -> None:
1381 if system() != "Windows":
1384 user_config_path = Path.home() / ".black"
1386 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1389 def test_bpo_33660_workaround(self) -> None:
1390 if system() == "Windows":
1393 # https://bugs.python.org/issue33660
1395 with change_directory(root):
1396 path = Path("workspace") / "project"
1397 report = black.Report(verbose=True)
1398 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1399 self.assertEqual(normalized_path, "workspace/project")
1401 def test_newline_comment_interaction(self) -> None:
1402 source = "class A:\\\r\n# type: ignore\n pass\n"
1403 output = black.format_str(source, mode=DEFAULT_MODE)
1404 black.assert_stable(source, output, mode=DEFAULT_MODE)
1406 def test_bpo_2142_workaround(self) -> None:
1408 # https://bugs.python.org/issue2142
1410 source, _ = read_data("missing_final_newline.py")
1411 # read_data adds a trailing newline
1412 source = source.rstrip()
1413 expected, _ = read_data("missing_final_newline.diff")
1414 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1415 diff_header = re.compile(
1416 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1417 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1420 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1421 self.assertEqual(result.exit_code, 0)
1424 actual = result.output
1425 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1426 self.assertEqual(actual, expected)
1429 def compare_results(
1430 result: click.testing.Result, expected_value: str, expected_exit_code: int
1432 """Helper method to test the value and exit code of a click Result."""
1434 result.output == expected_value
1435 ), "The output did not match the expected value."
1436 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1438 def test_code_option(self) -> None:
1439 """Test the code option with no changes."""
1440 code = 'print("Hello world")\n'
1441 args = ["--code", code]
1442 result = CliRunner().invoke(black.main, args)
1444 self.compare_results(result, code, 0)
1446 def test_code_option_changed(self) -> None:
1447 """Test the code option when changes are required."""
1448 code = "print('hello world')"
1449 formatted = black.format_str(code, mode=DEFAULT_MODE)
1451 args = ["--code", code]
1452 result = CliRunner().invoke(black.main, args)
1454 self.compare_results(result, formatted, 0)
1456 def test_code_option_check(self) -> None:
1457 """Test the code option when check is passed."""
1458 args = ["--check", "--code", 'print("Hello world")\n']
1459 result = CliRunner().invoke(black.main, args)
1460 self.compare_results(result, "", 0)
1462 def test_code_option_check_changed(self) -> None:
1463 """Test the code option when changes are required, and check is passed."""
1464 args = ["--check", "--code", "print('hello world')"]
1465 result = CliRunner().invoke(black.main, args)
1466 self.compare_results(result, "", 1)
1468 def test_code_option_diff(self) -> None:
1469 """Test the code option when diff is passed."""
1470 code = "print('hello world')"
1471 formatted = black.format_str(code, mode=DEFAULT_MODE)
1472 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1474 args = ["--diff", "--code", code]
1475 result = CliRunner().invoke(black.main, args)
1477 # Remove time from diff
1478 output = DIFF_TIME.sub("", result.output)
1480 assert output == result_diff, "The output did not match the expected value."
1481 assert result.exit_code == 0, "The exit code is incorrect."
1483 def test_code_option_color_diff(self) -> None:
1484 """Test the code option when color and diff are passed."""
1485 code = "print('hello world')"
1486 formatted = black.format_str(code, mode=DEFAULT_MODE)
1488 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1489 result_diff = color_diff(result_diff)
1491 args = ["--diff", "--color", "--code", code]
1492 result = CliRunner().invoke(black.main, args)
1494 # Remove time from diff
1495 output = DIFF_TIME.sub("", result.output)
1497 assert output == result_diff, "The output did not match the expected value."
1498 assert result.exit_code == 0, "The exit code is incorrect."
1500 @pytest.mark.incompatible_with_mypyc
1501 def test_code_option_safe(self) -> None:
1502 """Test that the code option throws an error when the sanity checks fail."""
1503 # Patch black.assert_equivalent to ensure the sanity checks fail
1504 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1505 code = 'print("Hello world")'
1506 error_msg = f"{code}\nerror: cannot format <string>: \n"
1508 args = ["--safe", "--code", code]
1509 result = CliRunner().invoke(black.main, args)
1511 self.compare_results(result, error_msg, 123)
1513 def test_code_option_fast(self) -> None:
1514 """Test that the code option ignores errors when the sanity checks fail."""
1515 # Patch black.assert_equivalent to ensure the sanity checks fail
1516 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1517 code = 'print("Hello world")'
1518 formatted = black.format_str(code, mode=DEFAULT_MODE)
1520 args = ["--fast", "--code", code]
1521 result = CliRunner().invoke(black.main, args)
1523 self.compare_results(result, formatted, 0)
1525 @pytest.mark.incompatible_with_mypyc
1526 def test_code_option_config(self) -> None:
1528 Test that the code option finds the pyproject.toml in the current directory.
1530 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1531 args = ["--code", "print"]
1532 # This is the only directory known to contain a pyproject.toml
1533 with change_directory(PROJECT_ROOT):
1534 CliRunner().invoke(black.main, args)
1535 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1538 len(parse.mock_calls) >= 1
1539 ), "Expected config parse to be called with the current directory."
1541 _, call_args, _ = parse.mock_calls[0]
1543 call_args[0].lower() == str(pyproject_path).lower()
1544 ), "Incorrect config loaded."
1546 @pytest.mark.incompatible_with_mypyc
1547 def test_code_option_parent_config(self) -> None:
1549 Test that the code option finds the pyproject.toml in the parent directory.
1551 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1552 with change_directory(THIS_DIR):
1553 args = ["--code", "print"]
1554 CliRunner().invoke(black.main, args)
1556 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1558 len(parse.mock_calls) >= 1
1559 ), "Expected config parse to be called with the current directory."
1561 _, call_args, _ = parse.mock_calls[0]
1563 call_args[0].lower() == str(pyproject_path).lower()
1564 ), "Incorrect config loaded."
1566 def test_for_handled_unexpected_eof_error(self) -> None:
1568 Test that an unexpected EOF SyntaxError is nicely presented.
1570 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1571 black.lib2to3_parse("print(", {})
1573 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1575 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1576 with pytest.raises(AssertionError) as err:
1577 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1580 # Unfortunately the SyntaxError message has changed in newer versions so we
1581 # can't match it directly.
1582 err.match("invalid character")
1583 err.match(r"\(<unknown>, line 1\)")
1587 def test_get_cache_dir(
1590 monkeypatch: pytest.MonkeyPatch,
1592 # Create multiple cache directories
1593 workspace1 = tmp_path / "ws1"
1595 workspace2 = tmp_path / "ws2"
1598 # Force user_cache_dir to use the temporary directory for easier assertions
1599 patch_user_cache_dir = patch(
1600 target="black.cache.user_cache_dir",
1602 return_value=str(workspace1),
1605 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1606 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1607 with patch_user_cache_dir:
1608 assert get_cache_dir() == workspace1
1610 # If it is set, use the path provided in the env var.
1611 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1612 assert get_cache_dir() == workspace2
1614 def test_cache_broken_file(self) -> None:
1616 with cache_dir() as workspace:
1617 cache_file = get_cache_file(mode)
1618 cache_file.write_text("this is not a pickle")
1619 assert black.read_cache(mode) == {}
1620 src = (workspace / "test.py").resolve()
1621 src.write_text("print('hello')")
1622 invokeBlack([str(src)])
1623 cache = black.read_cache(mode)
1624 assert str(src) in cache
1626 def test_cache_single_file_already_cached(self) -> None:
1628 with cache_dir() as workspace:
1629 src = (workspace / "test.py").resolve()
1630 src.write_text("print('hello')")
1631 black.write_cache({}, [src], mode)
1632 invokeBlack([str(src)])
1633 assert src.read_text() == "print('hello')"
1636 def test_cache_multiple_files(self) -> None:
1638 with cache_dir() as workspace, patch(
1639 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1641 one = (workspace / "one.py").resolve()
1642 with one.open("w") as fobj:
1643 fobj.write("print('hello')")
1644 two = (workspace / "two.py").resolve()
1645 with two.open("w") as fobj:
1646 fobj.write("print('hello')")
1647 black.write_cache({}, [one], mode)
1648 invokeBlack([str(workspace)])
1649 with one.open("r") as fobj:
1650 assert fobj.read() == "print('hello')"
1651 with two.open("r") as fobj:
1652 assert fobj.read() == 'print("hello")\n'
1653 cache = black.read_cache(mode)
1654 assert str(one) in cache
1655 assert str(two) in cache
1657 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1658 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1660 with cache_dir() as workspace:
1661 src = (workspace / "test.py").resolve()
1662 with src.open("w") as fobj:
1663 fobj.write("print('hello')")
1664 with patch("black.read_cache") as read_cache, patch(
1667 cmd = [str(src), "--diff"]
1669 cmd.append("--color")
1671 cache_file = get_cache_file(mode)
1672 assert cache_file.exists() is False
1673 write_cache.assert_not_called()
1674 read_cache.assert_not_called()
1676 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1678 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1679 with cache_dir() as workspace:
1680 for tag in range(0, 4):
1681 src = (workspace / f"test{tag}.py").resolve()
1682 with src.open("w") as fobj:
1683 fobj.write("print('hello')")
1684 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1685 cmd = ["--diff", str(workspace)]
1687 cmd.append("--color")
1688 invokeBlack(cmd, exit_code=0)
1689 # this isn't quite doing what we want, but if it _isn't_
1690 # called then we cannot be using the lock it provides
1693 def test_no_cache_when_stdin(self) -> None:
1696 result = CliRunner().invoke(
1697 black.main, ["-"], input=BytesIO(b"print('hello')")
1699 assert not result.exit_code
1700 cache_file = get_cache_file(mode)
1701 assert not cache_file.exists()
1703 def test_read_cache_no_cachefile(self) -> None:
1706 assert black.read_cache(mode) == {}
1708 def test_write_cache_read_cache(self) -> None:
1710 with cache_dir() as workspace:
1711 src = (workspace / "test.py").resolve()
1713 black.write_cache({}, [src], mode)
1714 cache = black.read_cache(mode)
1715 assert str(src) in cache
1716 assert cache[str(src)] == black.get_cache_info(src)
1718 def test_filter_cached(self) -> None:
1719 with TemporaryDirectory() as workspace:
1720 path = Path(workspace)
1721 uncached = (path / "uncached").resolve()
1722 cached = (path / "cached").resolve()
1723 cached_but_changed = (path / "changed").resolve()
1726 cached_but_changed.touch()
1728 str(cached): black.get_cache_info(cached),
1729 str(cached_but_changed): (0.0, 0),
1731 todo, done = black.filter_cached(
1732 cache, {uncached, cached, cached_but_changed}
1734 assert todo == {uncached, cached_but_changed}
1735 assert done == {cached}
1737 def test_write_cache_creates_directory_if_needed(self) -> None:
1739 with cache_dir(exists=False) as workspace:
1740 assert not workspace.exists()
1741 black.write_cache({}, [], mode)
1742 assert workspace.exists()
1745 def test_failed_formatting_does_not_get_cached(self) -> None:
1747 with cache_dir() as workspace, patch(
1748 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1750 failing = (workspace / "failing.py").resolve()
1751 with failing.open("w") as fobj:
1752 fobj.write("not actually python")
1753 clean = (workspace / "clean.py").resolve()
1754 with clean.open("w") as fobj:
1755 fobj.write('print("hello")\n')
1756 invokeBlack([str(workspace)], exit_code=123)
1757 cache = black.read_cache(mode)
1758 assert str(failing) not in cache
1759 assert str(clean) in cache
1761 def test_write_cache_write_fail(self) -> None:
1763 with cache_dir(), patch.object(Path, "open") as mock:
1764 mock.side_effect = OSError
1765 black.write_cache({}, [], mode)
1767 def test_read_cache_line_lengths(self) -> None:
1769 short_mode = replace(DEFAULT_MODE, line_length=1)
1770 with cache_dir() as workspace:
1771 path = (workspace / "file.py").resolve()
1773 black.write_cache({}, [path], mode)
1774 one = black.read_cache(mode)
1775 assert str(path) in one
1776 two = black.read_cache(short_mode)
1777 assert str(path) not in two
1780 def assert_collected_sources(
1781 src: Sequence[Union[str, Path]],
1782 expected: Sequence[Union[str, Path]],
1784 ctx: Optional[FakeContext] = None,
1785 exclude: Optional[str] = None,
1786 include: Optional[str] = None,
1787 extend_exclude: Optional[str] = None,
1788 force_exclude: Optional[str] = None,
1789 stdin_filename: Optional[str] = None,
1791 gs_src = tuple(str(Path(s)) for s in src)
1792 gs_expected = [Path(s) for s in expected]
1793 gs_exclude = None if exclude is None else compile_pattern(exclude)
1794 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
1795 gs_extend_exclude = (
1796 None if extend_exclude is None else compile_pattern(extend_exclude)
1798 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
1799 collected = black.get_sources(
1800 ctx=ctx or FakeContext(),
1806 extend_exclude=gs_extend_exclude,
1807 force_exclude=gs_force_exclude,
1808 report=black.Report(),
1809 stdin_filename=stdin_filename,
1811 assert sorted(collected) == sorted(gs_expected)
1814 class TestFileCollection:
1815 def test_include_exclude(self) -> None:
1816 path = THIS_DIR / "data" / "include_exclude_tests"
1819 Path(path / "b/dont_exclude/a.py"),
1820 Path(path / "b/dont_exclude/a.pyi"),
1822 assert_collected_sources(
1826 exclude=r"/exclude/|/\.definitely_exclude/",
1829 def test_gitignore_used_as_default(self) -> None:
1830 base = Path(DATA_DIR / "include_exclude_tests")
1832 base / "b/.definitely_exclude/a.py",
1833 base / "b/.definitely_exclude/a.pyi",
1837 ctx.obj["root"] = base
1838 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
1840 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
1841 def test_exclude_for_issue_1572(self) -> None:
1842 # Exclude shouldn't touch files that were explicitly given to Black through the
1843 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1844 # https://github.com/psf/black/issues/1572
1845 path = DATA_DIR / "include_exclude_tests"
1846 src = [path / "b/exclude/a.py"]
1847 expected = [path / "b/exclude/a.py"]
1848 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1850 def test_gitignore_exclude(self) -> None:
1851 path = THIS_DIR / "data" / "include_exclude_tests"
1852 include = re.compile(r"\.pyi?$")
1853 exclude = re.compile(r"")
1854 report = black.Report()
1855 gitignore = PathSpec.from_lines(
1856 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1858 sources: List[Path] = []
1860 Path(path / "b/dont_exclude/a.py"),
1861 Path(path / "b/dont_exclude/a.pyi"),
1863 this_abs = THIS_DIR.resolve()
1865 black.gen_python_files(
1878 assert sorted(expected) == sorted(sources)
1880 def test_nested_gitignore(self) -> None:
1881 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
1882 include = re.compile(r"\.pyi?$")
1883 exclude = re.compile(r"")
1884 root_gitignore = black.files.get_gitignore(path)
1885 report = black.Report()
1886 expected: List[Path] = [
1887 Path(path / "x.py"),
1888 Path(path / "root/b.py"),
1889 Path(path / "root/c.py"),
1890 Path(path / "root/child/c.py"),
1892 this_abs = THIS_DIR.resolve()
1894 black.gen_python_files(
1907 assert sorted(expected) == sorted(sources)
1909 def test_invalid_gitignore(self) -> None:
1910 path = THIS_DIR / "data" / "invalid_gitignore_tests"
1911 empty_config = path / "pyproject.toml"
1912 result = BlackRunner().invoke(
1913 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1915 assert result.exit_code == 1
1916 assert result.stderr_bytes is not None
1918 gitignore = path / ".gitignore"
1919 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1921 def test_invalid_nested_gitignore(self) -> None:
1922 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
1923 empty_config = path / "pyproject.toml"
1924 result = BlackRunner().invoke(
1925 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1927 assert result.exit_code == 1
1928 assert result.stderr_bytes is not None
1930 gitignore = path / "a" / ".gitignore"
1931 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1933 def test_empty_include(self) -> None:
1934 path = DATA_DIR / "include_exclude_tests"
1937 Path(path / "b/exclude/a.pie"),
1938 Path(path / "b/exclude/a.py"),
1939 Path(path / "b/exclude/a.pyi"),
1940 Path(path / "b/dont_exclude/a.pie"),
1941 Path(path / "b/dont_exclude/a.py"),
1942 Path(path / "b/dont_exclude/a.pyi"),
1943 Path(path / "b/.definitely_exclude/a.pie"),
1944 Path(path / "b/.definitely_exclude/a.py"),
1945 Path(path / "b/.definitely_exclude/a.pyi"),
1946 Path(path / ".gitignore"),
1947 Path(path / "pyproject.toml"),
1949 # Setting exclude explicitly to an empty string to block .gitignore usage.
1950 assert_collected_sources(src, expected, include="", exclude="")
1952 def test_extend_exclude(self) -> None:
1953 path = DATA_DIR / "include_exclude_tests"
1956 Path(path / "b/exclude/a.py"),
1957 Path(path / "b/dont_exclude/a.py"),
1959 assert_collected_sources(
1960 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
1963 @pytest.mark.incompatible_with_mypyc
1964 def test_symlink_out_of_root_directory(self) -> None:
1966 root = THIS_DIR.resolve()
1968 include = re.compile(black.DEFAULT_INCLUDES)
1969 exclude = re.compile(black.DEFAULT_EXCLUDES)
1970 report = black.Report()
1971 gitignore = PathSpec.from_lines("gitwildmatch", [])
1972 # `child` should behave like a symlink which resolved path is clearly
1973 # outside of the `root` directory.
1974 path.iterdir.return_value = [child]
1975 child.resolve.return_value = Path("/a/b/c")
1976 child.as_posix.return_value = "/a/b/c"
1977 child.is_symlink.return_value = True
1980 black.gen_python_files(
1993 except ValueError as ve:
1994 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
1995 path.iterdir.assert_called_once()
1996 child.resolve.assert_called_once()
1997 child.is_symlink.assert_called_once()
1998 # `child` should behave like a strange file which resolved path is clearly
1999 # outside of the `root` directory.
2000 child.is_symlink.return_value = False
2001 with pytest.raises(ValueError):
2003 black.gen_python_files(
2016 path.iterdir.assert_called()
2017 assert path.iterdir.call_count == 2
2018 child.resolve.assert_called()
2019 assert child.resolve.call_count == 2
2020 child.is_symlink.assert_called()
2021 assert child.is_symlink.call_count == 2
2023 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2024 def test_get_sources_with_stdin(self) -> None:
2027 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2029 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2030 def test_get_sources_with_stdin_filename(self) -> None:
2032 stdin_filename = str(THIS_DIR / "data/collections.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_exclude(self) -> None:
2043 # 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
2046 path = DATA_DIR / "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 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_extend_exclude(self) -> None:
2059 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2060 # file being passed directly. This is the same as
2061 # test_exclude_for_issue_1572
2063 path = THIS_DIR / "data" / "include_exclude_tests"
2064 stdin_filename = str(path / "b/exclude/a.py")
2065 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2066 assert_collected_sources(
2069 extend_exclude=r"/exclude/|a\.py",
2070 stdin_filename=stdin_filename,
2073 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2074 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2075 # Force exclude should exclude the file when passing it through
2077 path = THIS_DIR / "data" / "include_exclude_tests"
2078 stdin_filename = str(path / "b/exclude/a.py")
2079 assert_collected_sources(
2082 force_exclude=r"/exclude/|a\.py",
2083 stdin_filename=stdin_filename,
2088 with open(black.__file__, "r", encoding="utf-8") as _bf:
2089 black_source_lines = _bf.readlines()
2090 except UnicodeDecodeError:
2091 if not black.COMPILED:
2096 frame: types.FrameType, event: str, arg: Any
2097 ) -> Callable[[types.FrameType, str, Any], Any]:
2098 """Show function calls `from black/__init__.py` as they happen.
2100 Register this with `sys.settrace()` in a test you're debugging.
2105 stack = len(inspect.stack()) - 19
2107 filename = frame.f_code.co_filename
2108 lineno = frame.f_lineno
2109 func_sig_lineno = lineno - 1
2110 funcname = black_source_lines[func_sig_lineno].strip()
2111 while funcname.startswith("@"):
2112 func_sig_lineno += 1
2113 funcname = black_source_lines[func_sig_lineno].strip()
2114 if "black/__init__.py" in filename:
2115 print(f"{' ' * stack}{lineno}:{funcname}")