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, redirect_stderr
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}
797 node = black.lib2to3_parse("try: pass\nexcept Something: pass")
798 self.assertEqual(black.get_features_used(node), set())
799 node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass")
800 self.assertEqual(black.get_features_used(node), set())
801 node = black.lib2to3_parse("try: pass\nexcept *Group: pass")
802 self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR})
804 def test_get_features_used_for_future_flags(self) -> None:
805 for src, features in [
806 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
808 "from __future__ import (other, annotations)",
809 {Feature.FUTURE_ANNOTATIONS},
811 ("a = 1 + 2\nfrom something import annotations", set()),
812 ("from __future__ import x, y", set()),
814 with self.subTest(src=src, features=features):
815 node = black.lib2to3_parse(src)
816 future_imports = black.get_future_imports(node)
818 black.get_features_used(node, future_imports=future_imports),
822 def test_get_future_imports(self) -> None:
823 node = black.lib2to3_parse("\n")
824 self.assertEqual(set(), black.get_future_imports(node))
825 node = black.lib2to3_parse("from __future__ import black\n")
826 self.assertEqual({"black"}, black.get_future_imports(node))
827 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
828 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
829 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
830 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
831 node = black.lib2to3_parse(
832 "from __future__ import multiple\nfrom __future__ import imports\n"
834 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
835 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
836 self.assertEqual({"black"}, black.get_future_imports(node))
837 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
838 self.assertEqual({"black"}, black.get_future_imports(node))
839 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
840 self.assertEqual(set(), black.get_future_imports(node))
841 node = black.lib2to3_parse("from some.module import black\n")
842 self.assertEqual(set(), black.get_future_imports(node))
843 node = black.lib2to3_parse(
844 "from __future__ import unicode_literals as _unicode_literals"
846 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
847 node = black.lib2to3_parse(
848 "from __future__ import unicode_literals as _lol, print"
850 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
852 @pytest.mark.incompatible_with_mypyc
853 def test_debug_visitor(self) -> None:
854 source, _ = read_data("debug_visitor.py")
855 expected, _ = read_data("debug_visitor.out")
859 def out(msg: str, **kwargs: Any) -> None:
860 out_lines.append(msg)
862 def err(msg: str, **kwargs: Any) -> None:
863 err_lines.append(msg)
865 with patch("black.debug.out", out):
866 DebugVisitor.show(source)
867 actual = "\n".join(out_lines) + "\n"
869 if expected != actual:
870 log_name = black.dump_to_file(*out_lines)
874 f"AST print out is different. Actual version dumped to {log_name}",
877 def test_format_file_contents(self) -> None:
880 with self.assertRaises(black.NothingChanged):
881 black.format_file_contents(empty, mode=mode, fast=False)
883 with self.assertRaises(black.NothingChanged):
884 black.format_file_contents(just_nl, mode=mode, fast=False)
885 same = "j = [1, 2, 3]\n"
886 with self.assertRaises(black.NothingChanged):
887 black.format_file_contents(same, mode=mode, fast=False)
888 different = "j = [1,2,3]"
890 actual = black.format_file_contents(different, mode=mode, fast=False)
891 self.assertEqual(expected, actual)
892 invalid = "return if you can"
893 with self.assertRaises(black.InvalidInput) as e:
894 black.format_file_contents(invalid, mode=mode, fast=False)
895 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
897 def test_endmarker(self) -> None:
898 n = black.lib2to3_parse("\n")
899 self.assertEqual(n.type, black.syms.file_input)
900 self.assertEqual(len(n.children), 1)
901 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
903 @pytest.mark.incompatible_with_mypyc
904 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
905 def test_assertFormatEqual(self) -> None:
909 def out(msg: str, **kwargs: Any) -> None:
910 out_lines.append(msg)
912 def err(msg: str, **kwargs: Any) -> None:
913 err_lines.append(msg)
915 with patch("black.output._out", out), patch("black.output._err", err):
916 with self.assertRaises(AssertionError):
917 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
919 out_str = "".join(out_lines)
920 self.assertIn("Expected tree:", out_str)
921 self.assertIn("Actual tree:", out_str)
922 self.assertEqual("".join(err_lines), "")
925 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
926 def test_works_in_mono_process_only_environment(self) -> None:
927 with cache_dir() as workspace:
929 (workspace / "one.py").resolve(),
930 (workspace / "two.py").resolve(),
932 f.write_text('print("hello")\n')
933 self.invokeBlack([str(workspace)])
936 def test_check_diff_use_together(self) -> None:
938 # Files which will be reformatted.
939 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
940 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
941 # Files which will not be reformatted.
942 src2 = (THIS_DIR / "data" / "composition.py").resolve()
943 self.invokeBlack([str(src2), "--diff", "--check"])
944 # Multi file command.
945 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
947 def test_no_src_fails(self) -> None:
949 self.invokeBlack([], exit_code=1)
951 def test_src_and_code_fails(self) -> None:
953 self.invokeBlack([".", "-c", "0"], exit_code=1)
955 def test_broken_symlink(self) -> None:
956 with cache_dir() as workspace:
957 symlink = workspace / "broken_link.py"
959 symlink.symlink_to("nonexistent.py")
960 except (OSError, NotImplementedError) as e:
961 self.skipTest(f"Can't create symlinks: {e}")
962 self.invokeBlack([str(workspace.resolve())])
964 def test_single_file_force_pyi(self) -> None:
965 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
966 contents, expected = read_data("force_pyi")
967 with cache_dir() as workspace:
968 path = (workspace / "file.py").resolve()
969 with open(path, "w") as fh:
971 self.invokeBlack([str(path), "--pyi"])
972 with open(path, "r") as fh:
974 # verify cache with --pyi is separate
975 pyi_cache = black.read_cache(pyi_mode)
976 self.assertIn(str(path), pyi_cache)
977 normal_cache = black.read_cache(DEFAULT_MODE)
978 self.assertNotIn(str(path), normal_cache)
979 self.assertFormatEqual(expected, actual)
980 black.assert_equivalent(contents, actual)
981 black.assert_stable(contents, actual, pyi_mode)
984 def test_multi_file_force_pyi(self) -> None:
985 reg_mode = DEFAULT_MODE
986 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
987 contents, expected = read_data("force_pyi")
988 with cache_dir() as workspace:
990 (workspace / "file1.py").resolve(),
991 (workspace / "file2.py").resolve(),
994 with open(path, "w") as fh:
996 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
998 with open(path, "r") as fh:
1000 self.assertEqual(actual, expected)
1001 # verify cache with --pyi is separate
1002 pyi_cache = black.read_cache(pyi_mode)
1003 normal_cache = black.read_cache(reg_mode)
1005 self.assertIn(str(path), pyi_cache)
1006 self.assertNotIn(str(path), normal_cache)
1008 def test_pipe_force_pyi(self) -> None:
1009 source, expected = read_data("force_pyi")
1010 result = CliRunner().invoke(
1011 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1013 self.assertEqual(result.exit_code, 0)
1014 actual = result.output
1015 self.assertFormatEqual(actual, expected)
1017 def test_single_file_force_py36(self) -> None:
1018 reg_mode = DEFAULT_MODE
1019 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1020 source, expected = read_data("force_py36")
1021 with cache_dir() as workspace:
1022 path = (workspace / "file.py").resolve()
1023 with open(path, "w") as fh:
1025 self.invokeBlack([str(path), *PY36_ARGS])
1026 with open(path, "r") as fh:
1028 # verify cache with --target-version is separate
1029 py36_cache = black.read_cache(py36_mode)
1030 self.assertIn(str(path), py36_cache)
1031 normal_cache = black.read_cache(reg_mode)
1032 self.assertNotIn(str(path), normal_cache)
1033 self.assertEqual(actual, expected)
1036 def test_multi_file_force_py36(self) -> None:
1037 reg_mode = DEFAULT_MODE
1038 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1039 source, expected = read_data("force_py36")
1040 with cache_dir() as workspace:
1042 (workspace / "file1.py").resolve(),
1043 (workspace / "file2.py").resolve(),
1046 with open(path, "w") as fh:
1048 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1050 with open(path, "r") as fh:
1052 self.assertEqual(actual, expected)
1053 # verify cache with --target-version is separate
1054 pyi_cache = black.read_cache(py36_mode)
1055 normal_cache = black.read_cache(reg_mode)
1057 self.assertIn(str(path), pyi_cache)
1058 self.assertNotIn(str(path), normal_cache)
1060 def test_pipe_force_py36(self) -> None:
1061 source, expected = read_data("force_py36")
1062 result = CliRunner().invoke(
1064 ["-", "-q", "--target-version=py36"],
1065 input=BytesIO(source.encode("utf8")),
1067 self.assertEqual(result.exit_code, 0)
1068 actual = result.output
1069 self.assertFormatEqual(actual, expected)
1071 @pytest.mark.incompatible_with_mypyc
1072 def test_reformat_one_with_stdin(self) -> None:
1074 "black.format_stdin_to_stdout",
1075 return_value=lambda *args, **kwargs: black.Changed.YES,
1077 report = MagicMock()
1082 write_back=black.WriteBack.YES,
1086 fsts.assert_called_once()
1087 report.done.assert_called_with(path, black.Changed.YES)
1089 @pytest.mark.incompatible_with_mypyc
1090 def test_reformat_one_with_stdin_filename(self) -> None:
1092 "black.format_stdin_to_stdout",
1093 return_value=lambda *args, **kwargs: black.Changed.YES,
1095 report = MagicMock()
1097 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1102 write_back=black.WriteBack.YES,
1106 fsts.assert_called_once_with(
1107 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1109 # __BLACK_STDIN_FILENAME__ should have been stripped
1110 report.done.assert_called_with(expected, black.Changed.YES)
1112 @pytest.mark.incompatible_with_mypyc
1113 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1115 "black.format_stdin_to_stdout",
1116 return_value=lambda *args, **kwargs: black.Changed.YES,
1118 report = MagicMock()
1120 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1125 write_back=black.WriteBack.YES,
1129 fsts.assert_called_once_with(
1131 write_back=black.WriteBack.YES,
1132 mode=replace(DEFAULT_MODE, is_pyi=True),
1134 # __BLACK_STDIN_FILENAME__ should have been stripped
1135 report.done.assert_called_with(expected, black.Changed.YES)
1137 @pytest.mark.incompatible_with_mypyc
1138 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1140 "black.format_stdin_to_stdout",
1141 return_value=lambda *args, **kwargs: black.Changed.YES,
1143 report = MagicMock()
1145 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1150 write_back=black.WriteBack.YES,
1154 fsts.assert_called_once_with(
1156 write_back=black.WriteBack.YES,
1157 mode=replace(DEFAULT_MODE, is_ipynb=True),
1159 # __BLACK_STDIN_FILENAME__ should have been stripped
1160 report.done.assert_called_with(expected, black.Changed.YES)
1162 @pytest.mark.incompatible_with_mypyc
1163 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1165 "black.format_stdin_to_stdout",
1166 return_value=lambda *args, **kwargs: black.Changed.YES,
1168 report = MagicMock()
1169 # Even with an existing file, since we are forcing stdin, black
1170 # should output to stdout and not modify the file inplace
1171 p = Path(str(THIS_DIR / "data/collections.py"))
1172 # Make sure is_file actually returns True
1173 self.assertTrue(p.is_file())
1174 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1179 write_back=black.WriteBack.YES,
1183 fsts.assert_called_once()
1184 # __BLACK_STDIN_FILENAME__ should have been stripped
1185 report.done.assert_called_with(expected, black.Changed.YES)
1187 def test_reformat_one_with_stdin_empty(self) -> None:
1188 output = io.StringIO()
1189 with patch("io.TextIOWrapper", lambda *args, **kwargs: output):
1191 black.format_stdin_to_stdout(
1194 write_back=black.WriteBack.YES,
1197 except io.UnsupportedOperation:
1198 pass # StringIO does not support detach
1199 assert output.getvalue() == ""
1201 def test_invalid_cli_regex(self) -> None:
1202 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1203 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1205 def test_required_version_matches_version(self) -> None:
1207 ["--required-version", black.__version__, "-c", "0"],
1212 def test_required_version_matches_partial_version(self) -> None:
1214 ["--required-version", black.__version__.split(".")[0], "-c", "0"],
1219 def test_required_version_does_not_match_on_minor_version(self) -> None:
1221 ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
1226 def test_required_version_does_not_match_version(self) -> None:
1227 result = BlackRunner().invoke(
1229 ["--required-version", "20.99b", "-c", "0"],
1231 self.assertEqual(result.exit_code, 1)
1232 self.assertIn("required version", result.stderr)
1234 def test_preserves_line_endings(self) -> None:
1235 with TemporaryDirectory() as workspace:
1236 test_file = Path(workspace) / "test.py"
1237 for nl in ["\n", "\r\n"]:
1238 contents = nl.join(["def f( ):", " pass"])
1239 test_file.write_bytes(contents.encode())
1240 ff(test_file, write_back=black.WriteBack.YES)
1241 updated_contents: bytes = test_file.read_bytes()
1242 self.assertIn(nl.encode(), updated_contents)
1244 self.assertNotIn(b"\r\n", updated_contents)
1246 def test_preserves_line_endings_via_stdin(self) -> None:
1247 for nl in ["\n", "\r\n"]:
1248 contents = nl.join(["def f( ):", " pass"])
1249 runner = BlackRunner()
1250 result = runner.invoke(
1251 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1253 self.assertEqual(result.exit_code, 0)
1254 output = result.stdout_bytes
1255 self.assertIn(nl.encode("utf8"), output)
1257 self.assertNotIn(b"\r\n", output)
1259 def test_assert_equivalent_different_asts(self) -> None:
1260 with self.assertRaises(AssertionError):
1261 black.assert_equivalent("{}", "None")
1263 def test_shhh_click(self) -> None:
1265 from click import _unicodefun # type: ignore
1267 self.skipTest("Incompatible Click version")
1269 if not hasattr(_unicodefun, "_verify_python_env"):
1270 self.skipTest("Incompatible Click version")
1272 # First, let's see if Click is crashing with a preferred ASCII charset.
1273 with patch("locale.getpreferredencoding") as gpe:
1274 gpe.return_value = "ASCII"
1275 with self.assertRaises(RuntimeError):
1276 _unicodefun._verify_python_env()
1277 # Now, let's silence Click...
1279 # ...and confirm it's silent.
1280 with patch("locale.getpreferredencoding") as gpe:
1281 gpe.return_value = "ASCII"
1283 _unicodefun._verify_python_env()
1284 except RuntimeError as re:
1285 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1287 def test_root_logger_not_used_directly(self) -> None:
1288 def fail(*args: Any, **kwargs: Any) -> None:
1289 self.fail("Record created with root logger")
1291 with patch.multiple(
1300 ff(THIS_DIR / "util.py")
1302 def test_invalid_config_return_code(self) -> None:
1303 tmp_file = Path(black.dump_to_file())
1305 tmp_config = Path(black.dump_to_file())
1307 args = ["--config", str(tmp_config), str(tmp_file)]
1308 self.invokeBlack(args, exit_code=2, ignore_config=False)
1312 def test_parse_pyproject_toml(self) -> None:
1313 test_toml_file = THIS_DIR / "test.toml"
1314 config = black.parse_pyproject_toml(str(test_toml_file))
1315 self.assertEqual(config["verbose"], 1)
1316 self.assertEqual(config["check"], "no")
1317 self.assertEqual(config["diff"], "y")
1318 self.assertEqual(config["color"], True)
1319 self.assertEqual(config["line_length"], 79)
1320 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1321 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1322 self.assertEqual(config["exclude"], r"\.pyi?$")
1323 self.assertEqual(config["include"], r"\.py?$")
1325 def test_read_pyproject_toml(self) -> None:
1326 test_toml_file = THIS_DIR / "test.toml"
1327 fake_ctx = FakeContext()
1328 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1329 config = fake_ctx.default_map
1330 self.assertEqual(config["verbose"], "1")
1331 self.assertEqual(config["check"], "no")
1332 self.assertEqual(config["diff"], "y")
1333 self.assertEqual(config["color"], "True")
1334 self.assertEqual(config["line_length"], "79")
1335 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1336 self.assertEqual(config["exclude"], r"\.pyi?$")
1337 self.assertEqual(config["include"], r"\.py?$")
1339 @pytest.mark.incompatible_with_mypyc
1340 def test_find_project_root(self) -> None:
1341 with TemporaryDirectory() as workspace:
1342 root = Path(workspace)
1343 test_dir = root / "test"
1346 src_dir = root / "src"
1349 root_pyproject = root / "pyproject.toml"
1350 root_pyproject.touch()
1351 src_pyproject = src_dir / "pyproject.toml"
1352 src_pyproject.touch()
1353 src_python = src_dir / "foo.py"
1357 black.find_project_root((src_dir, test_dir)),
1358 (root.resolve(), "pyproject.toml"),
1361 black.find_project_root((src_dir,)),
1362 (src_dir.resolve(), "pyproject.toml"),
1365 black.find_project_root((src_python,)),
1366 (src_dir.resolve(), "pyproject.toml"),
1370 "black.files.find_user_pyproject_toml",
1372 def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
1373 find_user_pyproject_toml.side_effect = RuntimeError()
1375 with redirect_stderr(io.StringIO()) as stderr:
1376 result = black.files.find_pyproject_toml(
1377 path_search_start=(str(Path.cwd().root),)
1380 assert result is None
1381 err = stderr.getvalue()
1382 assert "Ignoring user configuration" in err
1385 "black.files.find_user_pyproject_toml",
1386 black.files.find_user_pyproject_toml.__wrapped__,
1388 def test_find_user_pyproject_toml_linux(self) -> None:
1389 if system() == "Windows":
1392 # Test if XDG_CONFIG_HOME is checked
1393 with TemporaryDirectory() as workspace:
1394 tmp_user_config = Path(workspace) / "black"
1395 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1397 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1400 # Test fallback for XDG_CONFIG_HOME
1401 with patch.dict("os.environ"):
1402 os.environ.pop("XDG_CONFIG_HOME", None)
1403 fallback_user_config = Path("~/.config").expanduser() / "black"
1405 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1408 def test_find_user_pyproject_toml_windows(self) -> None:
1409 if system() != "Windows":
1412 user_config_path = Path.home() / ".black"
1414 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1417 def test_bpo_33660_workaround(self) -> None:
1418 if system() == "Windows":
1421 # https://bugs.python.org/issue33660
1423 with change_directory(root):
1424 path = Path("workspace") / "project"
1425 report = black.Report(verbose=True)
1426 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1427 self.assertEqual(normalized_path, "workspace/project")
1429 def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
1430 if system() != "Windows":
1433 with TemporaryDirectory() as workspace:
1434 root = Path(workspace)
1435 junction_dir = root / "junction"
1436 junction_target_outside_of_root = root / ".."
1437 os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
1439 report = black.Report(verbose=True)
1440 normalized_path = black.normalize_path_maybe_ignore(
1441 junction_dir, root, report
1443 # Manually delete for Python < 3.8
1444 os.system(f"rmdir {junction_dir}")
1446 self.assertEqual(normalized_path, None)
1448 def test_newline_comment_interaction(self) -> None:
1449 source = "class A:\\\r\n# type: ignore\n pass\n"
1450 output = black.format_str(source, mode=DEFAULT_MODE)
1451 black.assert_stable(source, output, mode=DEFAULT_MODE)
1453 def test_bpo_2142_workaround(self) -> None:
1455 # https://bugs.python.org/issue2142
1457 source, _ = read_data("missing_final_newline.py")
1458 # read_data adds a trailing newline
1459 source = source.rstrip()
1460 expected, _ = read_data("missing_final_newline.diff")
1461 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1462 diff_header = re.compile(
1463 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1464 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1467 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1468 self.assertEqual(result.exit_code, 0)
1471 actual = result.output
1472 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1473 self.assertEqual(actual, expected)
1476 def compare_results(
1477 result: click.testing.Result, expected_value: str, expected_exit_code: int
1479 """Helper method to test the value and exit code of a click Result."""
1481 result.output == expected_value
1482 ), "The output did not match the expected value."
1483 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1485 def test_code_option(self) -> None:
1486 """Test the code option with no changes."""
1487 code = 'print("Hello world")\n'
1488 args = ["--code", code]
1489 result = CliRunner().invoke(black.main, args)
1491 self.compare_results(result, code, 0)
1493 def test_code_option_changed(self) -> None:
1494 """Test the code option when changes are required."""
1495 code = "print('hello world')"
1496 formatted = black.format_str(code, mode=DEFAULT_MODE)
1498 args = ["--code", code]
1499 result = CliRunner().invoke(black.main, args)
1501 self.compare_results(result, formatted, 0)
1503 def test_code_option_check(self) -> None:
1504 """Test the code option when check is passed."""
1505 args = ["--check", "--code", 'print("Hello world")\n']
1506 result = CliRunner().invoke(black.main, args)
1507 self.compare_results(result, "", 0)
1509 def test_code_option_check_changed(self) -> None:
1510 """Test the code option when changes are required, and check is passed."""
1511 args = ["--check", "--code", "print('hello world')"]
1512 result = CliRunner().invoke(black.main, args)
1513 self.compare_results(result, "", 1)
1515 def test_code_option_diff(self) -> None:
1516 """Test the code option when diff is passed."""
1517 code = "print('hello world')"
1518 formatted = black.format_str(code, mode=DEFAULT_MODE)
1519 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1521 args = ["--diff", "--code", code]
1522 result = CliRunner().invoke(black.main, args)
1524 # Remove time from diff
1525 output = DIFF_TIME.sub("", result.output)
1527 assert output == result_diff, "The output did not match the expected value."
1528 assert result.exit_code == 0, "The exit code is incorrect."
1530 def test_code_option_color_diff(self) -> None:
1531 """Test the code option when color and diff are passed."""
1532 code = "print('hello world')"
1533 formatted = black.format_str(code, mode=DEFAULT_MODE)
1535 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1536 result_diff = color_diff(result_diff)
1538 args = ["--diff", "--color", "--code", code]
1539 result = CliRunner().invoke(black.main, args)
1541 # Remove time from diff
1542 output = DIFF_TIME.sub("", result.output)
1544 assert output == result_diff, "The output did not match the expected value."
1545 assert result.exit_code == 0, "The exit code is incorrect."
1547 @pytest.mark.incompatible_with_mypyc
1548 def test_code_option_safe(self) -> None:
1549 """Test that the code option throws an error when the sanity checks fail."""
1550 # Patch black.assert_equivalent to ensure the sanity checks fail
1551 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1552 code = 'print("Hello world")'
1553 error_msg = f"{code}\nerror: cannot format <string>: \n"
1555 args = ["--safe", "--code", code]
1556 result = CliRunner().invoke(black.main, args)
1558 self.compare_results(result, error_msg, 123)
1560 def test_code_option_fast(self) -> None:
1561 """Test that the code option ignores errors when the sanity checks fail."""
1562 # Patch black.assert_equivalent to ensure the sanity checks fail
1563 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1564 code = 'print("Hello world")'
1565 formatted = black.format_str(code, mode=DEFAULT_MODE)
1567 args = ["--fast", "--code", code]
1568 result = CliRunner().invoke(black.main, args)
1570 self.compare_results(result, formatted, 0)
1572 @pytest.mark.incompatible_with_mypyc
1573 def test_code_option_config(self) -> None:
1575 Test that the code option finds the pyproject.toml in the current directory.
1577 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1578 args = ["--code", "print"]
1579 # This is the only directory known to contain a pyproject.toml
1580 with change_directory(PROJECT_ROOT):
1581 CliRunner().invoke(black.main, args)
1582 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1585 len(parse.mock_calls) >= 1
1586 ), "Expected config parse to be called with the current directory."
1588 _, call_args, _ = parse.mock_calls[0]
1590 call_args[0].lower() == str(pyproject_path).lower()
1591 ), "Incorrect config loaded."
1593 @pytest.mark.incompatible_with_mypyc
1594 def test_code_option_parent_config(self) -> None:
1596 Test that the code option finds the pyproject.toml in the parent directory.
1598 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1599 with change_directory(THIS_DIR):
1600 args = ["--code", "print"]
1601 CliRunner().invoke(black.main, args)
1603 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1605 len(parse.mock_calls) >= 1
1606 ), "Expected config parse to be called with the current directory."
1608 _, call_args, _ = parse.mock_calls[0]
1610 call_args[0].lower() == str(pyproject_path).lower()
1611 ), "Incorrect config loaded."
1613 def test_for_handled_unexpected_eof_error(self) -> None:
1615 Test that an unexpected EOF SyntaxError is nicely presented.
1617 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1618 black.lib2to3_parse("print(", {})
1620 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1622 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1623 with pytest.raises(AssertionError) as err:
1624 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1627 # Unfortunately the SyntaxError message has changed in newer versions so we
1628 # can't match it directly.
1629 err.match("invalid character")
1630 err.match(r"\(<unknown>, line 1\)")
1634 def test_get_cache_dir(
1637 monkeypatch: pytest.MonkeyPatch,
1639 # Create multiple cache directories
1640 workspace1 = tmp_path / "ws1"
1642 workspace2 = tmp_path / "ws2"
1645 # Force user_cache_dir to use the temporary directory for easier assertions
1646 patch_user_cache_dir = patch(
1647 target="black.cache.user_cache_dir",
1649 return_value=str(workspace1),
1652 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1653 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1654 with patch_user_cache_dir:
1655 assert get_cache_dir() == workspace1
1657 # If it is set, use the path provided in the env var.
1658 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1659 assert get_cache_dir() == workspace2
1661 def test_cache_broken_file(self) -> None:
1663 with cache_dir() as workspace:
1664 cache_file = get_cache_file(mode)
1665 cache_file.write_text("this is not a pickle")
1666 assert black.read_cache(mode) == {}
1667 src = (workspace / "test.py").resolve()
1668 src.write_text("print('hello')")
1669 invokeBlack([str(src)])
1670 cache = black.read_cache(mode)
1671 assert str(src) in cache
1673 def test_cache_single_file_already_cached(self) -> None:
1675 with cache_dir() as workspace:
1676 src = (workspace / "test.py").resolve()
1677 src.write_text("print('hello')")
1678 black.write_cache({}, [src], mode)
1679 invokeBlack([str(src)])
1680 assert src.read_text() == "print('hello')"
1683 def test_cache_multiple_files(self) -> None:
1685 with cache_dir() as workspace, patch(
1686 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1688 one = (workspace / "one.py").resolve()
1689 with one.open("w") as fobj:
1690 fobj.write("print('hello')")
1691 two = (workspace / "two.py").resolve()
1692 with two.open("w") as fobj:
1693 fobj.write("print('hello')")
1694 black.write_cache({}, [one], mode)
1695 invokeBlack([str(workspace)])
1696 with one.open("r") as fobj:
1697 assert fobj.read() == "print('hello')"
1698 with two.open("r") as fobj:
1699 assert fobj.read() == 'print("hello")\n'
1700 cache = black.read_cache(mode)
1701 assert str(one) in cache
1702 assert str(two) in cache
1704 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1705 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1707 with cache_dir() as workspace:
1708 src = (workspace / "test.py").resolve()
1709 with src.open("w") as fobj:
1710 fobj.write("print('hello')")
1711 with patch("black.read_cache") as read_cache, patch(
1714 cmd = [str(src), "--diff"]
1716 cmd.append("--color")
1718 cache_file = get_cache_file(mode)
1719 assert cache_file.exists() is False
1720 write_cache.assert_not_called()
1721 read_cache.assert_not_called()
1723 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1725 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1726 with cache_dir() as workspace:
1727 for tag in range(0, 4):
1728 src = (workspace / f"test{tag}.py").resolve()
1729 with src.open("w") as fobj:
1730 fobj.write("print('hello')")
1731 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1732 cmd = ["--diff", str(workspace)]
1734 cmd.append("--color")
1735 invokeBlack(cmd, exit_code=0)
1736 # this isn't quite doing what we want, but if it _isn't_
1737 # called then we cannot be using the lock it provides
1740 def test_no_cache_when_stdin(self) -> None:
1743 result = CliRunner().invoke(
1744 black.main, ["-"], input=BytesIO(b"print('hello')")
1746 assert not result.exit_code
1747 cache_file = get_cache_file(mode)
1748 assert not cache_file.exists()
1750 def test_read_cache_no_cachefile(self) -> None:
1753 assert black.read_cache(mode) == {}
1755 def test_write_cache_read_cache(self) -> None:
1757 with cache_dir() as workspace:
1758 src = (workspace / "test.py").resolve()
1760 black.write_cache({}, [src], mode)
1761 cache = black.read_cache(mode)
1762 assert str(src) in cache
1763 assert cache[str(src)] == black.get_cache_info(src)
1765 def test_filter_cached(self) -> None:
1766 with TemporaryDirectory() as workspace:
1767 path = Path(workspace)
1768 uncached = (path / "uncached").resolve()
1769 cached = (path / "cached").resolve()
1770 cached_but_changed = (path / "changed").resolve()
1773 cached_but_changed.touch()
1775 str(cached): black.get_cache_info(cached),
1776 str(cached_but_changed): (0.0, 0),
1778 todo, done = black.filter_cached(
1779 cache, {uncached, cached, cached_but_changed}
1781 assert todo == {uncached, cached_but_changed}
1782 assert done == {cached}
1784 def test_write_cache_creates_directory_if_needed(self) -> None:
1786 with cache_dir(exists=False) as workspace:
1787 assert not workspace.exists()
1788 black.write_cache({}, [], mode)
1789 assert workspace.exists()
1792 def test_failed_formatting_does_not_get_cached(self) -> None:
1794 with cache_dir() as workspace, patch(
1795 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1797 failing = (workspace / "failing.py").resolve()
1798 with failing.open("w") as fobj:
1799 fobj.write("not actually python")
1800 clean = (workspace / "clean.py").resolve()
1801 with clean.open("w") as fobj:
1802 fobj.write('print("hello")\n')
1803 invokeBlack([str(workspace)], exit_code=123)
1804 cache = black.read_cache(mode)
1805 assert str(failing) not in cache
1806 assert str(clean) in cache
1808 def test_write_cache_write_fail(self) -> None:
1810 with cache_dir(), patch.object(Path, "open") as mock:
1811 mock.side_effect = OSError
1812 black.write_cache({}, [], mode)
1814 def test_read_cache_line_lengths(self) -> None:
1816 short_mode = replace(DEFAULT_MODE, line_length=1)
1817 with cache_dir() as workspace:
1818 path = (workspace / "file.py").resolve()
1820 black.write_cache({}, [path], mode)
1821 one = black.read_cache(mode)
1822 assert str(path) in one
1823 two = black.read_cache(short_mode)
1824 assert str(path) not in two
1827 def assert_collected_sources(
1828 src: Sequence[Union[str, Path]],
1829 expected: Sequence[Union[str, Path]],
1831 ctx: Optional[FakeContext] = None,
1832 exclude: Optional[str] = None,
1833 include: Optional[str] = None,
1834 extend_exclude: Optional[str] = None,
1835 force_exclude: Optional[str] = None,
1836 stdin_filename: Optional[str] = None,
1838 gs_src = tuple(str(Path(s)) for s in src)
1839 gs_expected = [Path(s) for s in expected]
1840 gs_exclude = None if exclude is None else compile_pattern(exclude)
1841 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
1842 gs_extend_exclude = (
1843 None if extend_exclude is None else compile_pattern(extend_exclude)
1845 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
1846 collected = black.get_sources(
1847 ctx=ctx or FakeContext(),
1853 extend_exclude=gs_extend_exclude,
1854 force_exclude=gs_force_exclude,
1855 report=black.Report(),
1856 stdin_filename=stdin_filename,
1858 assert sorted(collected) == sorted(gs_expected)
1861 class TestFileCollection:
1862 def test_include_exclude(self) -> None:
1863 path = THIS_DIR / "data" / "include_exclude_tests"
1866 Path(path / "b/dont_exclude/a.py"),
1867 Path(path / "b/dont_exclude/a.pyi"),
1869 assert_collected_sources(
1873 exclude=r"/exclude/|/\.definitely_exclude/",
1876 def test_gitignore_used_as_default(self) -> None:
1877 base = Path(DATA_DIR / "include_exclude_tests")
1879 base / "b/.definitely_exclude/a.py",
1880 base / "b/.definitely_exclude/a.pyi",
1884 ctx.obj["root"] = base
1885 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
1887 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
1888 def test_exclude_for_issue_1572(self) -> None:
1889 # Exclude shouldn't touch files that were explicitly given to Black through the
1890 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1891 # https://github.com/psf/black/issues/1572
1892 path = DATA_DIR / "include_exclude_tests"
1893 src = [path / "b/exclude/a.py"]
1894 expected = [path / "b/exclude/a.py"]
1895 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1897 def test_gitignore_exclude(self) -> None:
1898 path = THIS_DIR / "data" / "include_exclude_tests"
1899 include = re.compile(r"\.pyi?$")
1900 exclude = re.compile(r"")
1901 report = black.Report()
1902 gitignore = PathSpec.from_lines(
1903 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1905 sources: List[Path] = []
1907 Path(path / "b/dont_exclude/a.py"),
1908 Path(path / "b/dont_exclude/a.pyi"),
1910 this_abs = THIS_DIR.resolve()
1912 black.gen_python_files(
1925 assert sorted(expected) == sorted(sources)
1927 def test_nested_gitignore(self) -> None:
1928 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
1929 include = re.compile(r"\.pyi?$")
1930 exclude = re.compile(r"")
1931 root_gitignore = black.files.get_gitignore(path)
1932 report = black.Report()
1933 expected: List[Path] = [
1934 Path(path / "x.py"),
1935 Path(path / "root/b.py"),
1936 Path(path / "root/c.py"),
1937 Path(path / "root/child/c.py"),
1939 this_abs = THIS_DIR.resolve()
1941 black.gen_python_files(
1954 assert sorted(expected) == sorted(sources)
1956 def test_invalid_gitignore(self) -> None:
1957 path = THIS_DIR / "data" / "invalid_gitignore_tests"
1958 empty_config = path / "pyproject.toml"
1959 result = BlackRunner().invoke(
1960 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1962 assert result.exit_code == 1
1963 assert result.stderr_bytes is not None
1965 gitignore = path / ".gitignore"
1966 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1968 def test_invalid_nested_gitignore(self) -> None:
1969 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
1970 empty_config = path / "pyproject.toml"
1971 result = BlackRunner().invoke(
1972 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1974 assert result.exit_code == 1
1975 assert result.stderr_bytes is not None
1977 gitignore = path / "a" / ".gitignore"
1978 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1980 def test_empty_include(self) -> None:
1981 path = DATA_DIR / "include_exclude_tests"
1984 Path(path / "b/exclude/a.pie"),
1985 Path(path / "b/exclude/a.py"),
1986 Path(path / "b/exclude/a.pyi"),
1987 Path(path / "b/dont_exclude/a.pie"),
1988 Path(path / "b/dont_exclude/a.py"),
1989 Path(path / "b/dont_exclude/a.pyi"),
1990 Path(path / "b/.definitely_exclude/a.pie"),
1991 Path(path / "b/.definitely_exclude/a.py"),
1992 Path(path / "b/.definitely_exclude/a.pyi"),
1993 Path(path / ".gitignore"),
1994 Path(path / "pyproject.toml"),
1996 # Setting exclude explicitly to an empty string to block .gitignore usage.
1997 assert_collected_sources(src, expected, include="", exclude="")
1999 def test_extend_exclude(self) -> None:
2000 path = DATA_DIR / "include_exclude_tests"
2003 Path(path / "b/exclude/a.py"),
2004 Path(path / "b/dont_exclude/a.py"),
2006 assert_collected_sources(
2007 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
2010 @pytest.mark.incompatible_with_mypyc
2011 def test_symlink_out_of_root_directory(self) -> None:
2013 root = THIS_DIR.resolve()
2015 include = re.compile(black.DEFAULT_INCLUDES)
2016 exclude = re.compile(black.DEFAULT_EXCLUDES)
2017 report = black.Report()
2018 gitignore = PathSpec.from_lines("gitwildmatch", [])
2019 # `child` should behave like a symlink which resolved path is clearly
2020 # outside of the `root` directory.
2021 path.iterdir.return_value = [child]
2022 child.resolve.return_value = Path("/a/b/c")
2023 child.as_posix.return_value = "/a/b/c"
2026 black.gen_python_files(
2039 except ValueError as ve:
2040 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
2041 path.iterdir.assert_called_once()
2042 child.resolve.assert_called_once()
2044 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2045 def test_get_sources_with_stdin(self) -> None:
2048 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2050 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2051 def test_get_sources_with_stdin_filename(self) -> None:
2053 stdin_filename = str(THIS_DIR / "data/collections.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_exclude(self) -> None:
2064 # 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
2067 path = DATA_DIR / "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 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_extend_exclude(self) -> None:
2080 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2081 # file being passed directly. This is the same as
2082 # test_exclude_for_issue_1572
2084 path = THIS_DIR / "data" / "include_exclude_tests"
2085 stdin_filename = str(path / "b/exclude/a.py")
2086 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2087 assert_collected_sources(
2090 extend_exclude=r"/exclude/|a\.py",
2091 stdin_filename=stdin_filename,
2094 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2095 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2096 # Force exclude should exclude the file when passing it through
2098 path = THIS_DIR / "data" / "include_exclude_tests"
2099 stdin_filename = str(path / "b/exclude/a.py")
2100 assert_collected_sources(
2103 force_exclude=r"/exclude/|a\.py",
2104 stdin_filename=stdin_filename,
2109 with open(black.__file__, "r", encoding="utf-8") as _bf:
2110 black_source_lines = _bf.readlines()
2111 except UnicodeDecodeError:
2112 if not black.COMPILED:
2117 frame: types.FrameType, event: str, arg: Any
2118 ) -> Callable[[types.FrameType, str, Any], Any]:
2119 """Show function calls `from black/__init__.py` as they happen.
2121 Register this with `sys.settrace()` in a test you're debugging.
2126 stack = len(inspect.stack()) - 19
2128 filename = frame.f_code.co_filename
2129 lineno = frame.f_lineno
2130 func_sig_lineno = lineno - 1
2131 funcname = black_source_lines[func_sig_lineno].strip()
2132 while funcname.startswith("@"):
2133 func_sig_lineno += 1
2134 funcname = black_source_lines[func_sig_lineno].strip()
2135 if "black/__init__.py" in filename:
2136 print(f"{' ' * stack}{lineno}:{funcname}")