All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
12 from concurrent.futures import ThreadPoolExecutor
13 from contextlib import contextmanager
14 from dataclasses import replace
15 from io import BytesIO
16 from pathlib import Path
17 from platform import system
18 from tempfile import TemporaryDirectory
30 from unittest.mock import MagicMock, patch
35 from click import unstyle
36 from click.testing import CliRunner
37 from pathspec import PathSpec
41 from black import Feature, TargetVersion
42 from black import re_compile_maybe_verbose as compile_pattern
43 from black.cache import get_cache_file
44 from black.debug import DebugVisitor
45 from black.output import color_diff, diff
46 from black.report import Report
48 # Import other test classes
49 from tests.util import (
65 THIS_FILE = Path(__file__)
66 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
67 DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
68 DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
72 # Match the time output in a diff, but nothing else
73 DIFF_TIME = re.compile(r"\t[\d\-:+\. ]+")
77 def cache_dir(exists: bool = True) -> Iterator[Path]:
78 with TemporaryDirectory() as workspace:
79 cache_dir = Path(workspace)
81 cache_dir = cache_dir / "new"
82 with patch("black.cache.CACHE_DIR", cache_dir):
87 def event_loop() -> Iterator[None]:
88 policy = asyncio.get_event_loop_policy()
89 loop = policy.new_event_loop()
90 asyncio.set_event_loop(loop)
98 class FakeContext(click.Context):
99 """A fake click Context for when calling functions that need it."""
101 def __init__(self) -> None:
102 self.default_map: Dict[str, Any] = {}
105 class FakeParameter(click.Parameter):
106 """A fake click Parameter for when calling functions that need it."""
108 def __init__(self) -> None:
112 class BlackRunner(CliRunner):
113 """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
115 def __init__(self) -> None:
116 super().__init__(mix_stderr=False)
120 args: List[str], exit_code: int = 0, ignore_config: bool = True
122 runner = BlackRunner()
124 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
125 result = runner.invoke(black.main, args, catch_exceptions=False)
126 assert result.stdout_bytes is not None
127 assert result.stderr_bytes is not None
129 f"Failed with args: {args}\n"
130 f"stdout: {result.stdout_bytes.decode()!r}\n"
131 f"stderr: {result.stderr_bytes.decode()!r}\n"
132 f"exception: {result.exception}"
134 assert result.exit_code == exit_code, msg
137 class BlackTestCase(BlackBaseTestCase):
138 invokeBlack = staticmethod(invokeBlack)
140 def test_empty_ff(self) -> None:
142 tmp_file = Path(black.dump_to_file())
144 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
145 with open(tmp_file, encoding="utf8") as f:
149 self.assertFormatEqual(expected, actual)
151 def test_piping(self) -> None:
152 source, expected = read_data("src/black/__init__", data=False)
153 result = BlackRunner().invoke(
155 ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
156 input=BytesIO(source.encode("utf8")),
158 self.assertEqual(result.exit_code, 0)
159 self.assertFormatEqual(expected, result.output)
160 if source != result.output:
161 black.assert_equivalent(source, result.output)
162 black.assert_stable(source, result.output, DEFAULT_MODE)
164 def test_piping_diff(self) -> None:
165 diff_header = re.compile(
166 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
169 source, _ = read_data("expression.py")
170 expected, _ = read_data("expression.diff")
171 config = THIS_DIR / "data" / "empty_pyproject.toml"
175 f"--line-length={black.DEFAULT_LINE_LENGTH}",
177 f"--config={config}",
179 result = BlackRunner().invoke(
180 black.main, args, input=BytesIO(source.encode("utf8"))
182 self.assertEqual(result.exit_code, 0)
183 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
184 actual = actual.rstrip() + "\n" # the diff output has a trailing space
185 self.assertEqual(expected, actual)
187 def test_piping_diff_with_color(self) -> None:
188 source, _ = read_data("expression.py")
189 config = THIS_DIR / "data" / "empty_pyproject.toml"
193 f"--line-length={black.DEFAULT_LINE_LENGTH}",
196 f"--config={config}",
198 result = BlackRunner().invoke(
199 black.main, args, input=BytesIO(source.encode("utf8"))
201 actual = result.output
202 # Again, the contents are checked in a different test, so only look for colors.
203 self.assertIn("\033[1m", actual)
204 self.assertIn("\033[36m", actual)
205 self.assertIn("\033[32m", actual)
206 self.assertIn("\033[31m", actual)
207 self.assertIn("\033[0m", actual)
209 @patch("black.dump_to_file", dump_to_stderr)
210 def _test_wip(self) -> None:
211 source, expected = read_data("wip")
212 sys.settrace(tracefunc)
215 experimental_string_processing=False,
216 target_versions={black.TargetVersion.PY38},
218 actual = fs(source, mode=mode)
220 self.assertFormatEqual(expected, actual)
221 black.assert_equivalent(source, actual)
222 black.assert_stable(source, actual, black.FileMode())
224 @unittest.expectedFailure
225 @patch("black.dump_to_file", dump_to_stderr)
226 def test_trailing_comma_optional_parens_stability1(self) -> None:
227 source, _expected = read_data("trailing_comma_optional_parens1")
229 black.assert_stable(source, actual, DEFAULT_MODE)
231 @unittest.expectedFailure
232 @patch("black.dump_to_file", dump_to_stderr)
233 def test_trailing_comma_optional_parens_stability2(self) -> None:
234 source, _expected = read_data("trailing_comma_optional_parens2")
236 black.assert_stable(source, actual, DEFAULT_MODE)
238 @unittest.expectedFailure
239 @patch("black.dump_to_file", dump_to_stderr)
240 def test_trailing_comma_optional_parens_stability3(self) -> None:
241 source, _expected = read_data("trailing_comma_optional_parens3")
243 black.assert_stable(source, actual, DEFAULT_MODE)
245 @patch("black.dump_to_file", dump_to_stderr)
246 def test_trailing_comma_optional_parens_stability1_pass2(self) -> None:
247 source, _expected = read_data("trailing_comma_optional_parens1")
248 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
249 black.assert_stable(source, actual, DEFAULT_MODE)
251 @patch("black.dump_to_file", dump_to_stderr)
252 def test_trailing_comma_optional_parens_stability2_pass2(self) -> None:
253 source, _expected = read_data("trailing_comma_optional_parens2")
254 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
255 black.assert_stable(source, actual, DEFAULT_MODE)
257 @patch("black.dump_to_file", dump_to_stderr)
258 def test_trailing_comma_optional_parens_stability3_pass2(self) -> None:
259 source, _expected = read_data("trailing_comma_optional_parens3")
260 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
261 black.assert_stable(source, actual, DEFAULT_MODE)
263 def test_pep_572_version_detection(self) -> None:
264 source, _ = read_data("pep_572")
265 root = black.lib2to3_parse(source)
266 features = black.get_features_used(root)
267 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
268 versions = black.detect_target_versions(root)
269 self.assertIn(black.TargetVersion.PY38, versions)
271 def test_expression_ff(self) -> None:
272 source, expected = read_data("expression")
273 tmp_file = Path(black.dump_to_file(source))
275 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
276 with open(tmp_file, encoding="utf8") as f:
280 self.assertFormatEqual(expected, actual)
281 with patch("black.dump_to_file", dump_to_stderr):
282 black.assert_equivalent(source, actual)
283 black.assert_stable(source, actual, DEFAULT_MODE)
285 def test_expression_diff(self) -> None:
286 source, _ = read_data("expression.py")
287 config = THIS_DIR / "data" / "empty_pyproject.toml"
288 expected, _ = read_data("expression.diff")
289 tmp_file = Path(black.dump_to_file(source))
290 diff_header = re.compile(
291 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
292 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
295 result = BlackRunner().invoke(
296 black.main, ["--diff", str(tmp_file), f"--config={config}"]
298 self.assertEqual(result.exit_code, 0)
301 actual = result.output
302 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
303 if expected != actual:
304 dump = black.dump_to_file(actual)
306 "Expected diff isn't equal to the actual. If you made changes to"
307 " expression.py and this is an anticipated difference, overwrite"
308 f" tests/data/expression.diff with {dump}"
310 self.assertEqual(expected, actual, msg)
312 def test_expression_diff_with_color(self) -> None:
313 source, _ = read_data("expression.py")
314 config = THIS_DIR / "data" / "empty_pyproject.toml"
315 expected, _ = read_data("expression.diff")
316 tmp_file = Path(black.dump_to_file(source))
318 result = BlackRunner().invoke(
319 black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
323 actual = result.output
324 # We check the contents of the diff in `test_expression_diff`. All
325 # we need to check here is that color codes exist in the result.
326 self.assertIn("\033[1m", actual)
327 self.assertIn("\033[36m", actual)
328 self.assertIn("\033[32m", actual)
329 self.assertIn("\033[31m", actual)
330 self.assertIn("\033[0m", actual)
332 def test_detect_pos_only_arguments(self) -> None:
333 source, _ = read_data("pep_570")
334 root = black.lib2to3_parse(source)
335 features = black.get_features_used(root)
336 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
337 versions = black.detect_target_versions(root)
338 self.assertIn(black.TargetVersion.PY38, versions)
340 @patch("black.dump_to_file", dump_to_stderr)
341 def test_string_quotes(self) -> None:
342 source, expected = read_data("string_quotes")
343 mode = black.Mode(experimental_string_processing=True)
344 assert_format(source, expected, mode)
345 mode = replace(mode, string_normalization=False)
346 not_normalized = fs(source, mode=mode)
347 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
348 black.assert_equivalent(source, not_normalized)
349 black.assert_stable(source, not_normalized, mode=mode)
351 def test_skip_magic_trailing_comma(self) -> None:
352 source, _ = read_data("expression.py")
353 expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
354 tmp_file = Path(black.dump_to_file(source))
355 diff_header = re.compile(
356 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
357 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
360 result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
361 self.assertEqual(result.exit_code, 0)
364 actual = result.output
365 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
366 actual = actual.rstrip() + "\n" # the diff output has a trailing space
367 if expected != actual:
368 dump = black.dump_to_file(actual)
370 "Expected diff isn't equal to the actual. If you made changes to"
371 " expression.py and this is an anticipated difference, overwrite"
372 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
374 self.assertEqual(expected, actual, msg)
376 @patch("black.dump_to_file", dump_to_stderr)
377 def test_async_as_identifier(self) -> None:
378 source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
379 source, expected = read_data("async_as_identifier")
381 self.assertFormatEqual(expected, actual)
382 major, minor = sys.version_info[:2]
383 if major < 3 or (major <= 3 and minor < 7):
384 black.assert_equivalent(source, actual)
385 black.assert_stable(source, actual, DEFAULT_MODE)
386 # ensure black can parse this when the target is 3.6
387 self.invokeBlack([str(source_path), "--target-version", "py36"])
388 # but not on 3.7, because async/await is no longer an identifier
389 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
391 @patch("black.dump_to_file", dump_to_stderr)
392 def test_python37(self) -> None:
393 source_path = (THIS_DIR / "data" / "python37.py").resolve()
394 source, expected = read_data("python37")
396 self.assertFormatEqual(expected, actual)
397 major, minor = sys.version_info[:2]
398 if major > 3 or (major == 3 and minor >= 7):
399 black.assert_equivalent(source, actual)
400 black.assert_stable(source, actual, DEFAULT_MODE)
401 # ensure black can parse this when the target is 3.7
402 self.invokeBlack([str(source_path), "--target-version", "py37"])
403 # but not on 3.6, because we use async as a reserved keyword
404 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
406 def test_tab_comment_indentation(self) -> None:
407 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
408 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
409 self.assertFormatEqual(contents_spc, fs(contents_spc))
410 self.assertFormatEqual(contents_spc, fs(contents_tab))
412 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
413 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
414 self.assertFormatEqual(contents_spc, fs(contents_spc))
415 self.assertFormatEqual(contents_spc, fs(contents_tab))
417 # mixed tabs and spaces (valid Python 2 code)
418 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
419 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
420 self.assertFormatEqual(contents_spc, fs(contents_spc))
421 self.assertFormatEqual(contents_spc, fs(contents_tab))
423 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
424 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
425 self.assertFormatEqual(contents_spc, fs(contents_spc))
426 self.assertFormatEqual(contents_spc, fs(contents_tab))
428 def test_report_verbose(self) -> None:
429 report = Report(verbose=True)
433 def out(msg: str, **kwargs: Any) -> None:
434 out_lines.append(msg)
436 def err(msg: str, **kwargs: Any) -> None:
437 err_lines.append(msg)
439 with patch("black.output._out", out), patch("black.output._err", err):
440 report.done(Path("f1"), black.Changed.NO)
441 self.assertEqual(len(out_lines), 1)
442 self.assertEqual(len(err_lines), 0)
443 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
444 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
445 self.assertEqual(report.return_code, 0)
446 report.done(Path("f2"), black.Changed.YES)
447 self.assertEqual(len(out_lines), 2)
448 self.assertEqual(len(err_lines), 0)
449 self.assertEqual(out_lines[-1], "reformatted f2")
451 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
453 report.done(Path("f3"), black.Changed.CACHED)
454 self.assertEqual(len(out_lines), 3)
455 self.assertEqual(len(err_lines), 0)
457 out_lines[-1], "f3 wasn't modified on disk since last run."
460 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
462 self.assertEqual(report.return_code, 0)
464 self.assertEqual(report.return_code, 1)
466 report.failed(Path("e1"), "boom")
467 self.assertEqual(len(out_lines), 3)
468 self.assertEqual(len(err_lines), 1)
469 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
471 unstyle(str(report)),
472 "1 file reformatted, 2 files left unchanged, 1 file failed to"
475 self.assertEqual(report.return_code, 123)
476 report.done(Path("f3"), black.Changed.YES)
477 self.assertEqual(len(out_lines), 4)
478 self.assertEqual(len(err_lines), 1)
479 self.assertEqual(out_lines[-1], "reformatted f3")
481 unstyle(str(report)),
482 "2 files reformatted, 2 files left unchanged, 1 file failed to"
485 self.assertEqual(report.return_code, 123)
486 report.failed(Path("e2"), "boom")
487 self.assertEqual(len(out_lines), 4)
488 self.assertEqual(len(err_lines), 2)
489 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
491 unstyle(str(report)),
492 "2 files reformatted, 2 files left unchanged, 2 files failed to"
495 self.assertEqual(report.return_code, 123)
496 report.path_ignored(Path("wat"), "no match")
497 self.assertEqual(len(out_lines), 5)
498 self.assertEqual(len(err_lines), 2)
499 self.assertEqual(out_lines[-1], "wat ignored: no match")
501 unstyle(str(report)),
502 "2 files reformatted, 2 files left unchanged, 2 files failed to"
505 self.assertEqual(report.return_code, 123)
506 report.done(Path("f4"), black.Changed.NO)
507 self.assertEqual(len(out_lines), 6)
508 self.assertEqual(len(err_lines), 2)
509 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
511 unstyle(str(report)),
512 "2 files reformatted, 3 files left unchanged, 2 files failed to"
515 self.assertEqual(report.return_code, 123)
518 unstyle(str(report)),
519 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
520 " would fail to reformat.",
525 unstyle(str(report)),
526 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
527 " would fail to reformat.",
530 def test_report_quiet(self) -> None:
531 report = Report(quiet=True)
535 def out(msg: str, **kwargs: Any) -> None:
536 out_lines.append(msg)
538 def err(msg: str, **kwargs: Any) -> None:
539 err_lines.append(msg)
541 with patch("black.output._out", out), patch("black.output._err", err):
542 report.done(Path("f1"), black.Changed.NO)
543 self.assertEqual(len(out_lines), 0)
544 self.assertEqual(len(err_lines), 0)
545 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
546 self.assertEqual(report.return_code, 0)
547 report.done(Path("f2"), black.Changed.YES)
548 self.assertEqual(len(out_lines), 0)
549 self.assertEqual(len(err_lines), 0)
551 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
553 report.done(Path("f3"), black.Changed.CACHED)
554 self.assertEqual(len(out_lines), 0)
555 self.assertEqual(len(err_lines), 0)
557 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
559 self.assertEqual(report.return_code, 0)
561 self.assertEqual(report.return_code, 1)
563 report.failed(Path("e1"), "boom")
564 self.assertEqual(len(out_lines), 0)
565 self.assertEqual(len(err_lines), 1)
566 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
568 unstyle(str(report)),
569 "1 file reformatted, 2 files left unchanged, 1 file failed to"
572 self.assertEqual(report.return_code, 123)
573 report.done(Path("f3"), black.Changed.YES)
574 self.assertEqual(len(out_lines), 0)
575 self.assertEqual(len(err_lines), 1)
577 unstyle(str(report)),
578 "2 files reformatted, 2 files left unchanged, 1 file failed to"
581 self.assertEqual(report.return_code, 123)
582 report.failed(Path("e2"), "boom")
583 self.assertEqual(len(out_lines), 0)
584 self.assertEqual(len(err_lines), 2)
585 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
587 unstyle(str(report)),
588 "2 files reformatted, 2 files left unchanged, 2 files failed to"
591 self.assertEqual(report.return_code, 123)
592 report.path_ignored(Path("wat"), "no match")
593 self.assertEqual(len(out_lines), 0)
594 self.assertEqual(len(err_lines), 2)
596 unstyle(str(report)),
597 "2 files reformatted, 2 files left unchanged, 2 files failed to"
600 self.assertEqual(report.return_code, 123)
601 report.done(Path("f4"), black.Changed.NO)
602 self.assertEqual(len(out_lines), 0)
603 self.assertEqual(len(err_lines), 2)
605 unstyle(str(report)),
606 "2 files reformatted, 3 files left unchanged, 2 files failed to"
609 self.assertEqual(report.return_code, 123)
612 unstyle(str(report)),
613 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
614 " would fail to reformat.",
619 unstyle(str(report)),
620 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
621 " would fail to reformat.",
624 def test_report_normal(self) -> None:
625 report = black.Report()
629 def out(msg: str, **kwargs: Any) -> None:
630 out_lines.append(msg)
632 def err(msg: str, **kwargs: Any) -> None:
633 err_lines.append(msg)
635 with patch("black.output._out", out), patch("black.output._err", err):
636 report.done(Path("f1"), black.Changed.NO)
637 self.assertEqual(len(out_lines), 0)
638 self.assertEqual(len(err_lines), 0)
639 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
640 self.assertEqual(report.return_code, 0)
641 report.done(Path("f2"), black.Changed.YES)
642 self.assertEqual(len(out_lines), 1)
643 self.assertEqual(len(err_lines), 0)
644 self.assertEqual(out_lines[-1], "reformatted f2")
646 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
648 report.done(Path("f3"), black.Changed.CACHED)
649 self.assertEqual(len(out_lines), 1)
650 self.assertEqual(len(err_lines), 0)
651 self.assertEqual(out_lines[-1], "reformatted f2")
653 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
655 self.assertEqual(report.return_code, 0)
657 self.assertEqual(report.return_code, 1)
659 report.failed(Path("e1"), "boom")
660 self.assertEqual(len(out_lines), 1)
661 self.assertEqual(len(err_lines), 1)
662 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
664 unstyle(str(report)),
665 "1 file reformatted, 2 files left unchanged, 1 file failed to"
668 self.assertEqual(report.return_code, 123)
669 report.done(Path("f3"), black.Changed.YES)
670 self.assertEqual(len(out_lines), 2)
671 self.assertEqual(len(err_lines), 1)
672 self.assertEqual(out_lines[-1], "reformatted f3")
674 unstyle(str(report)),
675 "2 files reformatted, 2 files left unchanged, 1 file failed to"
678 self.assertEqual(report.return_code, 123)
679 report.failed(Path("e2"), "boom")
680 self.assertEqual(len(out_lines), 2)
681 self.assertEqual(len(err_lines), 2)
682 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
684 unstyle(str(report)),
685 "2 files reformatted, 2 files left unchanged, 2 files failed to"
688 self.assertEqual(report.return_code, 123)
689 report.path_ignored(Path("wat"), "no match")
690 self.assertEqual(len(out_lines), 2)
691 self.assertEqual(len(err_lines), 2)
693 unstyle(str(report)),
694 "2 files reformatted, 2 files left unchanged, 2 files failed to"
697 self.assertEqual(report.return_code, 123)
698 report.done(Path("f4"), black.Changed.NO)
699 self.assertEqual(len(out_lines), 2)
700 self.assertEqual(len(err_lines), 2)
702 unstyle(str(report)),
703 "2 files reformatted, 3 files left unchanged, 2 files failed to"
706 self.assertEqual(report.return_code, 123)
709 unstyle(str(report)),
710 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
711 " would fail to reformat.",
716 unstyle(str(report)),
717 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
718 " would fail to reformat.",
721 def test_lib2to3_parse(self) -> None:
722 with self.assertRaises(black.InvalidInput):
723 black.lib2to3_parse("invalid syntax")
726 black.lib2to3_parse(straddling)
727 black.lib2to3_parse(straddling, {TargetVersion.PY36})
730 with self.assertRaises(black.InvalidInput):
731 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
733 py3_only = "exec(x, end=y)"
734 black.lib2to3_parse(py3_only)
735 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
737 def test_get_features_used_decorator(self) -> None:
738 # Test the feature detection of new decorator syntax
739 # since this makes some test cases of test_get_features_used()
740 # fails if it fails, this is tested first so that a useful case
742 simples, relaxed = read_data("decorators")
743 # skip explanation comments at the top of the file
744 for simple_test in simples.split("##")[1:]:
745 node = black.lib2to3_parse(simple_test)
746 decorator = str(node.children[0].children[0]).strip()
748 Feature.RELAXED_DECORATORS,
749 black.get_features_used(node),
751 f"decorator '{decorator}' follows python<=3.8 syntax"
752 "but is detected as 3.9+"
753 # f"The full node is\n{node!r}"
756 # skip the '# output' comment at the top of the output part
757 for relaxed_test in relaxed.split("##")[1:]:
758 node = black.lib2to3_parse(relaxed_test)
759 decorator = str(node.children[0].children[0]).strip()
761 Feature.RELAXED_DECORATORS,
762 black.get_features_used(node),
764 f"decorator '{decorator}' uses python3.9+ syntax"
765 "but is detected as python<=3.8"
766 # f"The full node is\n{node!r}"
770 def test_get_features_used(self) -> None:
771 node = black.lib2to3_parse("def f(*, arg): ...\n")
772 self.assertEqual(black.get_features_used(node), set())
773 node = black.lib2to3_parse("def f(*, arg,): ...\n")
774 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
775 node = black.lib2to3_parse("f(*arg,)\n")
777 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
779 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
780 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
781 node = black.lib2to3_parse("123_456\n")
782 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
783 node = black.lib2to3_parse("123456\n")
784 self.assertEqual(black.get_features_used(node), set())
785 source, expected = read_data("function")
786 node = black.lib2to3_parse(source)
787 expected_features = {
788 Feature.TRAILING_COMMA_IN_CALL,
789 Feature.TRAILING_COMMA_IN_DEF,
792 self.assertEqual(black.get_features_used(node), expected_features)
793 node = black.lib2to3_parse(expected)
794 self.assertEqual(black.get_features_used(node), expected_features)
795 source, expected = read_data("expression")
796 node = black.lib2to3_parse(source)
797 self.assertEqual(black.get_features_used(node), set())
798 node = black.lib2to3_parse(expected)
799 self.assertEqual(black.get_features_used(node), set())
800 node = black.lib2to3_parse("lambda a, /, b: ...")
801 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
802 node = black.lib2to3_parse("def fn(a, /, b): ...")
803 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
804 node = black.lib2to3_parse("def fn(): yield a, b")
805 self.assertEqual(black.get_features_used(node), set())
806 node = black.lib2to3_parse("def fn(): return a, b")
807 self.assertEqual(black.get_features_used(node), set())
808 node = black.lib2to3_parse("def fn(): yield *b, c")
809 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
810 node = black.lib2to3_parse("def fn(): return a, *b, c")
811 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
812 node = black.lib2to3_parse("x = a, *b, c")
813 self.assertEqual(black.get_features_used(node), set())
814 node = black.lib2to3_parse("x: Any = regular")
815 self.assertEqual(black.get_features_used(node), set())
816 node = black.lib2to3_parse("x: Any = (regular, regular)")
817 self.assertEqual(black.get_features_used(node), set())
818 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
819 self.assertEqual(black.get_features_used(node), set())
820 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
822 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
825 def test_get_features_used_for_future_flags(self) -> None:
826 for src, features in [
827 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
829 "from __future__ import (other, annotations)",
830 {Feature.FUTURE_ANNOTATIONS},
832 ("a = 1 + 2\nfrom something import annotations", set()),
833 ("from __future__ import x, y", set()),
835 with self.subTest(src=src, features=features):
836 node = black.lib2to3_parse(src)
837 future_imports = black.get_future_imports(node)
839 black.get_features_used(node, future_imports=future_imports),
843 def test_get_future_imports(self) -> None:
844 node = black.lib2to3_parse("\n")
845 self.assertEqual(set(), black.get_future_imports(node))
846 node = black.lib2to3_parse("from __future__ import black\n")
847 self.assertEqual({"black"}, black.get_future_imports(node))
848 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
849 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
850 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
851 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
852 node = black.lib2to3_parse(
853 "from __future__ import multiple\nfrom __future__ import imports\n"
855 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
856 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
857 self.assertEqual({"black"}, black.get_future_imports(node))
858 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
859 self.assertEqual({"black"}, black.get_future_imports(node))
860 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
861 self.assertEqual(set(), black.get_future_imports(node))
862 node = black.lib2to3_parse("from some.module import black\n")
863 self.assertEqual(set(), black.get_future_imports(node))
864 node = black.lib2to3_parse(
865 "from __future__ import unicode_literals as _unicode_literals"
867 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
868 node = black.lib2to3_parse(
869 "from __future__ import unicode_literals as _lol, print"
871 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
873 @pytest.mark.incompatible_with_mypyc
874 def test_debug_visitor(self) -> None:
875 source, _ = read_data("debug_visitor.py")
876 expected, _ = read_data("debug_visitor.out")
880 def out(msg: str, **kwargs: Any) -> None:
881 out_lines.append(msg)
883 def err(msg: str, **kwargs: Any) -> None:
884 err_lines.append(msg)
886 with patch("black.debug.out", out):
887 DebugVisitor.show(source)
888 actual = "\n".join(out_lines) + "\n"
890 if expected != actual:
891 log_name = black.dump_to_file(*out_lines)
895 f"AST print out is different. Actual version dumped to {log_name}",
898 def test_format_file_contents(self) -> None:
901 with self.assertRaises(black.NothingChanged):
902 black.format_file_contents(empty, mode=mode, fast=False)
904 with self.assertRaises(black.NothingChanged):
905 black.format_file_contents(just_nl, mode=mode, fast=False)
906 same = "j = [1, 2, 3]\n"
907 with self.assertRaises(black.NothingChanged):
908 black.format_file_contents(same, mode=mode, fast=False)
909 different = "j = [1,2,3]"
911 actual = black.format_file_contents(different, mode=mode, fast=False)
912 self.assertEqual(expected, actual)
913 invalid = "return if you can"
914 with self.assertRaises(black.InvalidInput) as e:
915 black.format_file_contents(invalid, mode=mode, fast=False)
916 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
918 def test_endmarker(self) -> None:
919 n = black.lib2to3_parse("\n")
920 self.assertEqual(n.type, black.syms.file_input)
921 self.assertEqual(len(n.children), 1)
922 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
924 @pytest.mark.incompatible_with_mypyc
925 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
926 def test_assertFormatEqual(self) -> None:
930 def out(msg: str, **kwargs: Any) -> None:
931 out_lines.append(msg)
933 def err(msg: str, **kwargs: Any) -> None:
934 err_lines.append(msg)
936 with patch("black.output._out", out), patch("black.output._err", err):
937 with self.assertRaises(AssertionError):
938 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
940 out_str = "".join(out_lines)
941 self.assertTrue("Expected tree:" in out_str)
942 self.assertTrue("Actual tree:" in out_str)
943 self.assertEqual("".join(err_lines), "")
946 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
947 def test_works_in_mono_process_only_environment(self) -> None:
948 with cache_dir() as workspace:
950 (workspace / "one.py").resolve(),
951 (workspace / "two.py").resolve(),
953 f.write_text('print("hello")\n')
954 self.invokeBlack([str(workspace)])
957 def test_check_diff_use_together(self) -> None:
959 # Files which will be reformatted.
960 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
961 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
962 # Files which will not be reformatted.
963 src2 = (THIS_DIR / "data" / "composition.py").resolve()
964 self.invokeBlack([str(src2), "--diff", "--check"])
965 # Multi file command.
966 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
968 def test_no_files(self) -> None:
970 # Without an argument, black exits with error code 0.
973 def test_broken_symlink(self) -> None:
974 with cache_dir() as workspace:
975 symlink = workspace / "broken_link.py"
977 symlink.symlink_to("nonexistent.py")
978 except (OSError, NotImplementedError) as e:
979 self.skipTest(f"Can't create symlinks: {e}")
980 self.invokeBlack([str(workspace.resolve())])
982 def test_single_file_force_pyi(self) -> None:
983 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
984 contents, expected = read_data("force_pyi")
985 with cache_dir() as workspace:
986 path = (workspace / "file.py").resolve()
987 with open(path, "w") as fh:
989 self.invokeBlack([str(path), "--pyi"])
990 with open(path, "r") as fh:
992 # verify cache with --pyi is separate
993 pyi_cache = black.read_cache(pyi_mode)
994 self.assertIn(str(path), pyi_cache)
995 normal_cache = black.read_cache(DEFAULT_MODE)
996 self.assertNotIn(str(path), normal_cache)
997 self.assertFormatEqual(expected, actual)
998 black.assert_equivalent(contents, actual)
999 black.assert_stable(contents, actual, pyi_mode)
1002 def test_multi_file_force_pyi(self) -> None:
1003 reg_mode = DEFAULT_MODE
1004 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1005 contents, expected = read_data("force_pyi")
1006 with cache_dir() as workspace:
1008 (workspace / "file1.py").resolve(),
1009 (workspace / "file2.py").resolve(),
1012 with open(path, "w") as fh:
1014 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1016 with open(path, "r") as fh:
1018 self.assertEqual(actual, expected)
1019 # verify cache with --pyi is separate
1020 pyi_cache = black.read_cache(pyi_mode)
1021 normal_cache = black.read_cache(reg_mode)
1023 self.assertIn(str(path), pyi_cache)
1024 self.assertNotIn(str(path), normal_cache)
1026 def test_pipe_force_pyi(self) -> None:
1027 source, expected = read_data("force_pyi")
1028 result = CliRunner().invoke(
1029 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1031 self.assertEqual(result.exit_code, 0)
1032 actual = result.output
1033 self.assertFormatEqual(actual, expected)
1035 def test_single_file_force_py36(self) -> None:
1036 reg_mode = DEFAULT_MODE
1037 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1038 source, expected = read_data("force_py36")
1039 with cache_dir() as workspace:
1040 path = (workspace / "file.py").resolve()
1041 with open(path, "w") as fh:
1043 self.invokeBlack([str(path), *PY36_ARGS])
1044 with open(path, "r") as fh:
1046 # verify cache with --target-version is separate
1047 py36_cache = black.read_cache(py36_mode)
1048 self.assertIn(str(path), py36_cache)
1049 normal_cache = black.read_cache(reg_mode)
1050 self.assertNotIn(str(path), normal_cache)
1051 self.assertEqual(actual, expected)
1054 def test_multi_file_force_py36(self) -> None:
1055 reg_mode = DEFAULT_MODE
1056 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1057 source, expected = read_data("force_py36")
1058 with cache_dir() as workspace:
1060 (workspace / "file1.py").resolve(),
1061 (workspace / "file2.py").resolve(),
1064 with open(path, "w") as fh:
1066 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1068 with open(path, "r") as fh:
1070 self.assertEqual(actual, expected)
1071 # verify cache with --target-version is separate
1072 pyi_cache = black.read_cache(py36_mode)
1073 normal_cache = black.read_cache(reg_mode)
1075 self.assertIn(str(path), pyi_cache)
1076 self.assertNotIn(str(path), normal_cache)
1078 def test_pipe_force_py36(self) -> None:
1079 source, expected = read_data("force_py36")
1080 result = CliRunner().invoke(
1082 ["-", "-q", "--target-version=py36"],
1083 input=BytesIO(source.encode("utf8")),
1085 self.assertEqual(result.exit_code, 0)
1086 actual = result.output
1087 self.assertFormatEqual(actual, expected)
1089 @pytest.mark.incompatible_with_mypyc
1090 def test_reformat_one_with_stdin(self) -> None:
1092 "black.format_stdin_to_stdout",
1093 return_value=lambda *args, **kwargs: black.Changed.YES,
1095 report = MagicMock()
1100 write_back=black.WriteBack.YES,
1104 fsts.assert_called_once()
1105 report.done.assert_called_with(path, black.Changed.YES)
1107 @pytest.mark.incompatible_with_mypyc
1108 def test_reformat_one_with_stdin_filename(self) -> None:
1110 "black.format_stdin_to_stdout",
1111 return_value=lambda *args, **kwargs: black.Changed.YES,
1113 report = MagicMock()
1115 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1120 write_back=black.WriteBack.YES,
1124 fsts.assert_called_once_with(
1125 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1127 # __BLACK_STDIN_FILENAME__ should have been stripped
1128 report.done.assert_called_with(expected, black.Changed.YES)
1130 @pytest.mark.incompatible_with_mypyc
1131 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1133 "black.format_stdin_to_stdout",
1134 return_value=lambda *args, **kwargs: black.Changed.YES,
1136 report = MagicMock()
1138 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1143 write_back=black.WriteBack.YES,
1147 fsts.assert_called_once_with(
1149 write_back=black.WriteBack.YES,
1150 mode=replace(DEFAULT_MODE, is_pyi=True),
1152 # __BLACK_STDIN_FILENAME__ should have been stripped
1153 report.done.assert_called_with(expected, black.Changed.YES)
1155 @pytest.mark.incompatible_with_mypyc
1156 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1158 "black.format_stdin_to_stdout",
1159 return_value=lambda *args, **kwargs: black.Changed.YES,
1161 report = MagicMock()
1163 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1168 write_back=black.WriteBack.YES,
1172 fsts.assert_called_once_with(
1174 write_back=black.WriteBack.YES,
1175 mode=replace(DEFAULT_MODE, is_ipynb=True),
1177 # __BLACK_STDIN_FILENAME__ should have been stripped
1178 report.done.assert_called_with(expected, black.Changed.YES)
1180 @pytest.mark.incompatible_with_mypyc
1181 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1183 "black.format_stdin_to_stdout",
1184 return_value=lambda *args, **kwargs: black.Changed.YES,
1186 report = MagicMock()
1187 # Even with an existing file, since we are forcing stdin, black
1188 # should output to stdout and not modify the file inplace
1189 p = Path(str(THIS_DIR / "data/collections.py"))
1190 # Make sure is_file actually returns True
1191 self.assertTrue(p.is_file())
1192 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1197 write_back=black.WriteBack.YES,
1201 fsts.assert_called_once()
1202 # __BLACK_STDIN_FILENAME__ should have been stripped
1203 report.done.assert_called_with(expected, black.Changed.YES)
1205 def test_reformat_one_with_stdin_empty(self) -> None:
1206 output = io.StringIO()
1207 with patch("io.TextIOWrapper", lambda *args, **kwargs: output):
1209 black.format_stdin_to_stdout(
1212 write_back=black.WriteBack.YES,
1215 except io.UnsupportedOperation:
1216 pass # StringIO does not support detach
1217 assert output.getvalue() == ""
1219 def test_invalid_cli_regex(self) -> None:
1220 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1221 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1223 def test_required_version_matches_version(self) -> None:
1225 ["--required-version", black.__version__], exit_code=0, ignore_config=True
1228 def test_required_version_does_not_match_version(self) -> None:
1230 ["--required-version", "20.99b"], exit_code=1, ignore_config=True
1233 def test_preserves_line_endings(self) -> None:
1234 with TemporaryDirectory() as workspace:
1235 test_file = Path(workspace) / "test.py"
1236 for nl in ["\n", "\r\n"]:
1237 contents = nl.join(["def f( ):", " pass"])
1238 test_file.write_bytes(contents.encode())
1239 ff(test_file, write_back=black.WriteBack.YES)
1240 updated_contents: bytes = test_file.read_bytes()
1241 self.assertIn(nl.encode(), updated_contents)
1243 self.assertNotIn(b"\r\n", updated_contents)
1245 def test_preserves_line_endings_via_stdin(self) -> None:
1246 for nl in ["\n", "\r\n"]:
1247 contents = nl.join(["def f( ):", " pass"])
1248 runner = BlackRunner()
1249 result = runner.invoke(
1250 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1252 self.assertEqual(result.exit_code, 0)
1253 output = result.stdout_bytes
1254 self.assertIn(nl.encode("utf8"), output)
1256 self.assertNotIn(b"\r\n", output)
1258 def test_assert_equivalent_different_asts(self) -> None:
1259 with self.assertRaises(AssertionError):
1260 black.assert_equivalent("{}", "None")
1262 def test_shhh_click(self) -> None:
1264 from click import _unicodefun
1265 except ModuleNotFoundError:
1266 self.skipTest("Incompatible Click version")
1267 if not hasattr(_unicodefun, "_verify_python3_env"):
1268 self.skipTest("Incompatible Click version")
1269 # First, let's see if Click is crashing with a preferred ASCII charset.
1270 with patch("locale.getpreferredencoding") as gpe:
1271 gpe.return_value = "ASCII"
1272 with self.assertRaises(RuntimeError):
1273 _unicodefun._verify_python3_env() # type: ignore
1274 # Now, let's silence Click...
1276 # ...and confirm it's silent.
1277 with patch("locale.getpreferredencoding") as gpe:
1278 gpe.return_value = "ASCII"
1280 _unicodefun._verify_python3_env() # type: ignore
1281 except RuntimeError as re:
1282 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1284 def test_root_logger_not_used_directly(self) -> None:
1285 def fail(*args: Any, **kwargs: Any) -> None:
1286 self.fail("Record created with root logger")
1288 with patch.multiple(
1297 ff(THIS_DIR / "util.py")
1299 def test_invalid_config_return_code(self) -> None:
1300 tmp_file = Path(black.dump_to_file())
1302 tmp_config = Path(black.dump_to_file())
1304 args = ["--config", str(tmp_config), str(tmp_file)]
1305 self.invokeBlack(args, exit_code=2, ignore_config=False)
1309 def test_parse_pyproject_toml(self) -> None:
1310 test_toml_file = THIS_DIR / "test.toml"
1311 config = black.parse_pyproject_toml(str(test_toml_file))
1312 self.assertEqual(config["verbose"], 1)
1313 self.assertEqual(config["check"], "no")
1314 self.assertEqual(config["diff"], "y")
1315 self.assertEqual(config["color"], True)
1316 self.assertEqual(config["line_length"], 79)
1317 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1318 self.assertEqual(config["exclude"], r"\.pyi?$")
1319 self.assertEqual(config["include"], r"\.py?$")
1321 def test_read_pyproject_toml(self) -> None:
1322 test_toml_file = THIS_DIR / "test.toml"
1323 fake_ctx = FakeContext()
1324 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1325 config = fake_ctx.default_map
1326 self.assertEqual(config["verbose"], "1")
1327 self.assertEqual(config["check"], "no")
1328 self.assertEqual(config["diff"], "y")
1329 self.assertEqual(config["color"], "True")
1330 self.assertEqual(config["line_length"], "79")
1331 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1332 self.assertEqual(config["exclude"], r"\.pyi?$")
1333 self.assertEqual(config["include"], r"\.py?$")
1335 @pytest.mark.incompatible_with_mypyc
1336 def test_find_project_root(self) -> None:
1337 with TemporaryDirectory() as workspace:
1338 root = Path(workspace)
1339 test_dir = root / "test"
1342 src_dir = root / "src"
1345 root_pyproject = root / "pyproject.toml"
1346 root_pyproject.touch()
1347 src_pyproject = src_dir / "pyproject.toml"
1348 src_pyproject.touch()
1349 src_python = src_dir / "foo.py"
1353 black.find_project_root((src_dir, test_dir)), root.resolve()
1355 self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1356 self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1359 "black.files.find_user_pyproject_toml",
1360 black.files.find_user_pyproject_toml.__wrapped__,
1362 def test_find_user_pyproject_toml_linux(self) -> None:
1363 if system() == "Windows":
1366 # Test if XDG_CONFIG_HOME is checked
1367 with TemporaryDirectory() as workspace:
1368 tmp_user_config = Path(workspace) / "black"
1369 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1371 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1374 # Test fallback for XDG_CONFIG_HOME
1375 with patch.dict("os.environ"):
1376 os.environ.pop("XDG_CONFIG_HOME", None)
1377 fallback_user_config = Path("~/.config").expanduser() / "black"
1379 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1382 def test_find_user_pyproject_toml_windows(self) -> None:
1383 if system() != "Windows":
1386 user_config_path = Path.home() / ".black"
1388 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1391 def test_bpo_33660_workaround(self) -> None:
1392 if system() == "Windows":
1395 # https://bugs.python.org/issue33660
1397 with change_directory(root):
1398 path = Path("workspace") / "project"
1399 report = black.Report(verbose=True)
1400 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1401 self.assertEqual(normalized_path, "workspace/project")
1403 def test_newline_comment_interaction(self) -> None:
1404 source = "class A:\\\r\n# type: ignore\n pass\n"
1405 output = black.format_str(source, mode=DEFAULT_MODE)
1406 black.assert_stable(source, output, mode=DEFAULT_MODE)
1408 def test_bpo_2142_workaround(self) -> None:
1410 # https://bugs.python.org/issue2142
1412 source, _ = read_data("missing_final_newline.py")
1413 # read_data adds a trailing newline
1414 source = source.rstrip()
1415 expected, _ = read_data("missing_final_newline.diff")
1416 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1417 diff_header = re.compile(
1418 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1419 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1422 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1423 self.assertEqual(result.exit_code, 0)
1426 actual = result.output
1427 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1428 self.assertEqual(actual, expected)
1431 def compare_results(
1432 result: click.testing.Result, expected_value: str, expected_exit_code: int
1434 """Helper method to test the value and exit code of a click Result."""
1436 result.output == expected_value
1437 ), "The output did not match the expected value."
1438 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1440 def test_code_option(self) -> None:
1441 """Test the code option with no changes."""
1442 code = 'print("Hello world")\n'
1443 args = ["--code", code]
1444 result = CliRunner().invoke(black.main, args)
1446 self.compare_results(result, code, 0)
1448 def test_code_option_changed(self) -> None:
1449 """Test the code option when changes are required."""
1450 code = "print('hello world')"
1451 formatted = black.format_str(code, mode=DEFAULT_MODE)
1453 args = ["--code", code]
1454 result = CliRunner().invoke(black.main, args)
1456 self.compare_results(result, formatted, 0)
1458 def test_code_option_check(self) -> None:
1459 """Test the code option when check is passed."""
1460 args = ["--check", "--code", 'print("Hello world")\n']
1461 result = CliRunner().invoke(black.main, args)
1462 self.compare_results(result, "", 0)
1464 def test_code_option_check_changed(self) -> None:
1465 """Test the code option when changes are required, and check is passed."""
1466 args = ["--check", "--code", "print('hello world')"]
1467 result = CliRunner().invoke(black.main, args)
1468 self.compare_results(result, "", 1)
1470 def test_code_option_diff(self) -> None:
1471 """Test the code option when diff is passed."""
1472 code = "print('hello world')"
1473 formatted = black.format_str(code, mode=DEFAULT_MODE)
1474 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1476 args = ["--diff", "--code", code]
1477 result = CliRunner().invoke(black.main, args)
1479 # Remove time from diff
1480 output = DIFF_TIME.sub("", result.output)
1482 assert output == result_diff, "The output did not match the expected value."
1483 assert result.exit_code == 0, "The exit code is incorrect."
1485 def test_code_option_color_diff(self) -> None:
1486 """Test the code option when color and diff are passed."""
1487 code = "print('hello world')"
1488 formatted = black.format_str(code, mode=DEFAULT_MODE)
1490 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1491 result_diff = color_diff(result_diff)
1493 args = ["--diff", "--color", "--code", code]
1494 result = CliRunner().invoke(black.main, args)
1496 # Remove time from diff
1497 output = DIFF_TIME.sub("", result.output)
1499 assert output == result_diff, "The output did not match the expected value."
1500 assert result.exit_code == 0, "The exit code is incorrect."
1502 @pytest.mark.incompatible_with_mypyc
1503 def test_code_option_safe(self) -> None:
1504 """Test that the code option throws an error when the sanity checks fail."""
1505 # Patch black.assert_equivalent to ensure the sanity checks fail
1506 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1507 code = 'print("Hello world")'
1508 error_msg = f"{code}\nerror: cannot format <string>: \n"
1510 args = ["--safe", "--code", code]
1511 result = CliRunner().invoke(black.main, args)
1513 self.compare_results(result, error_msg, 123)
1515 def test_code_option_fast(self) -> None:
1516 """Test that the code option ignores errors when the sanity checks fail."""
1517 # Patch black.assert_equivalent to ensure the sanity checks fail
1518 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1519 code = 'print("Hello world")'
1520 formatted = black.format_str(code, mode=DEFAULT_MODE)
1522 args = ["--fast", "--code", code]
1523 result = CliRunner().invoke(black.main, args)
1525 self.compare_results(result, formatted, 0)
1527 @pytest.mark.incompatible_with_mypyc
1528 def test_code_option_config(self) -> None:
1530 Test that the code option finds the pyproject.toml in the current directory.
1532 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1533 args = ["--code", "print"]
1534 # This is the only directory known to contain a pyproject.toml
1535 with change_directory(PROJECT_ROOT):
1536 CliRunner().invoke(black.main, args)
1537 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1540 len(parse.mock_calls) >= 1
1541 ), "Expected config parse to be called with the current directory."
1543 _, call_args, _ = parse.mock_calls[0]
1545 call_args[0].lower() == str(pyproject_path).lower()
1546 ), "Incorrect config loaded."
1548 @pytest.mark.incompatible_with_mypyc
1549 def test_code_option_parent_config(self) -> None:
1551 Test that the code option finds the pyproject.toml in the parent directory.
1553 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1554 with change_directory(THIS_DIR):
1555 args = ["--code", "print"]
1556 CliRunner().invoke(black.main, args)
1558 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1560 len(parse.mock_calls) >= 1
1561 ), "Expected config parse to be called with the current directory."
1563 _, call_args, _ = parse.mock_calls[0]
1565 call_args[0].lower() == str(pyproject_path).lower()
1566 ), "Incorrect config loaded."
1568 def test_for_handled_unexpected_eof_error(self) -> None:
1570 Test that an unexpected EOF SyntaxError is nicely presented.
1572 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1573 black.lib2to3_parse("print(", {})
1575 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1577 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1578 with pytest.raises(AssertionError) as err:
1579 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1582 # Unfortunately the SyntaxError message has changed in newer versions so we
1583 # can't match it directly.
1584 err.match("invalid character")
1585 err.match(r"\(<unknown>, line 1\)")
1589 def test_cache_broken_file(self) -> None:
1591 with cache_dir() as workspace:
1592 cache_file = get_cache_file(mode)
1593 cache_file.write_text("this is not a pickle")
1594 assert black.read_cache(mode) == {}
1595 src = (workspace / "test.py").resolve()
1596 src.write_text("print('hello')")
1597 invokeBlack([str(src)])
1598 cache = black.read_cache(mode)
1599 assert str(src) in cache
1601 def test_cache_single_file_already_cached(self) -> None:
1603 with cache_dir() as workspace:
1604 src = (workspace / "test.py").resolve()
1605 src.write_text("print('hello')")
1606 black.write_cache({}, [src], mode)
1607 invokeBlack([str(src)])
1608 assert src.read_text() == "print('hello')"
1611 def test_cache_multiple_files(self) -> None:
1613 with cache_dir() as workspace, patch(
1614 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1616 one = (workspace / "one.py").resolve()
1617 with one.open("w") as fobj:
1618 fobj.write("print('hello')")
1619 two = (workspace / "two.py").resolve()
1620 with two.open("w") as fobj:
1621 fobj.write("print('hello')")
1622 black.write_cache({}, [one], mode)
1623 invokeBlack([str(workspace)])
1624 with one.open("r") as fobj:
1625 assert fobj.read() == "print('hello')"
1626 with two.open("r") as fobj:
1627 assert fobj.read() == 'print("hello")\n'
1628 cache = black.read_cache(mode)
1629 assert str(one) in cache
1630 assert str(two) in cache
1632 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1633 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1635 with cache_dir() as workspace:
1636 src = (workspace / "test.py").resolve()
1637 with src.open("w") as fobj:
1638 fobj.write("print('hello')")
1639 with patch("black.read_cache") as read_cache, patch(
1642 cmd = [str(src), "--diff"]
1644 cmd.append("--color")
1646 cache_file = get_cache_file(mode)
1647 assert cache_file.exists() is False
1648 write_cache.assert_not_called()
1649 read_cache.assert_not_called()
1651 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1653 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1654 with cache_dir() as workspace:
1655 for tag in range(0, 4):
1656 src = (workspace / f"test{tag}.py").resolve()
1657 with src.open("w") as fobj:
1658 fobj.write("print('hello')")
1659 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1660 cmd = ["--diff", str(workspace)]
1662 cmd.append("--color")
1663 invokeBlack(cmd, exit_code=0)
1664 # this isn't quite doing what we want, but if it _isn't_
1665 # called then we cannot be using the lock it provides
1668 def test_no_cache_when_stdin(self) -> None:
1671 result = CliRunner().invoke(
1672 black.main, ["-"], input=BytesIO(b"print('hello')")
1674 assert not result.exit_code
1675 cache_file = get_cache_file(mode)
1676 assert not cache_file.exists()
1678 def test_read_cache_no_cachefile(self) -> None:
1681 assert black.read_cache(mode) == {}
1683 def test_write_cache_read_cache(self) -> None:
1685 with cache_dir() as workspace:
1686 src = (workspace / "test.py").resolve()
1688 black.write_cache({}, [src], mode)
1689 cache = black.read_cache(mode)
1690 assert str(src) in cache
1691 assert cache[str(src)] == black.get_cache_info(src)
1693 def test_filter_cached(self) -> None:
1694 with TemporaryDirectory() as workspace:
1695 path = Path(workspace)
1696 uncached = (path / "uncached").resolve()
1697 cached = (path / "cached").resolve()
1698 cached_but_changed = (path / "changed").resolve()
1701 cached_but_changed.touch()
1703 str(cached): black.get_cache_info(cached),
1704 str(cached_but_changed): (0.0, 0),
1706 todo, done = black.filter_cached(
1707 cache, {uncached, cached, cached_but_changed}
1709 assert todo == {uncached, cached_but_changed}
1710 assert done == {cached}
1712 def test_write_cache_creates_directory_if_needed(self) -> None:
1714 with cache_dir(exists=False) as workspace:
1715 assert not workspace.exists()
1716 black.write_cache({}, [], mode)
1717 assert workspace.exists()
1720 def test_failed_formatting_does_not_get_cached(self) -> None:
1722 with cache_dir() as workspace, patch(
1723 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1725 failing = (workspace / "failing.py").resolve()
1726 with failing.open("w") as fobj:
1727 fobj.write("not actually python")
1728 clean = (workspace / "clean.py").resolve()
1729 with clean.open("w") as fobj:
1730 fobj.write('print("hello")\n')
1731 invokeBlack([str(workspace)], exit_code=123)
1732 cache = black.read_cache(mode)
1733 assert str(failing) not in cache
1734 assert str(clean) in cache
1736 def test_write_cache_write_fail(self) -> None:
1738 with cache_dir(), patch.object(Path, "open") as mock:
1739 mock.side_effect = OSError
1740 black.write_cache({}, [], mode)
1742 def test_read_cache_line_lengths(self) -> None:
1744 short_mode = replace(DEFAULT_MODE, line_length=1)
1745 with cache_dir() as workspace:
1746 path = (workspace / "file.py").resolve()
1748 black.write_cache({}, [path], mode)
1749 one = black.read_cache(mode)
1750 assert str(path) in one
1751 two = black.read_cache(short_mode)
1752 assert str(path) not in two
1755 def assert_collected_sources(
1756 src: Sequence[Union[str, Path]],
1757 expected: Sequence[Union[str, Path]],
1759 exclude: Optional[str] = None,
1760 include: Optional[str] = None,
1761 extend_exclude: Optional[str] = None,
1762 force_exclude: Optional[str] = None,
1763 stdin_filename: Optional[str] = None,
1765 gs_src = tuple(str(Path(s)) for s in src)
1766 gs_expected = [Path(s) for s in expected]
1767 gs_exclude = None if exclude is None else compile_pattern(exclude)
1768 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
1769 gs_extend_exclude = (
1770 None if extend_exclude is None else compile_pattern(extend_exclude)
1772 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
1773 collected = black.get_sources(
1780 extend_exclude=gs_extend_exclude,
1781 force_exclude=gs_force_exclude,
1782 report=black.Report(),
1783 stdin_filename=stdin_filename,
1785 assert sorted(collected) == sorted(gs_expected)
1788 class TestFileCollection:
1789 def test_include_exclude(self) -> None:
1790 path = THIS_DIR / "data" / "include_exclude_tests"
1793 Path(path / "b/dont_exclude/a.py"),
1794 Path(path / "b/dont_exclude/a.pyi"),
1796 assert_collected_sources(
1800 exclude=r"/exclude/|/\.definitely_exclude/",
1803 def test_gitignore_used_as_default(self) -> None:
1804 base = Path(DATA_DIR / "include_exclude_tests")
1806 base / "b/.definitely_exclude/a.py",
1807 base / "b/.definitely_exclude/a.pyi",
1810 assert_collected_sources(src, expected, extend_exclude=r"/exclude/")
1812 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1813 def test_exclude_for_issue_1572(self) -> None:
1814 # Exclude shouldn't touch files that were explicitly given to Black through the
1815 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1816 # https://github.com/psf/black/issues/1572
1817 path = DATA_DIR / "include_exclude_tests"
1818 src = [path / "b/exclude/a.py"]
1819 expected = [path / "b/exclude/a.py"]
1820 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1822 def test_gitignore_exclude(self) -> None:
1823 path = THIS_DIR / "data" / "include_exclude_tests"
1824 include = re.compile(r"\.pyi?$")
1825 exclude = re.compile(r"")
1826 report = black.Report()
1827 gitignore = PathSpec.from_lines(
1828 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1830 sources: List[Path] = []
1832 Path(path / "b/dont_exclude/a.py"),
1833 Path(path / "b/dont_exclude/a.pyi"),
1835 this_abs = THIS_DIR.resolve()
1837 black.gen_python_files(
1850 assert sorted(expected) == sorted(sources)
1852 def test_nested_gitignore(self) -> None:
1853 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
1854 include = re.compile(r"\.pyi?$")
1855 exclude = re.compile(r"")
1856 root_gitignore = black.files.get_gitignore(path)
1857 report = black.Report()
1858 expected: List[Path] = [
1859 Path(path / "x.py"),
1860 Path(path / "root/b.py"),
1861 Path(path / "root/c.py"),
1862 Path(path / "root/child/c.py"),
1864 this_abs = THIS_DIR.resolve()
1866 black.gen_python_files(
1879 assert sorted(expected) == sorted(sources)
1881 def test_invalid_gitignore(self) -> None:
1882 path = THIS_DIR / "data" / "invalid_gitignore_tests"
1883 empty_config = path / "pyproject.toml"
1884 result = BlackRunner().invoke(
1885 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1887 assert result.exit_code == 1
1888 assert result.stderr_bytes is not None
1890 gitignore = path / ".gitignore"
1891 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1893 def test_invalid_nested_gitignore(self) -> None:
1894 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
1895 empty_config = path / "pyproject.toml"
1896 result = BlackRunner().invoke(
1897 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1899 assert result.exit_code == 1
1900 assert result.stderr_bytes is not None
1902 gitignore = path / "a" / ".gitignore"
1903 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1905 def test_empty_include(self) -> None:
1906 path = DATA_DIR / "include_exclude_tests"
1909 Path(path / "b/exclude/a.pie"),
1910 Path(path / "b/exclude/a.py"),
1911 Path(path / "b/exclude/a.pyi"),
1912 Path(path / "b/dont_exclude/a.pie"),
1913 Path(path / "b/dont_exclude/a.py"),
1914 Path(path / "b/dont_exclude/a.pyi"),
1915 Path(path / "b/.definitely_exclude/a.pie"),
1916 Path(path / "b/.definitely_exclude/a.py"),
1917 Path(path / "b/.definitely_exclude/a.pyi"),
1918 Path(path / ".gitignore"),
1919 Path(path / "pyproject.toml"),
1921 # Setting exclude explicitly to an empty string to block .gitignore usage.
1922 assert_collected_sources(src, expected, include="", exclude="")
1924 def test_extend_exclude(self) -> None:
1925 path = DATA_DIR / "include_exclude_tests"
1928 Path(path / "b/exclude/a.py"),
1929 Path(path / "b/dont_exclude/a.py"),
1931 assert_collected_sources(
1932 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
1935 @pytest.mark.incompatible_with_mypyc
1936 def test_symlink_out_of_root_directory(self) -> None:
1938 root = THIS_DIR.resolve()
1940 include = re.compile(black.DEFAULT_INCLUDES)
1941 exclude = re.compile(black.DEFAULT_EXCLUDES)
1942 report = black.Report()
1943 gitignore = PathSpec.from_lines("gitwildmatch", [])
1944 # `child` should behave like a symlink which resolved path is clearly
1945 # outside of the `root` directory.
1946 path.iterdir.return_value = [child]
1947 child.resolve.return_value = Path("/a/b/c")
1948 child.as_posix.return_value = "/a/b/c"
1949 child.is_symlink.return_value = True
1952 black.gen_python_files(
1965 except ValueError as ve:
1966 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
1967 path.iterdir.assert_called_once()
1968 child.resolve.assert_called_once()
1969 child.is_symlink.assert_called_once()
1970 # `child` should behave like a strange file which resolved path is clearly
1971 # outside of the `root` directory.
1972 child.is_symlink.return_value = False
1973 with pytest.raises(ValueError):
1975 black.gen_python_files(
1988 path.iterdir.assert_called()
1989 assert path.iterdir.call_count == 2
1990 child.resolve.assert_called()
1991 assert child.resolve.call_count == 2
1992 child.is_symlink.assert_called()
1993 assert child.is_symlink.call_count == 2
1995 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1996 def test_get_sources_with_stdin(self) -> None:
1999 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2001 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
2002 def test_get_sources_with_stdin_filename(self) -> None:
2004 stdin_filename = str(THIS_DIR / "data/collections.py")
2005 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2006 assert_collected_sources(
2009 exclude=r"/exclude/a\.py",
2010 stdin_filename=stdin_filename,
2013 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
2014 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2015 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2016 # file being passed directly. This is the same as
2017 # test_exclude_for_issue_1572
2018 path = DATA_DIR / "include_exclude_tests"
2020 stdin_filename = str(path / "b/exclude/a.py")
2021 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2022 assert_collected_sources(
2025 exclude=r"/exclude/|a\.py",
2026 stdin_filename=stdin_filename,
2029 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
2030 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2031 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2032 # file being passed directly. This is the same as
2033 # test_exclude_for_issue_1572
2035 path = THIS_DIR / "data" / "include_exclude_tests"
2036 stdin_filename = str(path / "b/exclude/a.py")
2037 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2038 assert_collected_sources(
2041 extend_exclude=r"/exclude/|a\.py",
2042 stdin_filename=stdin_filename,
2045 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
2046 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2047 # Force exclude should exclude the file when passing it through
2049 path = THIS_DIR / "data" / "include_exclude_tests"
2050 stdin_filename = str(path / "b/exclude/a.py")
2051 assert_collected_sources(
2054 force_exclude=r"/exclude/|a\.py",
2055 stdin_filename=stdin_filename,
2060 with open(black.__file__, "r", encoding="utf-8") as _bf:
2061 black_source_lines = _bf.readlines()
2062 except UnicodeDecodeError:
2063 if not black.COMPILED:
2068 frame: types.FrameType, event: str, arg: Any
2069 ) -> Callable[[types.FrameType, str, Any], Any]:
2070 """Show function calls `from black/__init__.py` as they happen.
2072 Register this with `sys.settrace()` in a test you're debugging.
2077 stack = len(inspect.stack()) - 19
2079 filename = frame.f_code.co_filename
2080 lineno = frame.f_lineno
2081 func_sig_lineno = lineno - 1
2082 funcname = black_source_lines[func_sig_lineno].strip()
2083 while funcname.startswith("@"):
2084 func_sig_lineno += 1
2085 funcname = black_source_lines[func_sig_lineno].strip()
2086 if "black/__init__.py" in filename:
2087 print(f"{' ' * stack}{lineno}:{funcname}")