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.
13 from concurrent.futures import ThreadPoolExecutor
14 from contextlib import contextmanager, redirect_stderr
15 from dataclasses import replace
16 from io import BytesIO
17 from pathlib import Path
18 from platform import system
19 from tempfile import TemporaryDirectory
32 from unittest.mock import MagicMock, patch
36 from click import unstyle
37 from click.testing import CliRunner
38 from pathspec import PathSpec
42 from black import Feature, TargetVersion
43 from black import re_compile_maybe_verbose as compile_pattern
44 from black.cache import get_cache_dir, get_cache_file
45 from black.debug import DebugVisitor
46 from black.output import color_diff, diff
47 from black.report import Report
49 # Import other test classes
50 from tests.util import (
68 THIS_FILE = Path(__file__)
69 EMPTY_CONFIG = THIS_DIR / "data" / "empty_pyproject.toml"
70 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
71 DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
72 DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
76 # Match the time output in a diff, but nothing else
77 DIFF_TIME = re.compile(r"\t[\d\-:+\. ]+")
81 def cache_dir(exists: bool = True) -> Iterator[Path]:
82 with TemporaryDirectory() as workspace:
83 cache_dir = Path(workspace)
85 cache_dir = cache_dir / "new"
86 with patch("black.cache.CACHE_DIR", cache_dir):
91 def event_loop() -> Iterator[None]:
92 policy = asyncio.get_event_loop_policy()
93 loop = policy.new_event_loop()
94 asyncio.set_event_loop(loop)
102 class FakeContext(click.Context):
103 """A fake click Context for when calling functions that need it."""
105 def __init__(self) -> None:
106 self.default_map: Dict[str, Any] = {}
107 # Dummy root, since most of the tests don't care about it
108 self.obj: Dict[str, Any] = {"root": PROJECT_ROOT}
111 class FakeParameter(click.Parameter):
112 """A fake click Parameter for when calling functions that need it."""
114 def __init__(self) -> None:
118 class BlackRunner(CliRunner):
119 """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
121 def __init__(self) -> None:
122 super().__init__(mix_stderr=False)
126 args: List[str], exit_code: int = 0, ignore_config: bool = True
128 runner = BlackRunner()
130 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
131 result = runner.invoke(black.main, args, catch_exceptions=False)
132 assert result.stdout_bytes is not None
133 assert result.stderr_bytes is not None
135 f"Failed with args: {args}\n"
136 f"stdout: {result.stdout_bytes.decode()!r}\n"
137 f"stderr: {result.stderr_bytes.decode()!r}\n"
138 f"exception: {result.exception}"
140 assert result.exit_code == exit_code, msg
143 class BlackTestCase(BlackBaseTestCase):
144 invokeBlack = staticmethod(invokeBlack)
146 def test_empty_ff(self) -> None:
148 tmp_file = Path(black.dump_to_file())
150 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
151 with open(tmp_file, encoding="utf8") as f:
155 self.assertFormatEqual(expected, actual)
157 @patch("black.dump_to_file", dump_to_stderr)
158 def test_one_empty_line(self) -> None:
159 mode = black.Mode(preview=True)
160 for nl in ["\n", "\r\n"]:
161 source = expected = nl
162 assert_format(source, expected, mode=mode)
164 def test_one_empty_line_ff(self) -> None:
165 mode = black.Mode(preview=True)
166 for nl in ["\n", "\r\n"]:
168 tmp_file = Path(black.dump_to_file(nl))
169 if system() == "Windows":
170 # Writing files in text mode automatically uses the system newline,
171 # but in this case we don't want this for testing reasons. See:
172 # https://github.com/psf/black/pull/3348
173 with open(tmp_file, "wb") as f:
174 f.write(nl.encode("utf-8"))
177 ff(tmp_file, mode=mode, write_back=black.WriteBack.YES)
179 with open(tmp_file, "rb") as f:
180 actual = f.read().decode("utf8")
183 self.assertFormatEqual(expected, actual)
185 def test_experimental_string_processing_warns(self) -> None:
187 black.mode.Deprecated, black.Mode, experimental_string_processing=True
190 def test_piping(self) -> None:
191 source, expected = read_data_from_file(PROJECT_ROOT / "src/black/__init__.py")
192 result = BlackRunner().invoke(
197 f"--line-length={black.DEFAULT_LINE_LENGTH}",
198 f"--config={EMPTY_CONFIG}",
200 input=BytesIO(source.encode("utf8")),
202 self.assertEqual(result.exit_code, 0)
203 self.assertFormatEqual(expected, result.output)
204 if source != result.output:
205 black.assert_equivalent(source, result.output)
206 black.assert_stable(source, result.output, DEFAULT_MODE)
208 def test_piping_diff(self) -> None:
209 diff_header = re.compile(
210 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
213 source, _ = read_data("simple_cases", "expression.py")
214 expected, _ = read_data("simple_cases", "expression.diff")
218 f"--line-length={black.DEFAULT_LINE_LENGTH}",
220 f"--config={EMPTY_CONFIG}",
222 result = BlackRunner().invoke(
223 black.main, args, input=BytesIO(source.encode("utf8"))
225 self.assertEqual(result.exit_code, 0)
226 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
227 actual = actual.rstrip() + "\n" # the diff output has a trailing space
228 self.assertEqual(expected, actual)
230 def test_piping_diff_with_color(self) -> None:
231 source, _ = read_data("simple_cases", "expression.py")
235 f"--line-length={black.DEFAULT_LINE_LENGTH}",
238 f"--config={EMPTY_CONFIG}",
240 result = BlackRunner().invoke(
241 black.main, args, input=BytesIO(source.encode("utf8"))
243 actual = result.output
244 # Again, the contents are checked in a different test, so only look for colors.
245 self.assertIn("\033[1m", actual)
246 self.assertIn("\033[36m", actual)
247 self.assertIn("\033[32m", actual)
248 self.assertIn("\033[31m", actual)
249 self.assertIn("\033[0m", actual)
251 @patch("black.dump_to_file", dump_to_stderr)
252 def _test_wip(self) -> None:
253 source, expected = read_data("miscellaneous", "wip")
254 sys.settrace(tracefunc)
257 experimental_string_processing=False,
258 target_versions={black.TargetVersion.PY38},
260 actual = fs(source, mode=mode)
262 self.assertFormatEqual(expected, actual)
263 black.assert_equivalent(source, actual)
264 black.assert_stable(source, actual, black.FileMode())
266 def test_pep_572_version_detection(self) -> None:
267 source, _ = read_data("py_38", "pep_572")
268 root = black.lib2to3_parse(source)
269 features = black.get_features_used(root)
270 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
271 versions = black.detect_target_versions(root)
272 self.assertIn(black.TargetVersion.PY38, versions)
274 def test_pep_695_version_detection(self) -> None:
275 for file in ("type_aliases", "type_params"):
276 source, _ = read_data("py_312", file)
277 root = black.lib2to3_parse(source)
278 features = black.get_features_used(root)
279 self.assertIn(black.Feature.TYPE_PARAMS, features)
280 versions = black.detect_target_versions(root)
281 self.assertIn(black.TargetVersion.PY312, versions)
283 def test_expression_ff(self) -> None:
284 source, expected = read_data("simple_cases", "expression.py")
285 tmp_file = Path(black.dump_to_file(source))
287 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
288 with open(tmp_file, encoding="utf8") as f:
292 self.assertFormatEqual(expected, actual)
293 with patch("black.dump_to_file", dump_to_stderr):
294 black.assert_equivalent(source, actual)
295 black.assert_stable(source, actual, DEFAULT_MODE)
297 def test_expression_diff(self) -> None:
298 source, _ = read_data("simple_cases", "expression.py")
299 expected, _ = read_data("simple_cases", "expression.diff")
300 tmp_file = Path(black.dump_to_file(source))
301 diff_header = re.compile(
302 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
303 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
306 result = BlackRunner().invoke(
307 black.main, ["--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
309 self.assertEqual(result.exit_code, 0)
312 actual = result.output
313 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
314 if expected != actual:
315 dump = black.dump_to_file(actual)
317 "Expected diff isn't equal to the actual. If you made changes to"
318 " expression.py and this is an anticipated difference, overwrite"
319 f" tests/data/expression.diff with {dump}"
321 self.assertEqual(expected, actual, msg)
323 def test_expression_diff_with_color(self) -> None:
324 source, _ = read_data("simple_cases", "expression.py")
325 expected, _ = read_data("simple_cases", "expression.diff")
326 tmp_file = Path(black.dump_to_file(source))
328 result = BlackRunner().invoke(
330 ["--diff", "--color", str(tmp_file), f"--config={EMPTY_CONFIG}"],
334 actual = result.output
335 # We check the contents of the diff in `test_expression_diff`. All
336 # we need to check here is that color codes exist in the result.
337 self.assertIn("\033[1m", actual)
338 self.assertIn("\033[36m", actual)
339 self.assertIn("\033[32m", actual)
340 self.assertIn("\033[31m", actual)
341 self.assertIn("\033[0m", actual)
343 def test_detect_pos_only_arguments(self) -> None:
344 source, _ = read_data("py_38", "pep_570")
345 root = black.lib2to3_parse(source)
346 features = black.get_features_used(root)
347 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
348 versions = black.detect_target_versions(root)
349 self.assertIn(black.TargetVersion.PY38, versions)
351 def test_detect_debug_f_strings(self) -> None:
352 root = black.lib2to3_parse("""f"{x=}" """)
353 features = black.get_features_used(root)
354 self.assertIn(black.Feature.DEBUG_F_STRINGS, features)
355 versions = black.detect_target_versions(root)
356 self.assertIn(black.TargetVersion.PY38, versions)
358 root = black.lib2to3_parse(
359 """f"{x}"\nf'{"="}'\nf'{(x:=5)}'\nf'{f(a="3=")}'\nf'{x:=10}'\n"""
361 features = black.get_features_used(root)
362 self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
364 # We don't yet support feature version detection in nested f-strings
365 root = black.lib2to3_parse(
366 """f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """
368 features = black.get_features_used(root)
369 self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
371 @patch("black.dump_to_file", dump_to_stderr)
372 def test_string_quotes(self) -> None:
373 source, expected = read_data("miscellaneous", "string_quotes")
374 mode = black.Mode(preview=True)
375 assert_format(source, expected, mode)
376 mode = replace(mode, string_normalization=False)
377 not_normalized = fs(source, mode=mode)
378 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
379 black.assert_equivalent(source, not_normalized)
380 black.assert_stable(source, not_normalized, mode=mode)
382 def test_skip_source_first_line(self) -> None:
383 source, _ = read_data("miscellaneous", "invalid_header")
384 tmp_file = Path(black.dump_to_file(source))
385 # Full source should fail (invalid syntax at header)
386 self.invokeBlack([str(tmp_file), "--diff", "--check"], exit_code=123)
387 # So, skipping the first line should work
388 result = BlackRunner().invoke(
389 black.main, [str(tmp_file), "-x", f"--config={EMPTY_CONFIG}"]
391 self.assertEqual(result.exit_code, 0)
392 with open(tmp_file, encoding="utf8") as f:
394 self.assertFormatEqual(source, actual)
396 def test_skip_source_first_line_when_mixing_newlines(self) -> None:
397 code_mixing_newlines = b"Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n"
398 expected = b"Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n"
399 with TemporaryDirectory() as workspace:
400 test_file = Path(workspace) / "skip_header.py"
401 test_file.write_bytes(code_mixing_newlines)
402 mode = replace(DEFAULT_MODE, skip_source_first_line=True)
403 ff(test_file, mode=mode, write_back=black.WriteBack.YES)
404 self.assertEqual(test_file.read_bytes(), expected)
406 def test_skip_magic_trailing_comma(self) -> None:
407 source, _ = read_data("simple_cases", "expression")
408 expected, _ = read_data(
409 "miscellaneous", "expression_skip_magic_trailing_comma.diff"
411 tmp_file = Path(black.dump_to_file(source))
412 diff_header = re.compile(
413 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
414 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
417 result = BlackRunner().invoke(
418 black.main, ["-C", "--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
420 self.assertEqual(result.exit_code, 0)
423 actual = result.output
424 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
425 actual = actual.rstrip() + "\n" # the diff output has a trailing space
426 if expected != actual:
427 dump = black.dump_to_file(actual)
429 "Expected diff isn't equal to the actual. If you made changes to"
430 " expression.py and this is an anticipated difference, overwrite"
431 " tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff"
434 self.assertEqual(expected, actual, msg)
436 @patch("black.dump_to_file", dump_to_stderr)
437 def test_async_as_identifier(self) -> None:
438 source_path = get_case_path("miscellaneous", "async_as_identifier")
439 source, expected = read_data_from_file(source_path)
441 self.assertFormatEqual(expected, actual)
442 major, minor = sys.version_info[:2]
443 if major < 3 or (major <= 3 and minor < 7):
444 black.assert_equivalent(source, actual)
445 black.assert_stable(source, actual, DEFAULT_MODE)
446 # ensure black can parse this when the target is 3.6
447 self.invokeBlack([str(source_path), "--target-version", "py36"])
448 # but not on 3.7, because async/await is no longer an identifier
449 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
451 @patch("black.dump_to_file", dump_to_stderr)
452 def test_python37(self) -> None:
453 source_path = get_case_path("py_37", "python37")
454 source, expected = read_data_from_file(source_path)
456 self.assertFormatEqual(expected, actual)
457 major, minor = sys.version_info[:2]
458 if major > 3 or (major == 3 and minor >= 7):
459 black.assert_equivalent(source, actual)
460 black.assert_stable(source, actual, DEFAULT_MODE)
461 # ensure black can parse this when the target is 3.7
462 self.invokeBlack([str(source_path), "--target-version", "py37"])
463 # but not on 3.6, because we use async as a reserved keyword
464 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
466 def test_tab_comment_indentation(self) -> None:
467 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
468 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
469 self.assertFormatEqual(contents_spc, fs(contents_spc))
470 self.assertFormatEqual(contents_spc, fs(contents_tab))
472 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
473 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
474 self.assertFormatEqual(contents_spc, fs(contents_spc))
475 self.assertFormatEqual(contents_spc, fs(contents_tab))
477 # mixed tabs and spaces (valid Python 2 code)
478 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
479 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
480 self.assertFormatEqual(contents_spc, fs(contents_spc))
481 self.assertFormatEqual(contents_spc, fs(contents_tab))
483 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
484 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
485 self.assertFormatEqual(contents_spc, fs(contents_spc))
486 self.assertFormatEqual(contents_spc, fs(contents_tab))
488 def test_false_positive_symlink_output_issue_3384(self) -> None:
489 # Emulate the behavior when using the CLI (`black ./child --verbose`), which
490 # involves patching some `pathlib.Path` methods. In particular, `is_dir` is
491 # patched only on its first call: when checking if "./child" is a directory it
492 # should return True. The "./child" folder exists relative to the cwd when
493 # running from CLI, but fails when running the tests because cwd is different
494 project_root = Path(THIS_DIR / "data" / "nested_gitignore_tests")
495 working_directory = project_root / "root"
496 target_abspath = working_directory / "child"
498 src.relative_to(working_directory) for src in target_abspath.iterdir()
501 def mock_n_calls(responses: List[bool]) -> Callable[[], bool]:
502 def _mocked_calls() -> bool:
504 return responses.pop(0)
509 with patch("pathlib.Path.iterdir", return_value=target_contents), patch(
510 "pathlib.Path.cwd", return_value=working_directory
511 ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])):
513 ctx.obj["root"] = project_root
514 report = MagicMock(verbose=True)
520 include=DEFAULT_INCLUDE,
528 mock_args[1].startswith("is a symbolic link that points outside")
529 for _, mock_args, _ in report.path_ignored.mock_calls
530 ), "A symbolic link was reported."
531 report.path_ignored.assert_called_once_with(
532 Path("child", "b.py"), "matches a .gitignore file content"
535 def test_report_verbose(self) -> None:
536 report = Report(verbose=True)
540 def out(msg: str, **kwargs: Any) -> None:
541 out_lines.append(msg)
543 def err(msg: str, **kwargs: Any) -> None:
544 err_lines.append(msg)
546 with patch("black.output._out", out), patch("black.output._err", err):
547 report.done(Path("f1"), black.Changed.NO)
548 self.assertEqual(len(out_lines), 1)
549 self.assertEqual(len(err_lines), 0)
550 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
551 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
552 self.assertEqual(report.return_code, 0)
553 report.done(Path("f2"), black.Changed.YES)
554 self.assertEqual(len(out_lines), 2)
555 self.assertEqual(len(err_lines), 0)
556 self.assertEqual(out_lines[-1], "reformatted f2")
558 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
560 report.done(Path("f3"), black.Changed.CACHED)
561 self.assertEqual(len(out_lines), 3)
562 self.assertEqual(len(err_lines), 0)
564 out_lines[-1], "f3 wasn't modified on disk since last run."
567 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
569 self.assertEqual(report.return_code, 0)
571 self.assertEqual(report.return_code, 1)
573 report.failed(Path("e1"), "boom")
574 self.assertEqual(len(out_lines), 3)
575 self.assertEqual(len(err_lines), 1)
576 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
578 unstyle(str(report)),
579 "1 file reformatted, 2 files left unchanged, 1 file failed to"
582 self.assertEqual(report.return_code, 123)
583 report.done(Path("f3"), black.Changed.YES)
584 self.assertEqual(len(out_lines), 4)
585 self.assertEqual(len(err_lines), 1)
586 self.assertEqual(out_lines[-1], "reformatted f3")
588 unstyle(str(report)),
589 "2 files reformatted, 2 files left unchanged, 1 file failed to"
592 self.assertEqual(report.return_code, 123)
593 report.failed(Path("e2"), "boom")
594 self.assertEqual(len(out_lines), 4)
595 self.assertEqual(len(err_lines), 2)
596 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
598 unstyle(str(report)),
599 "2 files reformatted, 2 files left unchanged, 2 files failed to"
602 self.assertEqual(report.return_code, 123)
603 report.path_ignored(Path("wat"), "no match")
604 self.assertEqual(len(out_lines), 5)
605 self.assertEqual(len(err_lines), 2)
606 self.assertEqual(out_lines[-1], "wat ignored: no match")
608 unstyle(str(report)),
609 "2 files reformatted, 2 files left unchanged, 2 files failed to"
612 self.assertEqual(report.return_code, 123)
613 report.done(Path("f4"), black.Changed.NO)
614 self.assertEqual(len(out_lines), 6)
615 self.assertEqual(len(err_lines), 2)
616 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
618 unstyle(str(report)),
619 "2 files reformatted, 3 files left unchanged, 2 files failed to"
622 self.assertEqual(report.return_code, 123)
625 unstyle(str(report)),
626 "2 files would be reformatted, 3 files would be left unchanged, 2"
627 " files would fail to reformat.",
632 unstyle(str(report)),
633 "2 files would be reformatted, 3 files would be left unchanged, 2"
634 " files would fail to reformat.",
637 def test_report_quiet(self) -> None:
638 report = Report(quiet=True)
642 def out(msg: str, **kwargs: Any) -> None:
643 out_lines.append(msg)
645 def err(msg: str, **kwargs: Any) -> None:
646 err_lines.append(msg)
648 with patch("black.output._out", out), patch("black.output._err", err):
649 report.done(Path("f1"), black.Changed.NO)
650 self.assertEqual(len(out_lines), 0)
651 self.assertEqual(len(err_lines), 0)
652 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
653 self.assertEqual(report.return_code, 0)
654 report.done(Path("f2"), black.Changed.YES)
655 self.assertEqual(len(out_lines), 0)
656 self.assertEqual(len(err_lines), 0)
658 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
660 report.done(Path("f3"), black.Changed.CACHED)
661 self.assertEqual(len(out_lines), 0)
662 self.assertEqual(len(err_lines), 0)
664 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
666 self.assertEqual(report.return_code, 0)
668 self.assertEqual(report.return_code, 1)
670 report.failed(Path("e1"), "boom")
671 self.assertEqual(len(out_lines), 0)
672 self.assertEqual(len(err_lines), 1)
673 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
675 unstyle(str(report)),
676 "1 file reformatted, 2 files left unchanged, 1 file failed to"
679 self.assertEqual(report.return_code, 123)
680 report.done(Path("f3"), black.Changed.YES)
681 self.assertEqual(len(out_lines), 0)
682 self.assertEqual(len(err_lines), 1)
684 unstyle(str(report)),
685 "2 files reformatted, 2 files left unchanged, 1 file failed to"
688 self.assertEqual(report.return_code, 123)
689 report.failed(Path("e2"), "boom")
690 self.assertEqual(len(out_lines), 0)
691 self.assertEqual(len(err_lines), 2)
692 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
694 unstyle(str(report)),
695 "2 files reformatted, 2 files left unchanged, 2 files failed to"
698 self.assertEqual(report.return_code, 123)
699 report.path_ignored(Path("wat"), "no match")
700 self.assertEqual(len(out_lines), 0)
701 self.assertEqual(len(err_lines), 2)
703 unstyle(str(report)),
704 "2 files reformatted, 2 files left unchanged, 2 files failed to"
707 self.assertEqual(report.return_code, 123)
708 report.done(Path("f4"), black.Changed.NO)
709 self.assertEqual(len(out_lines), 0)
710 self.assertEqual(len(err_lines), 2)
712 unstyle(str(report)),
713 "2 files reformatted, 3 files left unchanged, 2 files failed to"
716 self.assertEqual(report.return_code, 123)
719 unstyle(str(report)),
720 "2 files would be reformatted, 3 files would be left unchanged, 2"
721 " files would fail to reformat.",
726 unstyle(str(report)),
727 "2 files would be reformatted, 3 files would be left unchanged, 2"
728 " files would fail to reformat.",
731 def test_report_normal(self) -> None:
732 report = black.Report()
736 def out(msg: str, **kwargs: Any) -> None:
737 out_lines.append(msg)
739 def err(msg: str, **kwargs: Any) -> None:
740 err_lines.append(msg)
742 with patch("black.output._out", out), patch("black.output._err", err):
743 report.done(Path("f1"), black.Changed.NO)
744 self.assertEqual(len(out_lines), 0)
745 self.assertEqual(len(err_lines), 0)
746 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
747 self.assertEqual(report.return_code, 0)
748 report.done(Path("f2"), black.Changed.YES)
749 self.assertEqual(len(out_lines), 1)
750 self.assertEqual(len(err_lines), 0)
751 self.assertEqual(out_lines[-1], "reformatted f2")
753 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
755 report.done(Path("f3"), black.Changed.CACHED)
756 self.assertEqual(len(out_lines), 1)
757 self.assertEqual(len(err_lines), 0)
758 self.assertEqual(out_lines[-1], "reformatted f2")
760 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
762 self.assertEqual(report.return_code, 0)
764 self.assertEqual(report.return_code, 1)
766 report.failed(Path("e1"), "boom")
767 self.assertEqual(len(out_lines), 1)
768 self.assertEqual(len(err_lines), 1)
769 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
771 unstyle(str(report)),
772 "1 file reformatted, 2 files left unchanged, 1 file failed to"
775 self.assertEqual(report.return_code, 123)
776 report.done(Path("f3"), black.Changed.YES)
777 self.assertEqual(len(out_lines), 2)
778 self.assertEqual(len(err_lines), 1)
779 self.assertEqual(out_lines[-1], "reformatted f3")
781 unstyle(str(report)),
782 "2 files reformatted, 2 files left unchanged, 1 file failed to"
785 self.assertEqual(report.return_code, 123)
786 report.failed(Path("e2"), "boom")
787 self.assertEqual(len(out_lines), 2)
788 self.assertEqual(len(err_lines), 2)
789 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
791 unstyle(str(report)),
792 "2 files reformatted, 2 files left unchanged, 2 files failed to"
795 self.assertEqual(report.return_code, 123)
796 report.path_ignored(Path("wat"), "no match")
797 self.assertEqual(len(out_lines), 2)
798 self.assertEqual(len(err_lines), 2)
800 unstyle(str(report)),
801 "2 files reformatted, 2 files left unchanged, 2 files failed to"
804 self.assertEqual(report.return_code, 123)
805 report.done(Path("f4"), black.Changed.NO)
806 self.assertEqual(len(out_lines), 2)
807 self.assertEqual(len(err_lines), 2)
809 unstyle(str(report)),
810 "2 files reformatted, 3 files left unchanged, 2 files failed to"
813 self.assertEqual(report.return_code, 123)
816 unstyle(str(report)),
817 "2 files would be reformatted, 3 files would be left unchanged, 2"
818 " files would fail to reformat.",
823 unstyle(str(report)),
824 "2 files would be reformatted, 3 files would be left unchanged, 2"
825 " files would fail to reformat.",
828 def test_lib2to3_parse(self) -> None:
829 with self.assertRaises(black.InvalidInput):
830 black.lib2to3_parse("invalid syntax")
833 black.lib2to3_parse(straddling)
834 black.lib2to3_parse(straddling, {TargetVersion.PY36})
837 with self.assertRaises(black.InvalidInput):
838 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
840 py3_only = "exec(x, end=y)"
841 black.lib2to3_parse(py3_only)
842 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
844 def test_get_features_used_decorator(self) -> None:
845 # Test the feature detection of new decorator syntax
846 # since this makes some test cases of test_get_features_used()
847 # fails if it fails, this is tested first so that a useful case
849 simples, relaxed = read_data("miscellaneous", "decorators")
850 # skip explanation comments at the top of the file
851 for simple_test in simples.split("##")[1:]:
852 node = black.lib2to3_parse(simple_test)
853 decorator = str(node.children[0].children[0]).strip()
855 Feature.RELAXED_DECORATORS,
856 black.get_features_used(node),
858 f"decorator '{decorator}' follows python<=3.8 syntax"
859 "but is detected as 3.9+"
860 # f"The full node is\n{node!r}"
863 # skip the '# output' comment at the top of the output part
864 for relaxed_test in relaxed.split("##")[1:]:
865 node = black.lib2to3_parse(relaxed_test)
866 decorator = str(node.children[0].children[0]).strip()
868 Feature.RELAXED_DECORATORS,
869 black.get_features_used(node),
871 f"decorator '{decorator}' uses python3.9+ syntax"
872 "but is detected as python<=3.8"
873 # f"The full node is\n{node!r}"
877 def test_get_features_used(self) -> None:
878 node = black.lib2to3_parse("def f(*, arg): ...\n")
879 self.assertEqual(black.get_features_used(node), set())
880 node = black.lib2to3_parse("def f(*, arg,): ...\n")
881 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
882 node = black.lib2to3_parse("f(*arg,)\n")
884 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
886 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
887 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
888 node = black.lib2to3_parse("123_456\n")
889 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
890 node = black.lib2to3_parse("123456\n")
891 self.assertEqual(black.get_features_used(node), set())
892 source, expected = read_data("simple_cases", "function")
893 node = black.lib2to3_parse(source)
894 expected_features = {
895 Feature.TRAILING_COMMA_IN_CALL,
896 Feature.TRAILING_COMMA_IN_DEF,
899 self.assertEqual(black.get_features_used(node), expected_features)
900 node = black.lib2to3_parse(expected)
901 self.assertEqual(black.get_features_used(node), expected_features)
902 source, expected = read_data("simple_cases", "expression")
903 node = black.lib2to3_parse(source)
904 self.assertEqual(black.get_features_used(node), set())
905 node = black.lib2to3_parse(expected)
906 self.assertEqual(black.get_features_used(node), set())
907 node = black.lib2to3_parse("lambda a, /, b: ...")
908 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
909 node = black.lib2to3_parse("def fn(a, /, b): ...")
910 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
911 node = black.lib2to3_parse("def fn(): yield a, b")
912 self.assertEqual(black.get_features_used(node), set())
913 node = black.lib2to3_parse("def fn(): return a, b")
914 self.assertEqual(black.get_features_used(node), set())
915 node = black.lib2to3_parse("def fn(): yield *b, c")
916 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
917 node = black.lib2to3_parse("def fn(): return a, *b, c")
918 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
919 node = black.lib2to3_parse("x = a, *b, c")
920 self.assertEqual(black.get_features_used(node), set())
921 node = black.lib2to3_parse("x: Any = regular")
922 self.assertEqual(black.get_features_used(node), set())
923 node = black.lib2to3_parse("x: Any = (regular, regular)")
924 self.assertEqual(black.get_features_used(node), set())
925 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
926 self.assertEqual(black.get_features_used(node), set())
927 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
929 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
931 node = black.lib2to3_parse("try: pass\nexcept Something: pass")
932 self.assertEqual(black.get_features_used(node), set())
933 node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass")
934 self.assertEqual(black.get_features_used(node), set())
935 node = black.lib2to3_parse("try: pass\nexcept *Group: pass")
936 self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR})
937 node = black.lib2to3_parse("a[*b]")
938 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
939 node = black.lib2to3_parse("a[x, *y(), z] = t")
940 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
941 node = black.lib2to3_parse("def fn(*args: *T): pass")
942 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
944 def test_get_features_used_for_future_flags(self) -> None:
945 for src, features in [
946 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
948 "from __future__ import (other, annotations)",
949 {Feature.FUTURE_ANNOTATIONS},
951 ("a = 1 + 2\nfrom something import annotations", set()),
952 ("from __future__ import x, y", set()),
954 with self.subTest(src=src, features=features):
955 node = black.lib2to3_parse(src)
956 future_imports = black.get_future_imports(node)
958 black.get_features_used(node, future_imports=future_imports),
962 def test_get_future_imports(self) -> None:
963 node = black.lib2to3_parse("\n")
964 self.assertEqual(set(), black.get_future_imports(node))
965 node = black.lib2to3_parse("from __future__ import black\n")
966 self.assertEqual({"black"}, black.get_future_imports(node))
967 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
968 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
969 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
970 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
971 node = black.lib2to3_parse(
972 "from __future__ import multiple\nfrom __future__ import imports\n"
974 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
975 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
976 self.assertEqual({"black"}, black.get_future_imports(node))
977 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
978 self.assertEqual({"black"}, black.get_future_imports(node))
979 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
980 self.assertEqual(set(), black.get_future_imports(node))
981 node = black.lib2to3_parse("from some.module import black\n")
982 self.assertEqual(set(), black.get_future_imports(node))
983 node = black.lib2to3_parse(
984 "from __future__ import unicode_literals as _unicode_literals"
986 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
987 node = black.lib2to3_parse(
988 "from __future__ import unicode_literals as _lol, print"
990 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
992 @pytest.mark.incompatible_with_mypyc
993 def test_debug_visitor(self) -> None:
994 source, _ = read_data("miscellaneous", "debug_visitor")
995 expected, _ = read_data("miscellaneous", "debug_visitor.out")
999 def out(msg: str, **kwargs: Any) -> None:
1000 out_lines.append(msg)
1002 def err(msg: str, **kwargs: Any) -> None:
1003 err_lines.append(msg)
1005 with patch("black.debug.out", out):
1006 DebugVisitor.show(source)
1007 actual = "\n".join(out_lines) + "\n"
1009 if expected != actual:
1010 log_name = black.dump_to_file(*out_lines)
1014 f"AST print out is different. Actual version dumped to {log_name}",
1017 def test_format_file_contents(self) -> None:
1020 with self.assertRaises(black.NothingChanged):
1021 black.format_file_contents(empty, mode=mode, fast=False)
1023 with self.assertRaises(black.NothingChanged):
1024 black.format_file_contents(just_nl, mode=mode, fast=False)
1025 same = "j = [1, 2, 3]\n"
1026 with self.assertRaises(black.NothingChanged):
1027 black.format_file_contents(same, mode=mode, fast=False)
1028 different = "j = [1,2,3]"
1030 actual = black.format_file_contents(different, mode=mode, fast=False)
1031 self.assertEqual(expected, actual)
1032 invalid = "return if you can"
1033 with self.assertRaises(black.InvalidInput) as e:
1034 black.format_file_contents(invalid, mode=mode, fast=False)
1035 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
1037 mode = black.Mode(preview=True)
1039 with self.assertRaises(black.NothingChanged):
1040 black.format_file_contents(just_crlf, mode=mode, fast=False)
1041 just_whitespace_nl = "\n\t\n \n\t \n \t\n\n"
1042 actual = black.format_file_contents(just_whitespace_nl, mode=mode, fast=False)
1043 self.assertEqual("\n", actual)
1044 just_whitespace_crlf = "\r\n\t\r\n \r\n\t \r\n \t\r\n\r\n"
1045 actual = black.format_file_contents(just_whitespace_crlf, mode=mode, fast=False)
1046 self.assertEqual("\r\n", actual)
1048 def test_endmarker(self) -> None:
1049 n = black.lib2to3_parse("\n")
1050 self.assertEqual(n.type, black.syms.file_input)
1051 self.assertEqual(len(n.children), 1)
1052 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
1054 @pytest.mark.incompatible_with_mypyc
1055 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
1056 def test_assertFormatEqual(self) -> None:
1060 def out(msg: str, **kwargs: Any) -> None:
1061 out_lines.append(msg)
1063 def err(msg: str, **kwargs: Any) -> None:
1064 err_lines.append(msg)
1066 with patch("black.output._out", out), patch("black.output._err", err):
1067 with self.assertRaises(AssertionError):
1068 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
1070 out_str = "".join(out_lines)
1071 self.assertIn("Expected tree:", out_str)
1072 self.assertIn("Actual tree:", out_str)
1073 self.assertEqual("".join(err_lines), "")
1076 @patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1077 def test_works_in_mono_process_only_environment(self) -> None:
1078 with cache_dir() as workspace:
1080 (workspace / "one.py").resolve(),
1081 (workspace / "two.py").resolve(),
1083 f.write_text('print("hello")\n')
1084 self.invokeBlack([str(workspace)])
1087 def test_check_diff_use_together(self) -> None:
1089 # Files which will be reformatted.
1090 src1 = get_case_path("miscellaneous", "string_quotes")
1091 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1092 # Files which will not be reformatted.
1093 src2 = get_case_path("simple_cases", "composition")
1094 self.invokeBlack([str(src2), "--diff", "--check"])
1095 # Multi file command.
1096 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1098 def test_no_src_fails(self) -> None:
1100 self.invokeBlack([], exit_code=1)
1102 def test_src_and_code_fails(self) -> None:
1104 self.invokeBlack([".", "-c", "0"], exit_code=1)
1106 def test_broken_symlink(self) -> None:
1107 with cache_dir() as workspace:
1108 symlink = workspace / "broken_link.py"
1110 symlink.symlink_to("nonexistent.py")
1111 except (OSError, NotImplementedError) as e:
1112 self.skipTest(f"Can't create symlinks: {e}")
1113 self.invokeBlack([str(workspace.resolve())])
1115 def test_single_file_force_pyi(self) -> None:
1116 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1117 contents, expected = read_data("miscellaneous", "force_pyi")
1118 with cache_dir() as workspace:
1119 path = (workspace / "file.py").resolve()
1120 with open(path, "w") as fh:
1122 self.invokeBlack([str(path), "--pyi"])
1123 with open(path, "r") as fh:
1125 # verify cache with --pyi is separate
1126 pyi_cache = black.read_cache(pyi_mode)
1127 self.assertIn(str(path), pyi_cache)
1128 normal_cache = black.read_cache(DEFAULT_MODE)
1129 self.assertNotIn(str(path), normal_cache)
1130 self.assertFormatEqual(expected, actual)
1131 black.assert_equivalent(contents, actual)
1132 black.assert_stable(contents, actual, pyi_mode)
1135 def test_multi_file_force_pyi(self) -> None:
1136 reg_mode = DEFAULT_MODE
1137 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1138 contents, expected = read_data("miscellaneous", "force_pyi")
1139 with cache_dir() as workspace:
1141 (workspace / "file1.py").resolve(),
1142 (workspace / "file2.py").resolve(),
1145 with open(path, "w") as fh:
1147 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1149 with open(path, "r") as fh:
1151 self.assertEqual(actual, expected)
1152 # verify cache with --pyi is separate
1153 pyi_cache = black.read_cache(pyi_mode)
1154 normal_cache = black.read_cache(reg_mode)
1156 self.assertIn(str(path), pyi_cache)
1157 self.assertNotIn(str(path), normal_cache)
1159 def test_pipe_force_pyi(self) -> None:
1160 source, expected = read_data("miscellaneous", "force_pyi")
1161 result = CliRunner().invoke(
1162 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1164 self.assertEqual(result.exit_code, 0)
1165 actual = result.output
1166 self.assertFormatEqual(actual, expected)
1168 def test_single_file_force_py36(self) -> None:
1169 reg_mode = DEFAULT_MODE
1170 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1171 source, expected = read_data("miscellaneous", "force_py36")
1172 with cache_dir() as workspace:
1173 path = (workspace / "file.py").resolve()
1174 with open(path, "w") as fh:
1176 self.invokeBlack([str(path), *PY36_ARGS])
1177 with open(path, "r") as fh:
1179 # verify cache with --target-version is separate
1180 py36_cache = black.read_cache(py36_mode)
1181 self.assertIn(str(path), py36_cache)
1182 normal_cache = black.read_cache(reg_mode)
1183 self.assertNotIn(str(path), normal_cache)
1184 self.assertEqual(actual, expected)
1187 def test_multi_file_force_py36(self) -> None:
1188 reg_mode = DEFAULT_MODE
1189 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1190 source, expected = read_data("miscellaneous", "force_py36")
1191 with cache_dir() as workspace:
1193 (workspace / "file1.py").resolve(),
1194 (workspace / "file2.py").resolve(),
1197 with open(path, "w") as fh:
1199 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1201 with open(path, "r") as fh:
1203 self.assertEqual(actual, expected)
1204 # verify cache with --target-version is separate
1205 pyi_cache = black.read_cache(py36_mode)
1206 normal_cache = black.read_cache(reg_mode)
1208 self.assertIn(str(path), pyi_cache)
1209 self.assertNotIn(str(path), normal_cache)
1211 def test_pipe_force_py36(self) -> None:
1212 source, expected = read_data("miscellaneous", "force_py36")
1213 result = CliRunner().invoke(
1215 ["-", "-q", "--target-version=py36"],
1216 input=BytesIO(source.encode("utf8")),
1218 self.assertEqual(result.exit_code, 0)
1219 actual = result.output
1220 self.assertFormatEqual(actual, expected)
1222 @pytest.mark.incompatible_with_mypyc
1223 def test_reformat_one_with_stdin(self) -> None:
1225 "black.format_stdin_to_stdout",
1226 return_value=lambda *args, **kwargs: black.Changed.YES,
1228 report = MagicMock()
1233 write_back=black.WriteBack.YES,
1237 fsts.assert_called_once()
1238 report.done.assert_called_with(path, black.Changed.YES)
1240 @pytest.mark.incompatible_with_mypyc
1241 def test_reformat_one_with_stdin_filename(self) -> None:
1243 "black.format_stdin_to_stdout",
1244 return_value=lambda *args, **kwargs: black.Changed.YES,
1246 report = MagicMock()
1248 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1253 write_back=black.WriteBack.YES,
1257 fsts.assert_called_once_with(
1258 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1260 # __BLACK_STDIN_FILENAME__ should have been stripped
1261 report.done.assert_called_with(expected, black.Changed.YES)
1263 @pytest.mark.incompatible_with_mypyc
1264 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1266 "black.format_stdin_to_stdout",
1267 return_value=lambda *args, **kwargs: black.Changed.YES,
1269 report = MagicMock()
1271 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1276 write_back=black.WriteBack.YES,
1280 fsts.assert_called_once_with(
1282 write_back=black.WriteBack.YES,
1283 mode=replace(DEFAULT_MODE, is_pyi=True),
1285 # __BLACK_STDIN_FILENAME__ should have been stripped
1286 report.done.assert_called_with(expected, black.Changed.YES)
1288 @pytest.mark.incompatible_with_mypyc
1289 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1291 "black.format_stdin_to_stdout",
1292 return_value=lambda *args, **kwargs: black.Changed.YES,
1294 report = MagicMock()
1296 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1301 write_back=black.WriteBack.YES,
1305 fsts.assert_called_once_with(
1307 write_back=black.WriteBack.YES,
1308 mode=replace(DEFAULT_MODE, is_ipynb=True),
1310 # __BLACK_STDIN_FILENAME__ should have been stripped
1311 report.done.assert_called_with(expected, black.Changed.YES)
1313 @pytest.mark.incompatible_with_mypyc
1314 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1316 "black.format_stdin_to_stdout",
1317 return_value=lambda *args, **kwargs: black.Changed.YES,
1319 report = MagicMock()
1320 # Even with an existing file, since we are forcing stdin, black
1321 # should output to stdout and not modify the file inplace
1322 p = THIS_DIR / "data" / "simple_cases" / "collections.py"
1323 # Make sure is_file actually returns True
1324 self.assertTrue(p.is_file())
1325 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1330 write_back=black.WriteBack.YES,
1334 fsts.assert_called_once()
1335 # __BLACK_STDIN_FILENAME__ should have been stripped
1336 report.done.assert_called_with(expected, black.Changed.YES)
1338 def test_reformat_one_with_stdin_empty(self) -> None:
1345 (" \t\r\n\t ", "\r\n"),
1349 output: io.StringIO, io_TextIOWrapper: Type[io.TextIOWrapper]
1350 ) -> Callable[[Any, Any], io.TextIOWrapper]:
1351 def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper:
1352 if args == (sys.stdout.buffer,):
1353 # It's `format_stdin_to_stdout()` calling `io.TextIOWrapper()`,
1354 # return our mock object.
1356 # It's something else (i.e. `decode_bytes()`) calling
1357 # `io.TextIOWrapper()`, pass through to the original implementation.
1358 # See discussion in https://github.com/psf/black/pull/2489
1359 return io_TextIOWrapper(*args, **kwargs)
1363 mode = black.Mode(preview=True)
1364 for content, expected in cases:
1365 output = io.StringIO()
1366 io_TextIOWrapper = io.TextIOWrapper
1368 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1370 black.format_stdin_to_stdout(
1373 write_back=black.WriteBack.YES,
1376 except io.UnsupportedOperation:
1377 pass # StringIO does not support detach
1378 assert output.getvalue() == expected
1380 # An empty string is the only test case for `preview=False`
1381 output = io.StringIO()
1382 io_TextIOWrapper = io.TextIOWrapper
1383 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1385 black.format_stdin_to_stdout(
1388 write_back=black.WriteBack.YES,
1391 except io.UnsupportedOperation:
1392 pass # StringIO does not support detach
1393 assert output.getvalue() == ""
1395 def test_invalid_cli_regex(self) -> None:
1396 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1397 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1399 def test_required_version_matches_version(self) -> None:
1401 ["--required-version", black.__version__, "-c", "0"],
1406 def test_required_version_matches_partial_version(self) -> None:
1408 ["--required-version", black.__version__.split(".")[0], "-c", "0"],
1413 def test_required_version_does_not_match_on_minor_version(self) -> None:
1415 ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
1420 def test_required_version_does_not_match_version(self) -> None:
1421 result = BlackRunner().invoke(
1423 ["--required-version", "20.99b", "-c", "0"],
1425 self.assertEqual(result.exit_code, 1)
1426 self.assertIn("required version", result.stderr)
1428 def test_preserves_line_endings(self) -> None:
1429 with TemporaryDirectory() as workspace:
1430 test_file = Path(workspace) / "test.py"
1431 for nl in ["\n", "\r\n"]:
1432 contents = nl.join(["def f( ):", " pass"])
1433 test_file.write_bytes(contents.encode())
1434 ff(test_file, write_back=black.WriteBack.YES)
1435 updated_contents: bytes = test_file.read_bytes()
1436 self.assertIn(nl.encode(), updated_contents)
1438 self.assertNotIn(b"\r\n", updated_contents)
1440 def test_preserves_line_endings_via_stdin(self) -> None:
1441 for nl in ["\n", "\r\n"]:
1442 contents = nl.join(["def f( ):", " pass"])
1443 runner = BlackRunner()
1444 result = runner.invoke(
1445 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1447 self.assertEqual(result.exit_code, 0)
1448 output = result.stdout_bytes
1449 self.assertIn(nl.encode("utf8"), output)
1451 self.assertNotIn(b"\r\n", output)
1453 def test_normalize_line_endings(self) -> None:
1454 with TemporaryDirectory() as workspace:
1455 test_file = Path(workspace) / "test.py"
1456 for data, expected in (
1457 (b"c\r\nc\n ", b"c\r\nc\r\n"),
1458 (b"l\nl\r\n ", b"l\nl\n"),
1460 test_file.write_bytes(data)
1461 ff(test_file, write_back=black.WriteBack.YES)
1462 self.assertEqual(test_file.read_bytes(), expected)
1464 def test_assert_equivalent_different_asts(self) -> None:
1465 with self.assertRaises(AssertionError):
1466 black.assert_equivalent("{}", "None")
1468 def test_shhh_click(self) -> None:
1470 from click import _unicodefun # type: ignore
1472 self.skipTest("Incompatible Click version")
1474 if not hasattr(_unicodefun, "_verify_python_env"):
1475 self.skipTest("Incompatible Click version")
1477 # First, let's see if Click is crashing with a preferred ASCII charset.
1478 with patch("locale.getpreferredencoding") as gpe:
1479 gpe.return_value = "ASCII"
1480 with self.assertRaises(RuntimeError):
1481 _unicodefun._verify_python_env()
1482 # Now, let's silence Click...
1484 # ...and confirm it's silent.
1485 with patch("locale.getpreferredencoding") as gpe:
1486 gpe.return_value = "ASCII"
1488 _unicodefun._verify_python_env()
1489 except RuntimeError as re:
1490 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1492 def test_root_logger_not_used_directly(self) -> None:
1493 def fail(*args: Any, **kwargs: Any) -> None:
1494 self.fail("Record created with root logger")
1496 with patch.multiple(
1505 ff(THIS_DIR / "util.py")
1507 def test_invalid_config_return_code(self) -> None:
1508 tmp_file = Path(black.dump_to_file())
1510 tmp_config = Path(black.dump_to_file())
1512 args = ["--config", str(tmp_config), str(tmp_file)]
1513 self.invokeBlack(args, exit_code=2, ignore_config=False)
1517 def test_parse_pyproject_toml(self) -> None:
1518 test_toml_file = THIS_DIR / "test.toml"
1519 config = black.parse_pyproject_toml(str(test_toml_file))
1520 self.assertEqual(config["verbose"], 1)
1521 self.assertEqual(config["check"], "no")
1522 self.assertEqual(config["diff"], "y")
1523 self.assertEqual(config["color"], True)
1524 self.assertEqual(config["line_length"], 79)
1525 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1526 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1527 self.assertEqual(config["exclude"], r"\.pyi?$")
1528 self.assertEqual(config["include"], r"\.py?$")
1530 def test_parse_pyproject_toml_project_metadata(self) -> None:
1531 for test_toml, expected in [
1532 ("only_black_pyproject.toml", ["py310"]),
1533 ("only_metadata_pyproject.toml", ["py37", "py38", "py39", "py310"]),
1534 ("neither_pyproject.toml", None),
1535 ("both_pyproject.toml", ["py310"]),
1537 test_toml_file = THIS_DIR / "data" / "project_metadata" / test_toml
1538 config = black.parse_pyproject_toml(str(test_toml_file))
1539 self.assertEqual(config.get("target_version"), expected)
1541 def test_infer_target_version(self) -> None:
1542 for version, expected in [
1543 ("3.6", [TargetVersion.PY36]),
1544 ("3.11.0rc1", [TargetVersion.PY311]),
1545 (">=3.10", [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312]),
1548 [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
1550 ("<3.6", [TargetVersion.PY33, TargetVersion.PY34, TargetVersion.PY35]),
1551 (">3.7,<3.10", [TargetVersion.PY38, TargetVersion.PY39]),
1554 [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
1557 "> 3.9.4, != 3.10.3",
1560 TargetVersion.PY310,
1561 TargetVersion.PY311,
1562 TargetVersion.PY312,
1573 TargetVersion.PY310,
1574 TargetVersion.PY311,
1575 TargetVersion.PY312,
1588 TargetVersion.PY310,
1589 TargetVersion.PY311,
1590 TargetVersion.PY312,
1593 ("==3.8.*", [TargetVersion.PY38]),
1597 ("==invalid", None),
1598 (">3.9,!=invalid", None),
1603 (">3.10,<3.11", None),
1605 test_toml = {"project": {"requires-python": version}}
1606 result = black.files.infer_target_version(test_toml)
1607 self.assertEqual(result, expected)
1609 def test_read_pyproject_toml(self) -> None:
1610 test_toml_file = THIS_DIR / "test.toml"
1611 fake_ctx = FakeContext()
1612 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1613 config = fake_ctx.default_map
1614 self.assertEqual(config["verbose"], "1")
1615 self.assertEqual(config["check"], "no")
1616 self.assertEqual(config["diff"], "y")
1617 self.assertEqual(config["color"], "True")
1618 self.assertEqual(config["line_length"], "79")
1619 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1620 self.assertEqual(config["exclude"], r"\.pyi?$")
1621 self.assertEqual(config["include"], r"\.py?$")
1623 @pytest.mark.incompatible_with_mypyc
1624 def test_find_project_root(self) -> None:
1625 with TemporaryDirectory() as workspace:
1626 root = Path(workspace)
1627 test_dir = root / "test"
1630 src_dir = root / "src"
1633 root_pyproject = root / "pyproject.toml"
1634 root_pyproject.touch()
1635 src_pyproject = src_dir / "pyproject.toml"
1636 src_pyproject.touch()
1637 src_python = src_dir / "foo.py"
1641 black.find_project_root((src_dir, test_dir)),
1642 (root.resolve(), "pyproject.toml"),
1645 black.find_project_root((src_dir,)),
1646 (src_dir.resolve(), "pyproject.toml"),
1649 black.find_project_root((src_python,)),
1650 (src_dir.resolve(), "pyproject.toml"),
1653 with change_directory(test_dir):
1655 black.find_project_root(("-",), stdin_filename="../src/a.py"),
1656 (src_dir.resolve(), "pyproject.toml"),
1660 "black.files.find_user_pyproject_toml",
1662 def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
1663 find_user_pyproject_toml.side_effect = RuntimeError()
1665 with redirect_stderr(io.StringIO()) as stderr:
1666 result = black.files.find_pyproject_toml(
1667 path_search_start=(str(Path.cwd().root),)
1670 assert result is None
1671 err = stderr.getvalue()
1672 assert "Ignoring user configuration" in err
1675 "black.files.find_user_pyproject_toml",
1676 black.files.find_user_pyproject_toml.__wrapped__,
1678 def test_find_user_pyproject_toml_linux(self) -> None:
1679 if system() == "Windows":
1682 # Test if XDG_CONFIG_HOME is checked
1683 with TemporaryDirectory() as workspace:
1684 tmp_user_config = Path(workspace) / "black"
1685 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1687 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1690 # Test fallback for XDG_CONFIG_HOME
1691 with patch.dict("os.environ"):
1692 os.environ.pop("XDG_CONFIG_HOME", None)
1693 fallback_user_config = Path("~/.config").expanduser() / "black"
1695 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1698 def test_find_user_pyproject_toml_windows(self) -> None:
1699 if system() != "Windows":
1702 user_config_path = Path.home() / ".black"
1704 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1707 def test_bpo_33660_workaround(self) -> None:
1708 if system() == "Windows":
1711 # https://bugs.python.org/issue33660
1713 with change_directory(root):
1714 path = Path("workspace") / "project"
1715 report = black.Report(verbose=True)
1716 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1717 self.assertEqual(normalized_path, "workspace/project")
1719 def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
1720 if system() != "Windows":
1723 with TemporaryDirectory() as workspace:
1724 root = Path(workspace)
1725 junction_dir = root / "junction"
1726 junction_target_outside_of_root = root / ".."
1727 os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
1729 report = black.Report(verbose=True)
1730 normalized_path = black.normalize_path_maybe_ignore(
1731 junction_dir, root, report
1733 # Manually delete for Python < 3.8
1734 os.system(f"rmdir {junction_dir}")
1736 self.assertEqual(normalized_path, None)
1738 def test_newline_comment_interaction(self) -> None:
1739 source = "class A:\\\r\n# type: ignore\n pass\n"
1740 output = black.format_str(source, mode=DEFAULT_MODE)
1741 black.assert_stable(source, output, mode=DEFAULT_MODE)
1743 def test_bpo_2142_workaround(self) -> None:
1744 # https://bugs.python.org/issue2142
1746 source, _ = read_data("miscellaneous", "missing_final_newline")
1747 # read_data adds a trailing newline
1748 source = source.rstrip()
1749 expected, _ = read_data("miscellaneous", "missing_final_newline.diff")
1750 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1751 diff_header = re.compile(
1752 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1753 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1756 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1757 self.assertEqual(result.exit_code, 0)
1760 actual = result.output
1761 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1762 self.assertEqual(actual, expected)
1765 def compare_results(
1766 result: click.testing.Result, expected_value: str, expected_exit_code: int
1768 """Helper method to test the value and exit code of a click Result."""
1770 result.output == expected_value
1771 ), "The output did not match the expected value."
1772 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1774 def test_code_option(self) -> None:
1775 """Test the code option with no changes."""
1776 code = 'print("Hello world")\n'
1777 args = ["--code", code]
1778 result = CliRunner().invoke(black.main, args)
1780 self.compare_results(result, code, 0)
1782 def test_code_option_changed(self) -> None:
1783 """Test the code option when changes are required."""
1784 code = "print('hello world')"
1785 formatted = black.format_str(code, mode=DEFAULT_MODE)
1787 args = ["--code", code]
1788 result = CliRunner().invoke(black.main, args)
1790 self.compare_results(result, formatted, 0)
1792 def test_code_option_check(self) -> None:
1793 """Test the code option when check is passed."""
1794 args = ["--check", "--code", 'print("Hello world")\n']
1795 result = CliRunner().invoke(black.main, args)
1796 self.compare_results(result, "", 0)
1798 def test_code_option_check_changed(self) -> None:
1799 """Test the code option when changes are required, and check is passed."""
1800 args = ["--check", "--code", "print('hello world')"]
1801 result = CliRunner().invoke(black.main, args)
1802 self.compare_results(result, "", 1)
1804 def test_code_option_diff(self) -> None:
1805 """Test the code option when diff is passed."""
1806 code = "print('hello world')"
1807 formatted = black.format_str(code, mode=DEFAULT_MODE)
1808 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1810 args = ["--diff", "--code", code]
1811 result = CliRunner().invoke(black.main, args)
1813 # Remove time from diff
1814 output = DIFF_TIME.sub("", result.output)
1816 assert output == result_diff, "The output did not match the expected value."
1817 assert result.exit_code == 0, "The exit code is incorrect."
1819 def test_code_option_color_diff(self) -> None:
1820 """Test the code option when color and diff are passed."""
1821 code = "print('hello world')"
1822 formatted = black.format_str(code, mode=DEFAULT_MODE)
1824 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1825 result_diff = color_diff(result_diff)
1827 args = ["--diff", "--color", "--code", code]
1828 result = CliRunner().invoke(black.main, args)
1830 # Remove time from diff
1831 output = DIFF_TIME.sub("", result.output)
1833 assert output == result_diff, "The output did not match the expected value."
1834 assert result.exit_code == 0, "The exit code is incorrect."
1836 @pytest.mark.incompatible_with_mypyc
1837 def test_code_option_safe(self) -> None:
1838 """Test that the code option throws an error when the sanity checks fail."""
1839 # Patch black.assert_equivalent to ensure the sanity checks fail
1840 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1841 code = 'print("Hello world")'
1842 error_msg = f"{code}\nerror: cannot format <string>: \n"
1844 args = ["--safe", "--code", code]
1845 result = CliRunner().invoke(black.main, args)
1847 self.compare_results(result, error_msg, 123)
1849 def test_code_option_fast(self) -> None:
1850 """Test that the code option ignores errors when the sanity checks fail."""
1851 # Patch black.assert_equivalent to ensure the sanity checks fail
1852 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1853 code = 'print("Hello world")'
1854 formatted = black.format_str(code, mode=DEFAULT_MODE)
1856 args = ["--fast", "--code", code]
1857 result = CliRunner().invoke(black.main, args)
1859 self.compare_results(result, formatted, 0)
1861 @pytest.mark.incompatible_with_mypyc
1862 def test_code_option_config(self) -> None:
1864 Test that the code option finds the pyproject.toml in the current directory.
1866 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1867 args = ["--code", "print"]
1868 # This is the only directory known to contain a pyproject.toml
1869 with change_directory(PROJECT_ROOT):
1870 CliRunner().invoke(black.main, args)
1871 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1874 len(parse.mock_calls) >= 1
1875 ), "Expected config parse to be called with the current directory."
1877 _, call_args, _ = parse.mock_calls[0]
1879 call_args[0].lower() == str(pyproject_path).lower()
1880 ), "Incorrect config loaded."
1882 @pytest.mark.incompatible_with_mypyc
1883 def test_code_option_parent_config(self) -> None:
1885 Test that the code option finds the pyproject.toml in the parent directory.
1887 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1888 with change_directory(THIS_DIR):
1889 args = ["--code", "print"]
1890 CliRunner().invoke(black.main, args)
1892 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1894 len(parse.mock_calls) >= 1
1895 ), "Expected config parse to be called with the current directory."
1897 _, call_args, _ = parse.mock_calls[0]
1899 call_args[0].lower() == str(pyproject_path).lower()
1900 ), "Incorrect config loaded."
1902 def test_for_handled_unexpected_eof_error(self) -> None:
1904 Test that an unexpected EOF SyntaxError is nicely presented.
1906 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1907 black.lib2to3_parse("print(", {})
1909 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1911 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1912 with pytest.raises(AssertionError) as err:
1913 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1916 # Unfortunately the SyntaxError message has changed in newer versions so we
1917 # can't match it directly.
1918 err.match("invalid character")
1919 err.match(r"\(<unknown>, line 1\)")
1923 def test_get_cache_dir(
1926 monkeypatch: pytest.MonkeyPatch,
1928 # Create multiple cache directories
1929 workspace1 = tmp_path / "ws1"
1931 workspace2 = tmp_path / "ws2"
1934 # Force user_cache_dir to use the temporary directory for easier assertions
1935 patch_user_cache_dir = patch(
1936 target="black.cache.user_cache_dir",
1938 return_value=str(workspace1),
1941 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1942 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1943 with patch_user_cache_dir:
1944 assert get_cache_dir() == workspace1
1946 # If it is set, use the path provided in the env var.
1947 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1948 assert get_cache_dir() == workspace2
1950 def test_cache_broken_file(self) -> None:
1952 with cache_dir() as workspace:
1953 cache_file = get_cache_file(mode)
1954 cache_file.write_text("this is not a pickle")
1955 assert black.read_cache(mode) == {}
1956 src = (workspace / "test.py").resolve()
1957 src.write_text("print('hello')")
1958 invokeBlack([str(src)])
1959 cache = black.read_cache(mode)
1960 assert str(src) in cache
1962 def test_cache_single_file_already_cached(self) -> None:
1964 with cache_dir() as workspace:
1965 src = (workspace / "test.py").resolve()
1966 src.write_text("print('hello')")
1967 black.write_cache({}, [src], mode)
1968 invokeBlack([str(src)])
1969 assert src.read_text() == "print('hello')"
1972 def test_cache_multiple_files(self) -> None:
1974 with cache_dir() as workspace, patch(
1975 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
1977 one = (workspace / "one.py").resolve()
1978 with one.open("w") as fobj:
1979 fobj.write("print('hello')")
1980 two = (workspace / "two.py").resolve()
1981 with two.open("w") as fobj:
1982 fobj.write("print('hello')")
1983 black.write_cache({}, [one], mode)
1984 invokeBlack([str(workspace)])
1985 with one.open("r") as fobj:
1986 assert fobj.read() == "print('hello')"
1987 with two.open("r") as fobj:
1988 assert fobj.read() == 'print("hello")\n'
1989 cache = black.read_cache(mode)
1990 assert str(one) in cache
1991 assert str(two) in cache
1993 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1994 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1996 with cache_dir() as workspace:
1997 src = (workspace / "test.py").resolve()
1998 with src.open("w") as fobj:
1999 fobj.write("print('hello')")
2000 with patch("black.read_cache") as read_cache, patch(
2003 cmd = [str(src), "--diff"]
2005 cmd.append("--color")
2007 cache_file = get_cache_file(mode)
2008 assert cache_file.exists() is False
2009 write_cache.assert_not_called()
2010 read_cache.assert_not_called()
2012 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
2014 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
2015 with cache_dir() as workspace:
2016 for tag in range(0, 4):
2017 src = (workspace / f"test{tag}.py").resolve()
2018 with src.open("w") as fobj:
2019 fobj.write("print('hello')")
2021 "black.concurrency.Manager", wraps=multiprocessing.Manager
2023 cmd = ["--diff", str(workspace)]
2025 cmd.append("--color")
2026 invokeBlack(cmd, exit_code=0)
2027 # this isn't quite doing what we want, but if it _isn't_
2028 # called then we cannot be using the lock it provides
2031 def test_no_cache_when_stdin(self) -> None:
2034 result = CliRunner().invoke(
2035 black.main, ["-"], input=BytesIO(b"print('hello')")
2037 assert not result.exit_code
2038 cache_file = get_cache_file(mode)
2039 assert not cache_file.exists()
2041 def test_read_cache_no_cachefile(self) -> None:
2044 assert black.read_cache(mode) == {}
2046 def test_write_cache_read_cache(self) -> None:
2048 with cache_dir() as workspace:
2049 src = (workspace / "test.py").resolve()
2051 black.write_cache({}, [src], mode)
2052 cache = black.read_cache(mode)
2053 assert str(src) in cache
2054 assert cache[str(src)] == black.get_cache_info(src)
2056 def test_filter_cached(self) -> None:
2057 with TemporaryDirectory() as workspace:
2058 path = Path(workspace)
2059 uncached = (path / "uncached").resolve()
2060 cached = (path / "cached").resolve()
2061 cached_but_changed = (path / "changed").resolve()
2064 cached_but_changed.touch()
2066 str(cached): black.get_cache_info(cached),
2067 str(cached_but_changed): (0.0, 0),
2069 todo, done = black.cache.filter_cached(
2070 cache, {uncached, cached, cached_but_changed}
2072 assert todo == {uncached, cached_but_changed}
2073 assert done == {cached}
2075 def test_write_cache_creates_directory_if_needed(self) -> None:
2077 with cache_dir(exists=False) as workspace:
2078 assert not workspace.exists()
2079 black.write_cache({}, [], mode)
2080 assert workspace.exists()
2083 def test_failed_formatting_does_not_get_cached(self) -> None:
2085 with cache_dir() as workspace, patch(
2086 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
2088 failing = (workspace / "failing.py").resolve()
2089 with failing.open("w") as fobj:
2090 fobj.write("not actually python")
2091 clean = (workspace / "clean.py").resolve()
2092 with clean.open("w") as fobj:
2093 fobj.write('print("hello")\n')
2094 invokeBlack([str(workspace)], exit_code=123)
2095 cache = black.read_cache(mode)
2096 assert str(failing) not in cache
2097 assert str(clean) in cache
2099 def test_write_cache_write_fail(self) -> None:
2101 with cache_dir(), patch.object(Path, "open") as mock:
2102 mock.side_effect = OSError
2103 black.write_cache({}, [], mode)
2105 def test_read_cache_line_lengths(self) -> None:
2107 short_mode = replace(DEFAULT_MODE, line_length=1)
2108 with cache_dir() as workspace:
2109 path = (workspace / "file.py").resolve()
2111 black.write_cache({}, [path], mode)
2112 one = black.read_cache(mode)
2113 assert str(path) in one
2114 two = black.read_cache(short_mode)
2115 assert str(path) not in two
2118 def assert_collected_sources(
2119 src: Sequence[Union[str, Path]],
2120 expected: Sequence[Union[str, Path]],
2122 ctx: Optional[FakeContext] = None,
2123 exclude: Optional[str] = None,
2124 include: Optional[str] = None,
2125 extend_exclude: Optional[str] = None,
2126 force_exclude: Optional[str] = None,
2127 stdin_filename: Optional[str] = None,
2129 gs_src = tuple(str(Path(s)) for s in src)
2130 gs_expected = [Path(s) for s in expected]
2131 gs_exclude = None if exclude is None else compile_pattern(exclude)
2132 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
2133 gs_extend_exclude = (
2134 None if extend_exclude is None else compile_pattern(extend_exclude)
2136 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
2137 collected = black.get_sources(
2138 ctx=ctx or FakeContext(),
2144 extend_exclude=gs_extend_exclude,
2145 force_exclude=gs_force_exclude,
2146 report=black.Report(),
2147 stdin_filename=stdin_filename,
2149 assert sorted(collected) == sorted(gs_expected)
2152 class TestFileCollection:
2153 def test_include_exclude(self) -> None:
2154 path = THIS_DIR / "data" / "include_exclude_tests"
2157 Path(path / "b/dont_exclude/a.py"),
2158 Path(path / "b/dont_exclude/a.pyi"),
2160 assert_collected_sources(
2164 exclude=r"/exclude/|/\.definitely_exclude/",
2167 def test_gitignore_used_as_default(self) -> None:
2168 base = Path(DATA_DIR / "include_exclude_tests")
2170 base / "b/.definitely_exclude/a.py",
2171 base / "b/.definitely_exclude/a.pyi",
2175 ctx.obj["root"] = base
2176 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
2178 def test_gitignore_used_on_multiple_sources(self) -> None:
2179 root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
2181 root / "dir1" / "b.py",
2182 root / "dir2" / "b.py",
2185 ctx.obj["root"] = root
2186 src = [root / "dir1", root / "dir2"]
2187 assert_collected_sources(src, expected, ctx=ctx)
2189 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2190 def test_exclude_for_issue_1572(self) -> None:
2191 # Exclude shouldn't touch files that were explicitly given to Black through the
2192 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
2193 # https://github.com/psf/black/issues/1572
2194 path = DATA_DIR / "include_exclude_tests"
2195 src = [path / "b/exclude/a.py"]
2196 expected = [path / "b/exclude/a.py"]
2197 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2199 def test_gitignore_exclude(self) -> None:
2200 path = THIS_DIR / "data" / "include_exclude_tests"
2201 include = re.compile(r"\.pyi?$")
2202 exclude = re.compile(r"")
2203 report = black.Report()
2204 gitignore = PathSpec.from_lines(
2205 "gitwildmatch", ["exclude/", ".definitely_exclude"]
2207 sources: List[Path] = []
2209 Path(path / "b/dont_exclude/a.py"),
2210 Path(path / "b/dont_exclude/a.pyi"),
2212 this_abs = THIS_DIR.resolve()
2214 black.gen_python_files(
2227 assert sorted(expected) == sorted(sources)
2229 def test_nested_gitignore(self) -> None:
2230 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
2231 include = re.compile(r"\.pyi?$")
2232 exclude = re.compile(r"")
2233 root_gitignore = black.files.get_gitignore(path)
2234 report = black.Report()
2235 expected: List[Path] = [
2236 Path(path / "x.py"),
2237 Path(path / "root/b.py"),
2238 Path(path / "root/c.py"),
2239 Path(path / "root/child/c.py"),
2241 this_abs = THIS_DIR.resolve()
2243 black.gen_python_files(
2251 {path: root_gitignore},
2256 assert sorted(expected) == sorted(sources)
2258 def test_nested_gitignore_directly_in_source_directory(self) -> None:
2259 # https://github.com/psf/black/issues/2598
2260 path = Path(DATA_DIR / "nested_gitignore_tests")
2261 src = Path(path / "root" / "child")
2262 expected = [src / "a.py", src / "c.py"]
2263 assert_collected_sources([src], expected)
2265 def test_invalid_gitignore(self) -> None:
2266 path = THIS_DIR / "data" / "invalid_gitignore_tests"
2267 empty_config = path / "pyproject.toml"
2268 result = BlackRunner().invoke(
2269 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2271 assert result.exit_code == 1
2272 assert result.stderr_bytes is not None
2274 gitignore = path / ".gitignore"
2275 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2277 def test_invalid_nested_gitignore(self) -> None:
2278 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
2279 empty_config = path / "pyproject.toml"
2280 result = BlackRunner().invoke(
2281 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2283 assert result.exit_code == 1
2284 assert result.stderr_bytes is not None
2286 gitignore = path / "a" / ".gitignore"
2287 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2289 def test_gitignore_that_ignores_subfolders(self) -> None:
2290 # If gitignore with */* is in root
2291 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir")
2292 expected = [root / "b.py"]
2294 ctx.obj["root"] = root
2295 assert_collected_sources([root], expected, ctx=ctx)
2297 # If .gitignore with */* is nested
2298 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2301 root / "subdir" / "b.py",
2304 ctx.obj["root"] = root
2305 assert_collected_sources([root], expected, ctx=ctx)
2307 # If command is executed from outer dir
2308 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2309 target = root / "subdir"
2310 expected = [target / "b.py"]
2312 ctx.obj["root"] = root
2313 assert_collected_sources([target], expected, ctx=ctx)
2315 def test_empty_include(self) -> None:
2316 path = DATA_DIR / "include_exclude_tests"
2319 Path(path / "b/exclude/a.pie"),
2320 Path(path / "b/exclude/a.py"),
2321 Path(path / "b/exclude/a.pyi"),
2322 Path(path / "b/dont_exclude/a.pie"),
2323 Path(path / "b/dont_exclude/a.py"),
2324 Path(path / "b/dont_exclude/a.pyi"),
2325 Path(path / "b/.definitely_exclude/a.pie"),
2326 Path(path / "b/.definitely_exclude/a.py"),
2327 Path(path / "b/.definitely_exclude/a.pyi"),
2328 Path(path / ".gitignore"),
2329 Path(path / "pyproject.toml"),
2331 # Setting exclude explicitly to an empty string to block .gitignore usage.
2332 assert_collected_sources(src, expected, include="", exclude="")
2334 def test_extend_exclude(self) -> None:
2335 path = DATA_DIR / "include_exclude_tests"
2338 Path(path / "b/exclude/a.py"),
2339 Path(path / "b/dont_exclude/a.py"),
2341 assert_collected_sources(
2342 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
2345 @pytest.mark.incompatible_with_mypyc
2346 def test_symlink_out_of_root_directory(self) -> None:
2348 root = THIS_DIR.resolve()
2350 include = re.compile(black.DEFAULT_INCLUDES)
2351 exclude = re.compile(black.DEFAULT_EXCLUDES)
2352 report = black.Report()
2353 gitignore = PathSpec.from_lines("gitwildmatch", [])
2354 # `child` should behave like a symlink which resolved path is clearly
2355 # outside of the `root` directory.
2356 path.iterdir.return_value = [child]
2357 child.resolve.return_value = Path("/a/b/c")
2358 child.as_posix.return_value = "/a/b/c"
2361 black.gen_python_files(
2374 except ValueError as ve:
2375 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
2376 path.iterdir.assert_called_once()
2377 child.resolve.assert_called_once()
2379 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2380 def test_get_sources_with_stdin(self) -> None:
2383 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2385 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2386 def test_get_sources_with_stdin_filename(self) -> None:
2388 stdin_filename = str(THIS_DIR / "data/collections.py")
2389 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2390 assert_collected_sources(
2393 exclude=r"/exclude/a\.py",
2394 stdin_filename=stdin_filename,
2397 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2398 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2399 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2400 # file being passed directly. This is the same as
2401 # test_exclude_for_issue_1572
2402 path = DATA_DIR / "include_exclude_tests"
2404 stdin_filename = str(path / "b/exclude/a.py")
2405 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2406 assert_collected_sources(
2409 exclude=r"/exclude/|a\.py",
2410 stdin_filename=stdin_filename,
2413 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2414 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2415 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2416 # file being passed directly. This is the same as
2417 # test_exclude_for_issue_1572
2419 path = THIS_DIR / "data" / "include_exclude_tests"
2420 stdin_filename = str(path / "b/exclude/a.py")
2421 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2422 assert_collected_sources(
2425 extend_exclude=r"/exclude/|a\.py",
2426 stdin_filename=stdin_filename,
2429 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2430 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2431 # Force exclude should exclude the file when passing it through
2433 path = THIS_DIR / "data" / "include_exclude_tests"
2434 stdin_filename = str(path / "b/exclude/a.py")
2435 assert_collected_sources(
2438 force_exclude=r"/exclude/|a\.py",
2439 stdin_filename=stdin_filename,
2444 with open(black.__file__, "r", encoding="utf-8") as _bf:
2445 black_source_lines = _bf.readlines()
2446 except UnicodeDecodeError:
2447 if not black.COMPILED:
2452 frame: types.FrameType, event: str, arg: Any
2453 ) -> Callable[[types.FrameType, str, Any], Any]:
2454 """Show function calls `from black/__init__.py` as they happen.
2456 Register this with `sys.settrace()` in a test you're debugging.
2461 stack = len(inspect.stack()) - 19
2463 filename = frame.f_code.co_filename
2464 lineno = frame.f_lineno
2465 func_sig_lineno = lineno - 1
2466 funcname = black_source_lines[func_sig_lineno].strip()
2467 while funcname.startswith("@"):
2468 func_sig_lineno += 1
2469 funcname = black_source_lines[func_sig_lineno].strip()
2470 if "black/__init__.py" in filename:
2471 print(f"{' ' * stack}{lineno}:{funcname}")