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 EMPTY_CONFIG = THIS_DIR / "data" / "empty_pyproject.toml"
67 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
68 DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
69 DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
73 # Match the time output in a diff, but nothing else
74 DIFF_TIME = re.compile(r"\t[\d\-:+\. ]+")
78 def cache_dir(exists: bool = True) -> Iterator[Path]:
79 with TemporaryDirectory() as workspace:
80 cache_dir = Path(workspace)
82 cache_dir = cache_dir / "new"
83 with patch("black.cache.CACHE_DIR", cache_dir):
88 def event_loop() -> Iterator[None]:
89 policy = asyncio.get_event_loop_policy()
90 loop = policy.new_event_loop()
91 asyncio.set_event_loop(loop)
99 class FakeContext(click.Context):
100 """A fake click Context for when calling functions that need it."""
102 def __init__(self) -> None:
103 self.default_map: Dict[str, Any] = {}
104 # Dummy root, since most of the tests don't care about it
105 self.obj: Dict[str, Any] = {"root": PROJECT_ROOT}
108 class FakeParameter(click.Parameter):
109 """A fake click Parameter for when calling functions that need it."""
111 def __init__(self) -> None:
115 class BlackRunner(CliRunner):
116 """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
118 def __init__(self) -> None:
119 super().__init__(mix_stderr=False)
123 args: List[str], exit_code: int = 0, ignore_config: bool = True
125 runner = BlackRunner()
127 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
128 result = runner.invoke(black.main, args, catch_exceptions=False)
129 assert result.stdout_bytes is not None
130 assert result.stderr_bytes is not None
132 f"Failed with args: {args}\n"
133 f"stdout: {result.stdout_bytes.decode()!r}\n"
134 f"stderr: {result.stderr_bytes.decode()!r}\n"
135 f"exception: {result.exception}"
137 assert result.exit_code == exit_code, msg
140 class BlackTestCase(BlackBaseTestCase):
141 invokeBlack = staticmethod(invokeBlack)
143 def test_empty_ff(self) -> None:
145 tmp_file = Path(black.dump_to_file())
147 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
148 with open(tmp_file, encoding="utf8") as f:
152 self.assertFormatEqual(expected, actual)
154 def test_experimental_string_processing_warns(self) -> None:
156 black.mode.Deprecated, black.Mode, experimental_string_processing=True
159 def test_piping(self) -> None:
160 source, expected = read_data("src/black/__init__", data=False)
161 result = BlackRunner().invoke(
166 f"--line-length={black.DEFAULT_LINE_LENGTH}",
167 f"--config={EMPTY_CONFIG}",
169 input=BytesIO(source.encode("utf8")),
171 self.assertEqual(result.exit_code, 0)
172 self.assertFormatEqual(expected, result.output)
173 if source != result.output:
174 black.assert_equivalent(source, result.output)
175 black.assert_stable(source, result.output, DEFAULT_MODE)
177 def test_piping_diff(self) -> None:
178 diff_header = re.compile(
179 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
182 source, _ = read_data("expression.py")
183 expected, _ = read_data("expression.diff")
187 f"--line-length={black.DEFAULT_LINE_LENGTH}",
189 f"--config={EMPTY_CONFIG}",
191 result = BlackRunner().invoke(
192 black.main, args, input=BytesIO(source.encode("utf8"))
194 self.assertEqual(result.exit_code, 0)
195 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
196 actual = actual.rstrip() + "\n" # the diff output has a trailing space
197 self.assertEqual(expected, actual)
199 def test_piping_diff_with_color(self) -> None:
200 source, _ = read_data("expression.py")
204 f"--line-length={black.DEFAULT_LINE_LENGTH}",
207 f"--config={EMPTY_CONFIG}",
209 result = BlackRunner().invoke(
210 black.main, args, input=BytesIO(source.encode("utf8"))
212 actual = result.output
213 # Again, the contents are checked in a different test, so only look for colors.
214 self.assertIn("\033[1m", actual)
215 self.assertIn("\033[36m", actual)
216 self.assertIn("\033[32m", actual)
217 self.assertIn("\033[31m", actual)
218 self.assertIn("\033[0m", actual)
220 @patch("black.dump_to_file", dump_to_stderr)
221 def _test_wip(self) -> None:
222 source, expected = read_data("wip")
223 sys.settrace(tracefunc)
226 experimental_string_processing=False,
227 target_versions={black.TargetVersion.PY38},
229 actual = fs(source, mode=mode)
231 self.assertFormatEqual(expected, actual)
232 black.assert_equivalent(source, actual)
233 black.assert_stable(source, actual, black.FileMode())
235 def test_pep_572_version_detection(self) -> None:
236 source, _ = read_data("pep_572")
237 root = black.lib2to3_parse(source)
238 features = black.get_features_used(root)
239 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
240 versions = black.detect_target_versions(root)
241 self.assertIn(black.TargetVersion.PY38, versions)
243 def test_expression_ff(self) -> None:
244 source, expected = read_data("expression")
245 tmp_file = Path(black.dump_to_file(source))
247 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
248 with open(tmp_file, encoding="utf8") as f:
252 self.assertFormatEqual(expected, actual)
253 with patch("black.dump_to_file", dump_to_stderr):
254 black.assert_equivalent(source, actual)
255 black.assert_stable(source, actual, DEFAULT_MODE)
257 def test_expression_diff(self) -> None:
258 source, _ = read_data("expression.py")
259 expected, _ = read_data("expression.diff")
260 tmp_file = Path(black.dump_to_file(source))
261 diff_header = re.compile(
262 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
263 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
266 result = BlackRunner().invoke(
267 black.main, ["--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
269 self.assertEqual(result.exit_code, 0)
272 actual = result.output
273 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
274 if expected != actual:
275 dump = black.dump_to_file(actual)
277 "Expected diff isn't equal to the actual. If you made changes to"
278 " expression.py and this is an anticipated difference, overwrite"
279 f" tests/data/expression.diff with {dump}"
281 self.assertEqual(expected, actual, msg)
283 def test_expression_diff_with_color(self) -> None:
284 source, _ = read_data("expression.py")
285 expected, _ = read_data("expression.diff")
286 tmp_file = Path(black.dump_to_file(source))
288 result = BlackRunner().invoke(
290 ["--diff", "--color", str(tmp_file), f"--config={EMPTY_CONFIG}"],
294 actual = result.output
295 # We check the contents of the diff in `test_expression_diff`. All
296 # we need to check here is that color codes exist in the result.
297 self.assertIn("\033[1m", actual)
298 self.assertIn("\033[36m", actual)
299 self.assertIn("\033[32m", actual)
300 self.assertIn("\033[31m", actual)
301 self.assertIn("\033[0m", actual)
303 def test_detect_pos_only_arguments(self) -> None:
304 source, _ = read_data("pep_570")
305 root = black.lib2to3_parse(source)
306 features = black.get_features_used(root)
307 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
308 versions = black.detect_target_versions(root)
309 self.assertIn(black.TargetVersion.PY38, versions)
311 @patch("black.dump_to_file", dump_to_stderr)
312 def test_string_quotes(self) -> None:
313 source, expected = read_data("string_quotes")
314 mode = black.Mode(preview=True)
315 assert_format(source, expected, mode)
316 mode = replace(mode, string_normalization=False)
317 not_normalized = fs(source, mode=mode)
318 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
319 black.assert_equivalent(source, not_normalized)
320 black.assert_stable(source, not_normalized, mode=mode)
322 def test_skip_magic_trailing_comma(self) -> None:
323 source, _ = read_data("expression.py")
324 expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
325 tmp_file = Path(black.dump_to_file(source))
326 diff_header = re.compile(
327 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
328 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
331 result = BlackRunner().invoke(
332 black.main, ["-C", "--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
334 self.assertEqual(result.exit_code, 0)
337 actual = result.output
338 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
339 actual = actual.rstrip() + "\n" # the diff output has a trailing space
340 if expected != actual:
341 dump = black.dump_to_file(actual)
343 "Expected diff isn't equal to the actual. If you made changes to"
344 " expression.py and this is an anticipated difference, overwrite"
345 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
347 self.assertEqual(expected, actual, msg)
349 @patch("black.dump_to_file", dump_to_stderr)
350 def test_async_as_identifier(self) -> None:
351 source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
352 source, expected = read_data("async_as_identifier")
354 self.assertFormatEqual(expected, actual)
355 major, minor = sys.version_info[:2]
356 if major < 3 or (major <= 3 and minor < 7):
357 black.assert_equivalent(source, actual)
358 black.assert_stable(source, actual, DEFAULT_MODE)
359 # ensure black can parse this when the target is 3.6
360 self.invokeBlack([str(source_path), "--target-version", "py36"])
361 # but not on 3.7, because async/await is no longer an identifier
362 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
364 @patch("black.dump_to_file", dump_to_stderr)
365 def test_python37(self) -> None:
366 source_path = (THIS_DIR / "data" / "python37.py").resolve()
367 source, expected = read_data("python37")
369 self.assertFormatEqual(expected, actual)
370 major, minor = sys.version_info[:2]
371 if major > 3 or (major == 3 and minor >= 7):
372 black.assert_equivalent(source, actual)
373 black.assert_stable(source, actual, DEFAULT_MODE)
374 # ensure black can parse this when the target is 3.7
375 self.invokeBlack([str(source_path), "--target-version", "py37"])
376 # but not on 3.6, because we use async as a reserved keyword
377 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
379 def test_tab_comment_indentation(self) -> None:
380 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\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 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
386 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
387 self.assertFormatEqual(contents_spc, fs(contents_spc))
388 self.assertFormatEqual(contents_spc, fs(contents_tab))
390 # mixed tabs and spaces (valid Python 2 code)
391 contents_tab = "if 1:\n if 2:\n\t\tpass\n\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 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
397 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
398 self.assertFormatEqual(contents_spc, fs(contents_spc))
399 self.assertFormatEqual(contents_spc, fs(contents_tab))
401 def test_report_verbose(self) -> None:
402 report = Report(verbose=True)
406 def out(msg: str, **kwargs: Any) -> None:
407 out_lines.append(msg)
409 def err(msg: str, **kwargs: Any) -> None:
410 err_lines.append(msg)
412 with patch("black.output._out", out), patch("black.output._err", err):
413 report.done(Path("f1"), black.Changed.NO)
414 self.assertEqual(len(out_lines), 1)
415 self.assertEqual(len(err_lines), 0)
416 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
417 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
418 self.assertEqual(report.return_code, 0)
419 report.done(Path("f2"), black.Changed.YES)
420 self.assertEqual(len(out_lines), 2)
421 self.assertEqual(len(err_lines), 0)
422 self.assertEqual(out_lines[-1], "reformatted f2")
424 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
426 report.done(Path("f3"), black.Changed.CACHED)
427 self.assertEqual(len(out_lines), 3)
428 self.assertEqual(len(err_lines), 0)
430 out_lines[-1], "f3 wasn't modified on disk since last run."
433 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
435 self.assertEqual(report.return_code, 0)
437 self.assertEqual(report.return_code, 1)
439 report.failed(Path("e1"), "boom")
440 self.assertEqual(len(out_lines), 3)
441 self.assertEqual(len(err_lines), 1)
442 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
444 unstyle(str(report)),
445 "1 file reformatted, 2 files left unchanged, 1 file failed to"
448 self.assertEqual(report.return_code, 123)
449 report.done(Path("f3"), black.Changed.YES)
450 self.assertEqual(len(out_lines), 4)
451 self.assertEqual(len(err_lines), 1)
452 self.assertEqual(out_lines[-1], "reformatted f3")
454 unstyle(str(report)),
455 "2 files reformatted, 2 files left unchanged, 1 file failed to"
458 self.assertEqual(report.return_code, 123)
459 report.failed(Path("e2"), "boom")
460 self.assertEqual(len(out_lines), 4)
461 self.assertEqual(len(err_lines), 2)
462 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
464 unstyle(str(report)),
465 "2 files reformatted, 2 files left unchanged, 2 files failed to"
468 self.assertEqual(report.return_code, 123)
469 report.path_ignored(Path("wat"), "no match")
470 self.assertEqual(len(out_lines), 5)
471 self.assertEqual(len(err_lines), 2)
472 self.assertEqual(out_lines[-1], "wat ignored: no match")
474 unstyle(str(report)),
475 "2 files reformatted, 2 files left unchanged, 2 files failed to"
478 self.assertEqual(report.return_code, 123)
479 report.done(Path("f4"), black.Changed.NO)
480 self.assertEqual(len(out_lines), 6)
481 self.assertEqual(len(err_lines), 2)
482 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
484 unstyle(str(report)),
485 "2 files reformatted, 3 files left unchanged, 2 files failed to"
488 self.assertEqual(report.return_code, 123)
491 unstyle(str(report)),
492 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
493 " would fail to reformat.",
498 unstyle(str(report)),
499 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
500 " would fail to reformat.",
503 def test_report_quiet(self) -> None:
504 report = Report(quiet=True)
508 def out(msg: str, **kwargs: Any) -> None:
509 out_lines.append(msg)
511 def err(msg: str, **kwargs: Any) -> None:
512 err_lines.append(msg)
514 with patch("black.output._out", out), patch("black.output._err", err):
515 report.done(Path("f1"), black.Changed.NO)
516 self.assertEqual(len(out_lines), 0)
517 self.assertEqual(len(err_lines), 0)
518 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
519 self.assertEqual(report.return_code, 0)
520 report.done(Path("f2"), black.Changed.YES)
521 self.assertEqual(len(out_lines), 0)
522 self.assertEqual(len(err_lines), 0)
524 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
526 report.done(Path("f3"), black.Changed.CACHED)
527 self.assertEqual(len(out_lines), 0)
528 self.assertEqual(len(err_lines), 0)
530 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
532 self.assertEqual(report.return_code, 0)
534 self.assertEqual(report.return_code, 1)
536 report.failed(Path("e1"), "boom")
537 self.assertEqual(len(out_lines), 0)
538 self.assertEqual(len(err_lines), 1)
539 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
541 unstyle(str(report)),
542 "1 file reformatted, 2 files left unchanged, 1 file failed to"
545 self.assertEqual(report.return_code, 123)
546 report.done(Path("f3"), black.Changed.YES)
547 self.assertEqual(len(out_lines), 0)
548 self.assertEqual(len(err_lines), 1)
550 unstyle(str(report)),
551 "2 files reformatted, 2 files left unchanged, 1 file failed to"
554 self.assertEqual(report.return_code, 123)
555 report.failed(Path("e2"), "boom")
556 self.assertEqual(len(out_lines), 0)
557 self.assertEqual(len(err_lines), 2)
558 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
560 unstyle(str(report)),
561 "2 files reformatted, 2 files left unchanged, 2 files failed to"
564 self.assertEqual(report.return_code, 123)
565 report.path_ignored(Path("wat"), "no match")
566 self.assertEqual(len(out_lines), 0)
567 self.assertEqual(len(err_lines), 2)
569 unstyle(str(report)),
570 "2 files reformatted, 2 files left unchanged, 2 files failed to"
573 self.assertEqual(report.return_code, 123)
574 report.done(Path("f4"), black.Changed.NO)
575 self.assertEqual(len(out_lines), 0)
576 self.assertEqual(len(err_lines), 2)
578 unstyle(str(report)),
579 "2 files reformatted, 3 files left unchanged, 2 files failed to"
582 self.assertEqual(report.return_code, 123)
585 unstyle(str(report)),
586 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
587 " would fail to reformat.",
592 unstyle(str(report)),
593 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
594 " would fail to reformat.",
597 def test_report_normal(self) -> None:
598 report = black.Report()
602 def out(msg: str, **kwargs: Any) -> None:
603 out_lines.append(msg)
605 def err(msg: str, **kwargs: Any) -> None:
606 err_lines.append(msg)
608 with patch("black.output._out", out), patch("black.output._err", err):
609 report.done(Path("f1"), black.Changed.NO)
610 self.assertEqual(len(out_lines), 0)
611 self.assertEqual(len(err_lines), 0)
612 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
613 self.assertEqual(report.return_code, 0)
614 report.done(Path("f2"), black.Changed.YES)
615 self.assertEqual(len(out_lines), 1)
616 self.assertEqual(len(err_lines), 0)
617 self.assertEqual(out_lines[-1], "reformatted f2")
619 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
621 report.done(Path("f3"), black.Changed.CACHED)
622 self.assertEqual(len(out_lines), 1)
623 self.assertEqual(len(err_lines), 0)
624 self.assertEqual(out_lines[-1], "reformatted f2")
626 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
628 self.assertEqual(report.return_code, 0)
630 self.assertEqual(report.return_code, 1)
632 report.failed(Path("e1"), "boom")
633 self.assertEqual(len(out_lines), 1)
634 self.assertEqual(len(err_lines), 1)
635 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
637 unstyle(str(report)),
638 "1 file reformatted, 2 files left unchanged, 1 file failed to"
641 self.assertEqual(report.return_code, 123)
642 report.done(Path("f3"), black.Changed.YES)
643 self.assertEqual(len(out_lines), 2)
644 self.assertEqual(len(err_lines), 1)
645 self.assertEqual(out_lines[-1], "reformatted f3")
647 unstyle(str(report)),
648 "2 files reformatted, 2 files left unchanged, 1 file failed to"
651 self.assertEqual(report.return_code, 123)
652 report.failed(Path("e2"), "boom")
653 self.assertEqual(len(out_lines), 2)
654 self.assertEqual(len(err_lines), 2)
655 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
657 unstyle(str(report)),
658 "2 files reformatted, 2 files left unchanged, 2 files failed to"
661 self.assertEqual(report.return_code, 123)
662 report.path_ignored(Path("wat"), "no match")
663 self.assertEqual(len(out_lines), 2)
664 self.assertEqual(len(err_lines), 2)
666 unstyle(str(report)),
667 "2 files reformatted, 2 files left unchanged, 2 files failed to"
670 self.assertEqual(report.return_code, 123)
671 report.done(Path("f4"), black.Changed.NO)
672 self.assertEqual(len(out_lines), 2)
673 self.assertEqual(len(err_lines), 2)
675 unstyle(str(report)),
676 "2 files reformatted, 3 files left unchanged, 2 files failed to"
679 self.assertEqual(report.return_code, 123)
682 unstyle(str(report)),
683 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
684 " would fail to reformat.",
689 unstyle(str(report)),
690 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
691 " would fail to reformat.",
694 def test_lib2to3_parse(self) -> None:
695 with self.assertRaises(black.InvalidInput):
696 black.lib2to3_parse("invalid syntax")
699 black.lib2to3_parse(straddling)
700 black.lib2to3_parse(straddling, {TargetVersion.PY36})
703 with self.assertRaises(black.InvalidInput):
704 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
706 py3_only = "exec(x, end=y)"
707 black.lib2to3_parse(py3_only)
708 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
710 def test_get_features_used_decorator(self) -> None:
711 # Test the feature detection of new decorator syntax
712 # since this makes some test cases of test_get_features_used()
713 # fails if it fails, this is tested first so that a useful case
715 simples, relaxed = read_data("decorators")
716 # skip explanation comments at the top of the file
717 for simple_test in simples.split("##")[1:]:
718 node = black.lib2to3_parse(simple_test)
719 decorator = str(node.children[0].children[0]).strip()
721 Feature.RELAXED_DECORATORS,
722 black.get_features_used(node),
724 f"decorator '{decorator}' follows python<=3.8 syntax"
725 "but is detected as 3.9+"
726 # f"The full node is\n{node!r}"
729 # skip the '# output' comment at the top of the output part
730 for relaxed_test in relaxed.split("##")[1:]:
731 node = black.lib2to3_parse(relaxed_test)
732 decorator = str(node.children[0].children[0]).strip()
734 Feature.RELAXED_DECORATORS,
735 black.get_features_used(node),
737 f"decorator '{decorator}' uses python3.9+ syntax"
738 "but is detected as python<=3.8"
739 # f"The full node is\n{node!r}"
743 def test_get_features_used(self) -> None:
744 node = black.lib2to3_parse("def f(*, arg): ...\n")
745 self.assertEqual(black.get_features_used(node), set())
746 node = black.lib2to3_parse("def f(*, arg,): ...\n")
747 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
748 node = black.lib2to3_parse("f(*arg,)\n")
750 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
752 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
753 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
754 node = black.lib2to3_parse("123_456\n")
755 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
756 node = black.lib2to3_parse("123456\n")
757 self.assertEqual(black.get_features_used(node), set())
758 source, expected = read_data("function")
759 node = black.lib2to3_parse(source)
760 expected_features = {
761 Feature.TRAILING_COMMA_IN_CALL,
762 Feature.TRAILING_COMMA_IN_DEF,
765 self.assertEqual(black.get_features_used(node), expected_features)
766 node = black.lib2to3_parse(expected)
767 self.assertEqual(black.get_features_used(node), expected_features)
768 source, expected = read_data("expression")
769 node = black.lib2to3_parse(source)
770 self.assertEqual(black.get_features_used(node), set())
771 node = black.lib2to3_parse(expected)
772 self.assertEqual(black.get_features_used(node), set())
773 node = black.lib2to3_parse("lambda a, /, b: ...")
774 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
775 node = black.lib2to3_parse("def fn(a, /, b): ...")
776 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
777 node = black.lib2to3_parse("def fn(): yield a, b")
778 self.assertEqual(black.get_features_used(node), set())
779 node = black.lib2to3_parse("def fn(): return a, b")
780 self.assertEqual(black.get_features_used(node), set())
781 node = black.lib2to3_parse("def fn(): yield *b, c")
782 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
783 node = black.lib2to3_parse("def fn(): return a, *b, c")
784 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
785 node = black.lib2to3_parse("x = a, *b, c")
786 self.assertEqual(black.get_features_used(node), set())
787 node = black.lib2to3_parse("x: Any = regular")
788 self.assertEqual(black.get_features_used(node), set())
789 node = black.lib2to3_parse("x: Any = (regular, regular)")
790 self.assertEqual(black.get_features_used(node), set())
791 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
792 self.assertEqual(black.get_features_used(node), set())
793 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
795 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
798 def test_get_features_used_for_future_flags(self) -> None:
799 for src, features in [
800 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
802 "from __future__ import (other, annotations)",
803 {Feature.FUTURE_ANNOTATIONS},
805 ("a = 1 + 2\nfrom something import annotations", set()),
806 ("from __future__ import x, y", set()),
808 with self.subTest(src=src, features=features):
809 node = black.lib2to3_parse(src)
810 future_imports = black.get_future_imports(node)
812 black.get_features_used(node, future_imports=future_imports),
816 def test_get_future_imports(self) -> None:
817 node = black.lib2to3_parse("\n")
818 self.assertEqual(set(), black.get_future_imports(node))
819 node = black.lib2to3_parse("from __future__ import black\n")
820 self.assertEqual({"black"}, black.get_future_imports(node))
821 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
822 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
823 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
824 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
825 node = black.lib2to3_parse(
826 "from __future__ import multiple\nfrom __future__ import imports\n"
828 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
829 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
830 self.assertEqual({"black"}, black.get_future_imports(node))
831 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
832 self.assertEqual({"black"}, black.get_future_imports(node))
833 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
834 self.assertEqual(set(), black.get_future_imports(node))
835 node = black.lib2to3_parse("from some.module import black\n")
836 self.assertEqual(set(), black.get_future_imports(node))
837 node = black.lib2to3_parse(
838 "from __future__ import unicode_literals as _unicode_literals"
840 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
841 node = black.lib2to3_parse(
842 "from __future__ import unicode_literals as _lol, print"
844 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
846 @pytest.mark.incompatible_with_mypyc
847 def test_debug_visitor(self) -> None:
848 source, _ = read_data("debug_visitor.py")
849 expected, _ = read_data("debug_visitor.out")
853 def out(msg: str, **kwargs: Any) -> None:
854 out_lines.append(msg)
856 def err(msg: str, **kwargs: Any) -> None:
857 err_lines.append(msg)
859 with patch("black.debug.out", out):
860 DebugVisitor.show(source)
861 actual = "\n".join(out_lines) + "\n"
863 if expected != actual:
864 log_name = black.dump_to_file(*out_lines)
868 f"AST print out is different. Actual version dumped to {log_name}",
871 def test_format_file_contents(self) -> None:
874 with self.assertRaises(black.NothingChanged):
875 black.format_file_contents(empty, mode=mode, fast=False)
877 with self.assertRaises(black.NothingChanged):
878 black.format_file_contents(just_nl, mode=mode, fast=False)
879 same = "j = [1, 2, 3]\n"
880 with self.assertRaises(black.NothingChanged):
881 black.format_file_contents(same, mode=mode, fast=False)
882 different = "j = [1,2,3]"
884 actual = black.format_file_contents(different, mode=mode, fast=False)
885 self.assertEqual(expected, actual)
886 invalid = "return if you can"
887 with self.assertRaises(black.InvalidInput) as e:
888 black.format_file_contents(invalid, mode=mode, fast=False)
889 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
891 def test_endmarker(self) -> None:
892 n = black.lib2to3_parse("\n")
893 self.assertEqual(n.type, black.syms.file_input)
894 self.assertEqual(len(n.children), 1)
895 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
897 @pytest.mark.incompatible_with_mypyc
898 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
899 def test_assertFormatEqual(self) -> None:
903 def out(msg: str, **kwargs: Any) -> None:
904 out_lines.append(msg)
906 def err(msg: str, **kwargs: Any) -> None:
907 err_lines.append(msg)
909 with patch("black.output._out", out), patch("black.output._err", err):
910 with self.assertRaises(AssertionError):
911 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
913 out_str = "".join(out_lines)
914 self.assertIn("Expected tree:", out_str)
915 self.assertIn("Actual tree:", out_str)
916 self.assertEqual("".join(err_lines), "")
919 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
920 def test_works_in_mono_process_only_environment(self) -> None:
921 with cache_dir() as workspace:
923 (workspace / "one.py").resolve(),
924 (workspace / "two.py").resolve(),
926 f.write_text('print("hello")\n')
927 self.invokeBlack([str(workspace)])
930 def test_check_diff_use_together(self) -> None:
932 # Files which will be reformatted.
933 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
934 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
935 # Files which will not be reformatted.
936 src2 = (THIS_DIR / "data" / "composition.py").resolve()
937 self.invokeBlack([str(src2), "--diff", "--check"])
938 # Multi file command.
939 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
941 def test_no_src_fails(self) -> None:
943 self.invokeBlack([], exit_code=1)
945 def test_src_and_code_fails(self) -> None:
947 self.invokeBlack([".", "-c", "0"], exit_code=1)
949 def test_broken_symlink(self) -> None:
950 with cache_dir() as workspace:
951 symlink = workspace / "broken_link.py"
953 symlink.symlink_to("nonexistent.py")
954 except (OSError, NotImplementedError) as e:
955 self.skipTest(f"Can't create symlinks: {e}")
956 self.invokeBlack([str(workspace.resolve())])
958 def test_single_file_force_pyi(self) -> None:
959 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
960 contents, expected = read_data("force_pyi")
961 with cache_dir() as workspace:
962 path = (workspace / "file.py").resolve()
963 with open(path, "w") as fh:
965 self.invokeBlack([str(path), "--pyi"])
966 with open(path, "r") as fh:
968 # verify cache with --pyi is separate
969 pyi_cache = black.read_cache(pyi_mode)
970 self.assertIn(str(path), pyi_cache)
971 normal_cache = black.read_cache(DEFAULT_MODE)
972 self.assertNotIn(str(path), normal_cache)
973 self.assertFormatEqual(expected, actual)
974 black.assert_equivalent(contents, actual)
975 black.assert_stable(contents, actual, pyi_mode)
978 def test_multi_file_force_pyi(self) -> None:
979 reg_mode = DEFAULT_MODE
980 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
981 contents, expected = read_data("force_pyi")
982 with cache_dir() as workspace:
984 (workspace / "file1.py").resolve(),
985 (workspace / "file2.py").resolve(),
988 with open(path, "w") as fh:
990 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
992 with open(path, "r") as fh:
994 self.assertEqual(actual, expected)
995 # verify cache with --pyi is separate
996 pyi_cache = black.read_cache(pyi_mode)
997 normal_cache = black.read_cache(reg_mode)
999 self.assertIn(str(path), pyi_cache)
1000 self.assertNotIn(str(path), normal_cache)
1002 def test_pipe_force_pyi(self) -> None:
1003 source, expected = read_data("force_pyi")
1004 result = CliRunner().invoke(
1005 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1007 self.assertEqual(result.exit_code, 0)
1008 actual = result.output
1009 self.assertFormatEqual(actual, expected)
1011 def test_single_file_force_py36(self) -> None:
1012 reg_mode = DEFAULT_MODE
1013 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1014 source, expected = read_data("force_py36")
1015 with cache_dir() as workspace:
1016 path = (workspace / "file.py").resolve()
1017 with open(path, "w") as fh:
1019 self.invokeBlack([str(path), *PY36_ARGS])
1020 with open(path, "r") as fh:
1022 # verify cache with --target-version is separate
1023 py36_cache = black.read_cache(py36_mode)
1024 self.assertIn(str(path), py36_cache)
1025 normal_cache = black.read_cache(reg_mode)
1026 self.assertNotIn(str(path), normal_cache)
1027 self.assertEqual(actual, expected)
1030 def test_multi_file_force_py36(self) -> None:
1031 reg_mode = DEFAULT_MODE
1032 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1033 source, expected = read_data("force_py36")
1034 with cache_dir() as workspace:
1036 (workspace / "file1.py").resolve(),
1037 (workspace / "file2.py").resolve(),
1040 with open(path, "w") as fh:
1042 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1044 with open(path, "r") as fh:
1046 self.assertEqual(actual, expected)
1047 # verify cache with --target-version is separate
1048 pyi_cache = black.read_cache(py36_mode)
1049 normal_cache = black.read_cache(reg_mode)
1051 self.assertIn(str(path), pyi_cache)
1052 self.assertNotIn(str(path), normal_cache)
1054 def test_pipe_force_py36(self) -> None:
1055 source, expected = read_data("force_py36")
1056 result = CliRunner().invoke(
1058 ["-", "-q", "--target-version=py36"],
1059 input=BytesIO(source.encode("utf8")),
1061 self.assertEqual(result.exit_code, 0)
1062 actual = result.output
1063 self.assertFormatEqual(actual, expected)
1065 @pytest.mark.incompatible_with_mypyc
1066 def test_reformat_one_with_stdin(self) -> None:
1068 "black.format_stdin_to_stdout",
1069 return_value=lambda *args, **kwargs: black.Changed.YES,
1071 report = MagicMock()
1076 write_back=black.WriteBack.YES,
1080 fsts.assert_called_once()
1081 report.done.assert_called_with(path, black.Changed.YES)
1083 @pytest.mark.incompatible_with_mypyc
1084 def test_reformat_one_with_stdin_filename(self) -> None:
1086 "black.format_stdin_to_stdout",
1087 return_value=lambda *args, **kwargs: black.Changed.YES,
1089 report = MagicMock()
1091 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1096 write_back=black.WriteBack.YES,
1100 fsts.assert_called_once_with(
1101 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1103 # __BLACK_STDIN_FILENAME__ should have been stripped
1104 report.done.assert_called_with(expected, black.Changed.YES)
1106 @pytest.mark.incompatible_with_mypyc
1107 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1109 "black.format_stdin_to_stdout",
1110 return_value=lambda *args, **kwargs: black.Changed.YES,
1112 report = MagicMock()
1114 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1119 write_back=black.WriteBack.YES,
1123 fsts.assert_called_once_with(
1125 write_back=black.WriteBack.YES,
1126 mode=replace(DEFAULT_MODE, is_pyi=True),
1128 # __BLACK_STDIN_FILENAME__ should have been stripped
1129 report.done.assert_called_with(expected, black.Changed.YES)
1131 @pytest.mark.incompatible_with_mypyc
1132 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1134 "black.format_stdin_to_stdout",
1135 return_value=lambda *args, **kwargs: black.Changed.YES,
1137 report = MagicMock()
1139 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1144 write_back=black.WriteBack.YES,
1148 fsts.assert_called_once_with(
1150 write_back=black.WriteBack.YES,
1151 mode=replace(DEFAULT_MODE, is_ipynb=True),
1153 # __BLACK_STDIN_FILENAME__ should have been stripped
1154 report.done.assert_called_with(expected, black.Changed.YES)
1156 @pytest.mark.incompatible_with_mypyc
1157 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1159 "black.format_stdin_to_stdout",
1160 return_value=lambda *args, **kwargs: black.Changed.YES,
1162 report = MagicMock()
1163 # Even with an existing file, since we are forcing stdin, black
1164 # should output to stdout and not modify the file inplace
1165 p = Path(str(THIS_DIR / "data/collections.py"))
1166 # Make sure is_file actually returns True
1167 self.assertTrue(p.is_file())
1168 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1173 write_back=black.WriteBack.YES,
1177 fsts.assert_called_once()
1178 # __BLACK_STDIN_FILENAME__ should have been stripped
1179 report.done.assert_called_with(expected, black.Changed.YES)
1181 def test_reformat_one_with_stdin_empty(self) -> None:
1182 output = io.StringIO()
1183 with patch("io.TextIOWrapper", lambda *args, **kwargs: output):
1185 black.format_stdin_to_stdout(
1188 write_back=black.WriteBack.YES,
1191 except io.UnsupportedOperation:
1192 pass # StringIO does not support detach
1193 assert output.getvalue() == ""
1195 def test_invalid_cli_regex(self) -> None:
1196 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1197 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1199 def test_required_version_matches_version(self) -> None:
1201 ["--required-version", black.__version__, "-c", "0"],
1206 def test_required_version_matches_partial_version(self) -> None:
1208 ["--required-version", black.__version__.split(".")[0], "-c", "0"],
1213 def test_required_version_does_not_match_on_minor_version(self) -> None:
1215 ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
1220 def test_required_version_does_not_match_version(self) -> None:
1221 result = BlackRunner().invoke(
1223 ["--required-version", "20.99b", "-c", "0"],
1225 self.assertEqual(result.exit_code, 1)
1226 self.assertIn("required version", result.stderr)
1228 def test_preserves_line_endings(self) -> None:
1229 with TemporaryDirectory() as workspace:
1230 test_file = Path(workspace) / "test.py"
1231 for nl in ["\n", "\r\n"]:
1232 contents = nl.join(["def f( ):", " pass"])
1233 test_file.write_bytes(contents.encode())
1234 ff(test_file, write_back=black.WriteBack.YES)
1235 updated_contents: bytes = test_file.read_bytes()
1236 self.assertIn(nl.encode(), updated_contents)
1238 self.assertNotIn(b"\r\n", updated_contents)
1240 def test_preserves_line_endings_via_stdin(self) -> None:
1241 for nl in ["\n", "\r\n"]:
1242 contents = nl.join(["def f( ):", " pass"])
1243 runner = BlackRunner()
1244 result = runner.invoke(
1245 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1247 self.assertEqual(result.exit_code, 0)
1248 output = result.stdout_bytes
1249 self.assertIn(nl.encode("utf8"), output)
1251 self.assertNotIn(b"\r\n", output)
1253 def test_assert_equivalent_different_asts(self) -> None:
1254 with self.assertRaises(AssertionError):
1255 black.assert_equivalent("{}", "None")
1257 def test_shhh_click(self) -> None:
1259 from click import _unicodefun
1260 except ModuleNotFoundError:
1261 self.skipTest("Incompatible Click version")
1262 if not hasattr(_unicodefun, "_verify_python3_env"):
1263 self.skipTest("Incompatible Click version")
1264 # First, let's see if Click is crashing with a preferred ASCII charset.
1265 with patch("locale.getpreferredencoding") as gpe:
1266 gpe.return_value = "ASCII"
1267 with self.assertRaises(RuntimeError):
1268 _unicodefun._verify_python3_env() # type: ignore
1269 # Now, let's silence Click...
1271 # ...and confirm it's silent.
1272 with patch("locale.getpreferredencoding") as gpe:
1273 gpe.return_value = "ASCII"
1275 _unicodefun._verify_python3_env() # type: ignore
1276 except RuntimeError as re:
1277 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1279 def test_root_logger_not_used_directly(self) -> None:
1280 def fail(*args: Any, **kwargs: Any) -> None:
1281 self.fail("Record created with root logger")
1283 with patch.multiple(
1292 ff(THIS_DIR / "util.py")
1294 def test_invalid_config_return_code(self) -> None:
1295 tmp_file = Path(black.dump_to_file())
1297 tmp_config = Path(black.dump_to_file())
1299 args = ["--config", str(tmp_config), str(tmp_file)]
1300 self.invokeBlack(args, exit_code=2, ignore_config=False)
1304 def test_parse_pyproject_toml(self) -> None:
1305 test_toml_file = THIS_DIR / "test.toml"
1306 config = black.parse_pyproject_toml(str(test_toml_file))
1307 self.assertEqual(config["verbose"], 1)
1308 self.assertEqual(config["check"], "no")
1309 self.assertEqual(config["diff"], "y")
1310 self.assertEqual(config["color"], True)
1311 self.assertEqual(config["line_length"], 79)
1312 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1313 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1314 self.assertEqual(config["exclude"], r"\.pyi?$")
1315 self.assertEqual(config["include"], r"\.py?$")
1317 def test_read_pyproject_toml(self) -> None:
1318 test_toml_file = THIS_DIR / "test.toml"
1319 fake_ctx = FakeContext()
1320 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1321 config = fake_ctx.default_map
1322 self.assertEqual(config["verbose"], "1")
1323 self.assertEqual(config["check"], "no")
1324 self.assertEqual(config["diff"], "y")
1325 self.assertEqual(config["color"], "True")
1326 self.assertEqual(config["line_length"], "79")
1327 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1328 self.assertEqual(config["exclude"], r"\.pyi?$")
1329 self.assertEqual(config["include"], r"\.py?$")
1331 @pytest.mark.incompatible_with_mypyc
1332 def test_find_project_root(self) -> None:
1333 with TemporaryDirectory() as workspace:
1334 root = Path(workspace)
1335 test_dir = root / "test"
1338 src_dir = root / "src"
1341 root_pyproject = root / "pyproject.toml"
1342 root_pyproject.touch()
1343 src_pyproject = src_dir / "pyproject.toml"
1344 src_pyproject.touch()
1345 src_python = src_dir / "foo.py"
1349 black.find_project_root((src_dir, test_dir)),
1350 (root.resolve(), "pyproject.toml"),
1353 black.find_project_root((src_dir,)),
1354 (src_dir.resolve(), "pyproject.toml"),
1357 black.find_project_root((src_python,)),
1358 (src_dir.resolve(), "pyproject.toml"),
1362 "black.files.find_user_pyproject_toml",
1363 black.files.find_user_pyproject_toml.__wrapped__,
1365 def test_find_user_pyproject_toml_linux(self) -> None:
1366 if system() == "Windows":
1369 # Test if XDG_CONFIG_HOME is checked
1370 with TemporaryDirectory() as workspace:
1371 tmp_user_config = Path(workspace) / "black"
1372 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1374 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1377 # Test fallback for XDG_CONFIG_HOME
1378 with patch.dict("os.environ"):
1379 os.environ.pop("XDG_CONFIG_HOME", None)
1380 fallback_user_config = Path("~/.config").expanduser() / "black"
1382 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1385 def test_find_user_pyproject_toml_windows(self) -> None:
1386 if system() != "Windows":
1389 user_config_path = Path.home() / ".black"
1391 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1394 def test_bpo_33660_workaround(self) -> None:
1395 if system() == "Windows":
1398 # https://bugs.python.org/issue33660
1400 with change_directory(root):
1401 path = Path("workspace") / "project"
1402 report = black.Report(verbose=True)
1403 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1404 self.assertEqual(normalized_path, "workspace/project")
1406 def test_newline_comment_interaction(self) -> None:
1407 source = "class A:\\\r\n# type: ignore\n pass\n"
1408 output = black.format_str(source, mode=DEFAULT_MODE)
1409 black.assert_stable(source, output, mode=DEFAULT_MODE)
1411 def test_bpo_2142_workaround(self) -> None:
1413 # https://bugs.python.org/issue2142
1415 source, _ = read_data("missing_final_newline.py")
1416 # read_data adds a trailing newline
1417 source = source.rstrip()
1418 expected, _ = read_data("missing_final_newline.diff")
1419 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1420 diff_header = re.compile(
1421 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1422 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1425 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1426 self.assertEqual(result.exit_code, 0)
1429 actual = result.output
1430 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1431 self.assertEqual(actual, expected)
1434 def compare_results(
1435 result: click.testing.Result, expected_value: str, expected_exit_code: int
1437 """Helper method to test the value and exit code of a click Result."""
1439 result.output == expected_value
1440 ), "The output did not match the expected value."
1441 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1443 def test_code_option(self) -> None:
1444 """Test the code option with no changes."""
1445 code = 'print("Hello world")\n'
1446 args = ["--code", code]
1447 result = CliRunner().invoke(black.main, args)
1449 self.compare_results(result, code, 0)
1451 def test_code_option_changed(self) -> None:
1452 """Test the code option when changes are required."""
1453 code = "print('hello world')"
1454 formatted = black.format_str(code, mode=DEFAULT_MODE)
1456 args = ["--code", code]
1457 result = CliRunner().invoke(black.main, args)
1459 self.compare_results(result, formatted, 0)
1461 def test_code_option_check(self) -> None:
1462 """Test the code option when check is passed."""
1463 args = ["--check", "--code", 'print("Hello world")\n']
1464 result = CliRunner().invoke(black.main, args)
1465 self.compare_results(result, "", 0)
1467 def test_code_option_check_changed(self) -> None:
1468 """Test the code option when changes are required, and check is passed."""
1469 args = ["--check", "--code", "print('hello world')"]
1470 result = CliRunner().invoke(black.main, args)
1471 self.compare_results(result, "", 1)
1473 def test_code_option_diff(self) -> None:
1474 """Test the code option when diff is passed."""
1475 code = "print('hello world')"
1476 formatted = black.format_str(code, mode=DEFAULT_MODE)
1477 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1479 args = ["--diff", "--code", code]
1480 result = CliRunner().invoke(black.main, args)
1482 # Remove time from diff
1483 output = DIFF_TIME.sub("", result.output)
1485 assert output == result_diff, "The output did not match the expected value."
1486 assert result.exit_code == 0, "The exit code is incorrect."
1488 def test_code_option_color_diff(self) -> None:
1489 """Test the code option when color and diff are passed."""
1490 code = "print('hello world')"
1491 formatted = black.format_str(code, mode=DEFAULT_MODE)
1493 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1494 result_diff = color_diff(result_diff)
1496 args = ["--diff", "--color", "--code", code]
1497 result = CliRunner().invoke(black.main, args)
1499 # Remove time from diff
1500 output = DIFF_TIME.sub("", result.output)
1502 assert output == result_diff, "The output did not match the expected value."
1503 assert result.exit_code == 0, "The exit code is incorrect."
1505 @pytest.mark.incompatible_with_mypyc
1506 def test_code_option_safe(self) -> None:
1507 """Test that the code option throws an error when the sanity checks fail."""
1508 # Patch black.assert_equivalent to ensure the sanity checks fail
1509 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1510 code = 'print("Hello world")'
1511 error_msg = f"{code}\nerror: cannot format <string>: \n"
1513 args = ["--safe", "--code", code]
1514 result = CliRunner().invoke(black.main, args)
1516 self.compare_results(result, error_msg, 123)
1518 def test_code_option_fast(self) -> None:
1519 """Test that the code option ignores errors when the sanity checks fail."""
1520 # Patch black.assert_equivalent to ensure the sanity checks fail
1521 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1522 code = 'print("Hello world")'
1523 formatted = black.format_str(code, mode=DEFAULT_MODE)
1525 args = ["--fast", "--code", code]
1526 result = CliRunner().invoke(black.main, args)
1528 self.compare_results(result, formatted, 0)
1530 @pytest.mark.incompatible_with_mypyc
1531 def test_code_option_config(self) -> None:
1533 Test that the code option finds the pyproject.toml in the current directory.
1535 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1536 args = ["--code", "print"]
1537 # This is the only directory known to contain a pyproject.toml
1538 with change_directory(PROJECT_ROOT):
1539 CliRunner().invoke(black.main, args)
1540 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1543 len(parse.mock_calls) >= 1
1544 ), "Expected config parse to be called with the current directory."
1546 _, call_args, _ = parse.mock_calls[0]
1548 call_args[0].lower() == str(pyproject_path).lower()
1549 ), "Incorrect config loaded."
1551 @pytest.mark.incompatible_with_mypyc
1552 def test_code_option_parent_config(self) -> None:
1554 Test that the code option finds the pyproject.toml in the parent directory.
1556 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1557 with change_directory(THIS_DIR):
1558 args = ["--code", "print"]
1559 CliRunner().invoke(black.main, args)
1561 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1563 len(parse.mock_calls) >= 1
1564 ), "Expected config parse to be called with the current directory."
1566 _, call_args, _ = parse.mock_calls[0]
1568 call_args[0].lower() == str(pyproject_path).lower()
1569 ), "Incorrect config loaded."
1571 def test_for_handled_unexpected_eof_error(self) -> None:
1573 Test that an unexpected EOF SyntaxError is nicely presented.
1575 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1576 black.lib2to3_parse("print(", {})
1578 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1580 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1581 with pytest.raises(AssertionError) as err:
1582 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1585 # Unfortunately the SyntaxError message has changed in newer versions so we
1586 # can't match it directly.
1587 err.match("invalid character")
1588 err.match(r"\(<unknown>, line 1\)")
1592 def test_get_cache_dir(
1595 monkeypatch: pytest.MonkeyPatch,
1597 # Create multiple cache directories
1598 workspace1 = tmp_path / "ws1"
1600 workspace2 = tmp_path / "ws2"
1603 # Force user_cache_dir to use the temporary directory for easier assertions
1604 patch_user_cache_dir = patch(
1605 target="black.cache.user_cache_dir",
1607 return_value=str(workspace1),
1610 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1611 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1612 with patch_user_cache_dir:
1613 assert get_cache_dir() == workspace1
1615 # If it is set, use the path provided in the env var.
1616 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1617 assert get_cache_dir() == workspace2
1619 def test_cache_broken_file(self) -> None:
1621 with cache_dir() as workspace:
1622 cache_file = get_cache_file(mode)
1623 cache_file.write_text("this is not a pickle")
1624 assert black.read_cache(mode) == {}
1625 src = (workspace / "test.py").resolve()
1626 src.write_text("print('hello')")
1627 invokeBlack([str(src)])
1628 cache = black.read_cache(mode)
1629 assert str(src) in cache
1631 def test_cache_single_file_already_cached(self) -> None:
1633 with cache_dir() as workspace:
1634 src = (workspace / "test.py").resolve()
1635 src.write_text("print('hello')")
1636 black.write_cache({}, [src], mode)
1637 invokeBlack([str(src)])
1638 assert src.read_text() == "print('hello')"
1641 def test_cache_multiple_files(self) -> None:
1643 with cache_dir() as workspace, patch(
1644 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1646 one = (workspace / "one.py").resolve()
1647 with one.open("w") as fobj:
1648 fobj.write("print('hello')")
1649 two = (workspace / "two.py").resolve()
1650 with two.open("w") as fobj:
1651 fobj.write("print('hello')")
1652 black.write_cache({}, [one], mode)
1653 invokeBlack([str(workspace)])
1654 with one.open("r") as fobj:
1655 assert fobj.read() == "print('hello')"
1656 with two.open("r") as fobj:
1657 assert fobj.read() == 'print("hello")\n'
1658 cache = black.read_cache(mode)
1659 assert str(one) in cache
1660 assert str(two) in cache
1662 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1663 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1665 with cache_dir() as workspace:
1666 src = (workspace / "test.py").resolve()
1667 with src.open("w") as fobj:
1668 fobj.write("print('hello')")
1669 with patch("black.read_cache") as read_cache, patch(
1672 cmd = [str(src), "--diff"]
1674 cmd.append("--color")
1676 cache_file = get_cache_file(mode)
1677 assert cache_file.exists() is False
1678 write_cache.assert_not_called()
1679 read_cache.assert_not_called()
1681 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1683 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1684 with cache_dir() as workspace:
1685 for tag in range(0, 4):
1686 src = (workspace / f"test{tag}.py").resolve()
1687 with src.open("w") as fobj:
1688 fobj.write("print('hello')")
1689 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1690 cmd = ["--diff", str(workspace)]
1692 cmd.append("--color")
1693 invokeBlack(cmd, exit_code=0)
1694 # this isn't quite doing what we want, but if it _isn't_
1695 # called then we cannot be using the lock it provides
1698 def test_no_cache_when_stdin(self) -> None:
1701 result = CliRunner().invoke(
1702 black.main, ["-"], input=BytesIO(b"print('hello')")
1704 assert not result.exit_code
1705 cache_file = get_cache_file(mode)
1706 assert not cache_file.exists()
1708 def test_read_cache_no_cachefile(self) -> None:
1711 assert black.read_cache(mode) == {}
1713 def test_write_cache_read_cache(self) -> None:
1715 with cache_dir() as workspace:
1716 src = (workspace / "test.py").resolve()
1718 black.write_cache({}, [src], mode)
1719 cache = black.read_cache(mode)
1720 assert str(src) in cache
1721 assert cache[str(src)] == black.get_cache_info(src)
1723 def test_filter_cached(self) -> None:
1724 with TemporaryDirectory() as workspace:
1725 path = Path(workspace)
1726 uncached = (path / "uncached").resolve()
1727 cached = (path / "cached").resolve()
1728 cached_but_changed = (path / "changed").resolve()
1731 cached_but_changed.touch()
1733 str(cached): black.get_cache_info(cached),
1734 str(cached_but_changed): (0.0, 0),
1736 todo, done = black.filter_cached(
1737 cache, {uncached, cached, cached_but_changed}
1739 assert todo == {uncached, cached_but_changed}
1740 assert done == {cached}
1742 def test_write_cache_creates_directory_if_needed(self) -> None:
1744 with cache_dir(exists=False) as workspace:
1745 assert not workspace.exists()
1746 black.write_cache({}, [], mode)
1747 assert workspace.exists()
1750 def test_failed_formatting_does_not_get_cached(self) -> None:
1752 with cache_dir() as workspace, patch(
1753 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1755 failing = (workspace / "failing.py").resolve()
1756 with failing.open("w") as fobj:
1757 fobj.write("not actually python")
1758 clean = (workspace / "clean.py").resolve()
1759 with clean.open("w") as fobj:
1760 fobj.write('print("hello")\n')
1761 invokeBlack([str(workspace)], exit_code=123)
1762 cache = black.read_cache(mode)
1763 assert str(failing) not in cache
1764 assert str(clean) in cache
1766 def test_write_cache_write_fail(self) -> None:
1768 with cache_dir(), patch.object(Path, "open") as mock:
1769 mock.side_effect = OSError
1770 black.write_cache({}, [], mode)
1772 def test_read_cache_line_lengths(self) -> None:
1774 short_mode = replace(DEFAULT_MODE, line_length=1)
1775 with cache_dir() as workspace:
1776 path = (workspace / "file.py").resolve()
1778 black.write_cache({}, [path], mode)
1779 one = black.read_cache(mode)
1780 assert str(path) in one
1781 two = black.read_cache(short_mode)
1782 assert str(path) not in two
1785 def assert_collected_sources(
1786 src: Sequence[Union[str, Path]],
1787 expected: Sequence[Union[str, Path]],
1789 ctx: Optional[FakeContext] = None,
1790 exclude: Optional[str] = None,
1791 include: Optional[str] = None,
1792 extend_exclude: Optional[str] = None,
1793 force_exclude: Optional[str] = None,
1794 stdin_filename: Optional[str] = None,
1796 gs_src = tuple(str(Path(s)) for s in src)
1797 gs_expected = [Path(s) for s in expected]
1798 gs_exclude = None if exclude is None else compile_pattern(exclude)
1799 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
1800 gs_extend_exclude = (
1801 None if extend_exclude is None else compile_pattern(extend_exclude)
1803 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
1804 collected = black.get_sources(
1805 ctx=ctx or FakeContext(),
1811 extend_exclude=gs_extend_exclude,
1812 force_exclude=gs_force_exclude,
1813 report=black.Report(),
1814 stdin_filename=stdin_filename,
1816 assert sorted(collected) == sorted(gs_expected)
1819 class TestFileCollection:
1820 def test_include_exclude(self) -> None:
1821 path = THIS_DIR / "data" / "include_exclude_tests"
1824 Path(path / "b/dont_exclude/a.py"),
1825 Path(path / "b/dont_exclude/a.pyi"),
1827 assert_collected_sources(
1831 exclude=r"/exclude/|/\.definitely_exclude/",
1834 def test_gitignore_used_as_default(self) -> None:
1835 base = Path(DATA_DIR / "include_exclude_tests")
1837 base / "b/.definitely_exclude/a.py",
1838 base / "b/.definitely_exclude/a.pyi",
1842 ctx.obj["root"] = base
1843 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
1845 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
1846 def test_exclude_for_issue_1572(self) -> None:
1847 # Exclude shouldn't touch files that were explicitly given to Black through the
1848 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1849 # https://github.com/psf/black/issues/1572
1850 path = DATA_DIR / "include_exclude_tests"
1851 src = [path / "b/exclude/a.py"]
1852 expected = [path / "b/exclude/a.py"]
1853 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1855 def test_gitignore_exclude(self) -> None:
1856 path = THIS_DIR / "data" / "include_exclude_tests"
1857 include = re.compile(r"\.pyi?$")
1858 exclude = re.compile(r"")
1859 report = black.Report()
1860 gitignore = PathSpec.from_lines(
1861 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1863 sources: List[Path] = []
1865 Path(path / "b/dont_exclude/a.py"),
1866 Path(path / "b/dont_exclude/a.pyi"),
1868 this_abs = THIS_DIR.resolve()
1870 black.gen_python_files(
1883 assert sorted(expected) == sorted(sources)
1885 def test_nested_gitignore(self) -> None:
1886 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
1887 include = re.compile(r"\.pyi?$")
1888 exclude = re.compile(r"")
1889 root_gitignore = black.files.get_gitignore(path)
1890 report = black.Report()
1891 expected: List[Path] = [
1892 Path(path / "x.py"),
1893 Path(path / "root/b.py"),
1894 Path(path / "root/c.py"),
1895 Path(path / "root/child/c.py"),
1897 this_abs = THIS_DIR.resolve()
1899 black.gen_python_files(
1912 assert sorted(expected) == sorted(sources)
1914 def test_invalid_gitignore(self) -> None:
1915 path = THIS_DIR / "data" / "invalid_gitignore_tests"
1916 empty_config = path / "pyproject.toml"
1917 result = BlackRunner().invoke(
1918 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1920 assert result.exit_code == 1
1921 assert result.stderr_bytes is not None
1923 gitignore = path / ".gitignore"
1924 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1926 def test_invalid_nested_gitignore(self) -> None:
1927 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
1928 empty_config = path / "pyproject.toml"
1929 result = BlackRunner().invoke(
1930 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1932 assert result.exit_code == 1
1933 assert result.stderr_bytes is not None
1935 gitignore = path / "a" / ".gitignore"
1936 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1938 def test_empty_include(self) -> None:
1939 path = DATA_DIR / "include_exclude_tests"
1942 Path(path / "b/exclude/a.pie"),
1943 Path(path / "b/exclude/a.py"),
1944 Path(path / "b/exclude/a.pyi"),
1945 Path(path / "b/dont_exclude/a.pie"),
1946 Path(path / "b/dont_exclude/a.py"),
1947 Path(path / "b/dont_exclude/a.pyi"),
1948 Path(path / "b/.definitely_exclude/a.pie"),
1949 Path(path / "b/.definitely_exclude/a.py"),
1950 Path(path / "b/.definitely_exclude/a.pyi"),
1951 Path(path / ".gitignore"),
1952 Path(path / "pyproject.toml"),
1954 # Setting exclude explicitly to an empty string to block .gitignore usage.
1955 assert_collected_sources(src, expected, include="", exclude="")
1957 def test_extend_exclude(self) -> None:
1958 path = DATA_DIR / "include_exclude_tests"
1961 Path(path / "b/exclude/a.py"),
1962 Path(path / "b/dont_exclude/a.py"),
1964 assert_collected_sources(
1965 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
1968 @pytest.mark.incompatible_with_mypyc
1969 def test_symlink_out_of_root_directory(self) -> None:
1971 root = THIS_DIR.resolve()
1973 include = re.compile(black.DEFAULT_INCLUDES)
1974 exclude = re.compile(black.DEFAULT_EXCLUDES)
1975 report = black.Report()
1976 gitignore = PathSpec.from_lines("gitwildmatch", [])
1977 # `child` should behave like a symlink which resolved path is clearly
1978 # outside of the `root` directory.
1979 path.iterdir.return_value = [child]
1980 child.resolve.return_value = Path("/a/b/c")
1981 child.as_posix.return_value = "/a/b/c"
1982 child.is_symlink.return_value = True
1985 black.gen_python_files(
1998 except ValueError as ve:
1999 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
2000 path.iterdir.assert_called_once()
2001 child.resolve.assert_called_once()
2002 child.is_symlink.assert_called_once()
2003 # `child` should behave like a strange file which resolved path is clearly
2004 # outside of the `root` directory.
2005 child.is_symlink.return_value = False
2006 with pytest.raises(ValueError):
2008 black.gen_python_files(
2021 path.iterdir.assert_called()
2022 assert path.iterdir.call_count == 2
2023 child.resolve.assert_called()
2024 assert child.resolve.call_count == 2
2025 child.is_symlink.assert_called()
2026 assert child.is_symlink.call_count == 2
2028 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2029 def test_get_sources_with_stdin(self) -> None:
2032 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2034 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2035 def test_get_sources_with_stdin_filename(self) -> None:
2037 stdin_filename = str(THIS_DIR / "data/collections.py")
2038 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2039 assert_collected_sources(
2042 exclude=r"/exclude/a\.py",
2043 stdin_filename=stdin_filename,
2046 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2047 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2048 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2049 # file being passed directly. This is the same as
2050 # test_exclude_for_issue_1572
2051 path = DATA_DIR / "include_exclude_tests"
2053 stdin_filename = str(path / "b/exclude/a.py")
2054 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2055 assert_collected_sources(
2058 exclude=r"/exclude/|a\.py",
2059 stdin_filename=stdin_filename,
2062 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2063 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2064 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2065 # file being passed directly. This is the same as
2066 # test_exclude_for_issue_1572
2068 path = THIS_DIR / "data" / "include_exclude_tests"
2069 stdin_filename = str(path / "b/exclude/a.py")
2070 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2071 assert_collected_sources(
2074 extend_exclude=r"/exclude/|a\.py",
2075 stdin_filename=stdin_filename,
2078 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2079 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2080 # Force exclude should exclude the file when passing it through
2082 path = THIS_DIR / "data" / "include_exclude_tests"
2083 stdin_filename = str(path / "b/exclude/a.py")
2084 assert_collected_sources(
2087 force_exclude=r"/exclude/|a\.py",
2088 stdin_filename=stdin_filename,
2093 with open(black.__file__, "r", encoding="utf-8") as _bf:
2094 black_source_lines = _bf.readlines()
2095 except UnicodeDecodeError:
2096 if not black.COMPILED:
2101 frame: types.FrameType, event: str, arg: Any
2102 ) -> Callable[[types.FrameType, str, Any], Any]:
2103 """Show function calls `from black/__init__.py` as they happen.
2105 Register this with `sys.settrace()` in a test you're debugging.
2110 stack = len(inspect.stack()) - 19
2112 filename = frame.f_code.co_filename
2113 lineno = frame.f_lineno
2114 func_sig_lineno = lineno - 1
2115 funcname = black_source_lines[func_sig_lineno].strip()
2116 while funcname.startswith("@"):
2117 func_sig_lineno += 1
2118 funcname = black_source_lines[func_sig_lineno].strip()
2119 if "black/__init__.py" in filename:
2120 print(f"{' ' * stack}{lineno}:{funcname}")