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_expression_ff(self) -> None:
275 source, expected = read_data("simple_cases", "expression.py")
276 tmp_file = Path(black.dump_to_file(source))
278 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
279 with open(tmp_file, encoding="utf8") as f:
283 self.assertFormatEqual(expected, actual)
284 with patch("black.dump_to_file", dump_to_stderr):
285 black.assert_equivalent(source, actual)
286 black.assert_stable(source, actual, DEFAULT_MODE)
288 def test_expression_diff(self) -> None:
289 source, _ = read_data("simple_cases", "expression.py")
290 expected, _ = read_data("simple_cases", "expression.diff")
291 tmp_file = Path(black.dump_to_file(source))
292 diff_header = re.compile(
293 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
294 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
297 result = BlackRunner().invoke(
298 black.main, ["--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
300 self.assertEqual(result.exit_code, 0)
303 actual = result.output
304 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
305 if expected != actual:
306 dump = black.dump_to_file(actual)
308 "Expected diff isn't equal to the actual. If you made changes to"
309 " expression.py and this is an anticipated difference, overwrite"
310 f" tests/data/expression.diff with {dump}"
312 self.assertEqual(expected, actual, msg)
314 def test_expression_diff_with_color(self) -> None:
315 source, _ = read_data("simple_cases", "expression.py")
316 expected, _ = read_data("simple_cases", "expression.diff")
317 tmp_file = Path(black.dump_to_file(source))
319 result = BlackRunner().invoke(
321 ["--diff", "--color", str(tmp_file), f"--config={EMPTY_CONFIG}"],
325 actual = result.output
326 # We check the contents of the diff in `test_expression_diff`. All
327 # we need to check here is that color codes exist in the result.
328 self.assertIn("\033[1m", actual)
329 self.assertIn("\033[36m", actual)
330 self.assertIn("\033[32m", actual)
331 self.assertIn("\033[31m", actual)
332 self.assertIn("\033[0m", actual)
334 def test_detect_pos_only_arguments(self) -> None:
335 source, _ = read_data("py_38", "pep_570")
336 root = black.lib2to3_parse(source)
337 features = black.get_features_used(root)
338 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
339 versions = black.detect_target_versions(root)
340 self.assertIn(black.TargetVersion.PY38, versions)
342 def test_detect_debug_f_strings(self) -> None:
343 root = black.lib2to3_parse("""f"{x=}" """)
344 features = black.get_features_used(root)
345 self.assertIn(black.Feature.DEBUG_F_STRINGS, features)
346 versions = black.detect_target_versions(root)
347 self.assertIn(black.TargetVersion.PY38, versions)
349 root = black.lib2to3_parse(
350 """f"{x}"\nf'{"="}'\nf'{(x:=5)}'\nf'{f(a="3=")}'\nf'{x:=10}'\n"""
352 features = black.get_features_used(root)
353 self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
355 # We don't yet support feature version detection in nested f-strings
356 root = black.lib2to3_parse(
357 """f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """
359 features = black.get_features_used(root)
360 self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
362 @patch("black.dump_to_file", dump_to_stderr)
363 def test_string_quotes(self) -> None:
364 source, expected = read_data("miscellaneous", "string_quotes")
365 mode = black.Mode(preview=True)
366 assert_format(source, expected, mode)
367 mode = replace(mode, string_normalization=False)
368 not_normalized = fs(source, mode=mode)
369 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
370 black.assert_equivalent(source, not_normalized)
371 black.assert_stable(source, not_normalized, mode=mode)
373 def test_skip_source_first_line(self) -> None:
374 source, _ = read_data("miscellaneous", "invalid_header")
375 tmp_file = Path(black.dump_to_file(source))
376 # Full source should fail (invalid syntax at header)
377 self.invokeBlack([str(tmp_file), "--diff", "--check"], exit_code=123)
378 # So, skipping the first line should work
379 result = BlackRunner().invoke(
380 black.main, [str(tmp_file), "-x", f"--config={EMPTY_CONFIG}"]
382 self.assertEqual(result.exit_code, 0)
383 with open(tmp_file, encoding="utf8") as f:
385 self.assertFormatEqual(source, actual)
387 def test_skip_source_first_line_when_mixing_newlines(self) -> None:
388 code_mixing_newlines = b"Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n"
389 expected = b"Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n"
390 with TemporaryDirectory() as workspace:
391 test_file = Path(workspace) / "skip_header.py"
392 test_file.write_bytes(code_mixing_newlines)
393 mode = replace(DEFAULT_MODE, skip_source_first_line=True)
394 ff(test_file, mode=mode, write_back=black.WriteBack.YES)
395 self.assertEqual(test_file.read_bytes(), expected)
397 def test_skip_magic_trailing_comma(self) -> None:
398 source, _ = read_data("simple_cases", "expression")
399 expected, _ = read_data(
400 "miscellaneous", "expression_skip_magic_trailing_comma.diff"
402 tmp_file = Path(black.dump_to_file(source))
403 diff_header = re.compile(
404 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
405 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
408 result = BlackRunner().invoke(
409 black.main, ["-C", "--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
411 self.assertEqual(result.exit_code, 0)
414 actual = result.output
415 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
416 actual = actual.rstrip() + "\n" # the diff output has a trailing space
417 if expected != actual:
418 dump = black.dump_to_file(actual)
420 "Expected diff isn't equal to the actual. If you made changes to"
421 " expression.py and this is an anticipated difference, overwrite"
422 " tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff"
425 self.assertEqual(expected, actual, msg)
427 @patch("black.dump_to_file", dump_to_stderr)
428 def test_async_as_identifier(self) -> None:
429 source_path = get_case_path("miscellaneous", "async_as_identifier")
430 source, expected = read_data_from_file(source_path)
432 self.assertFormatEqual(expected, actual)
433 major, minor = sys.version_info[:2]
434 if major < 3 or (major <= 3 and minor < 7):
435 black.assert_equivalent(source, actual)
436 black.assert_stable(source, actual, DEFAULT_MODE)
437 # ensure black can parse this when the target is 3.6
438 self.invokeBlack([str(source_path), "--target-version", "py36"])
439 # but not on 3.7, because async/await is no longer an identifier
440 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
442 @patch("black.dump_to_file", dump_to_stderr)
443 def test_python37(self) -> None:
444 source_path = get_case_path("py_37", "python37")
445 source, expected = read_data_from_file(source_path)
447 self.assertFormatEqual(expected, actual)
448 major, minor = sys.version_info[:2]
449 if major > 3 or (major == 3 and minor >= 7):
450 black.assert_equivalent(source, actual)
451 black.assert_stable(source, actual, DEFAULT_MODE)
452 # ensure black can parse this when the target is 3.7
453 self.invokeBlack([str(source_path), "--target-version", "py37"])
454 # but not on 3.6, because we use async as a reserved keyword
455 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
457 def test_tab_comment_indentation(self) -> None:
458 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
459 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
460 self.assertFormatEqual(contents_spc, fs(contents_spc))
461 self.assertFormatEqual(contents_spc, fs(contents_tab))
463 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
464 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
465 self.assertFormatEqual(contents_spc, fs(contents_spc))
466 self.assertFormatEqual(contents_spc, fs(contents_tab))
468 # mixed tabs and spaces (valid Python 2 code)
469 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
470 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
471 self.assertFormatEqual(contents_spc, fs(contents_spc))
472 self.assertFormatEqual(contents_spc, fs(contents_tab))
474 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
475 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
476 self.assertFormatEqual(contents_spc, fs(contents_spc))
477 self.assertFormatEqual(contents_spc, fs(contents_tab))
479 def test_false_positive_symlink_output_issue_3384(self) -> None:
480 # Emulate the behavior when using the CLI (`black ./child --verbose`), which
481 # involves patching some `pathlib.Path` methods. In particular, `is_dir` is
482 # patched only on its first call: when checking if "./child" is a directory it
483 # should return True. The "./child" folder exists relative to the cwd when
484 # running from CLI, but fails when running the tests because cwd is different
485 project_root = Path(THIS_DIR / "data" / "nested_gitignore_tests")
486 working_directory = project_root / "root"
487 target_abspath = working_directory / "child"
489 src.relative_to(working_directory) for src in target_abspath.iterdir()
492 def mock_n_calls(responses: List[bool]) -> Callable[[], bool]:
493 def _mocked_calls() -> bool:
495 return responses.pop(0)
500 with patch("pathlib.Path.iterdir", return_value=target_contents), patch(
501 "pathlib.Path.cwd", return_value=working_directory
502 ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])):
504 ctx.obj["root"] = project_root
505 report = MagicMock(verbose=True)
511 include=DEFAULT_INCLUDE,
519 mock_args[1].startswith("is a symbolic link that points outside")
520 for _, mock_args, _ in report.path_ignored.mock_calls
521 ), "A symbolic link was reported."
522 report.path_ignored.assert_called_once_with(
523 Path("child", "b.py"), "matches a .gitignore file content"
526 def test_report_verbose(self) -> None:
527 report = Report(verbose=True)
531 def out(msg: str, **kwargs: Any) -> None:
532 out_lines.append(msg)
534 def err(msg: str, **kwargs: Any) -> None:
535 err_lines.append(msg)
537 with patch("black.output._out", out), patch("black.output._err", err):
538 report.done(Path("f1"), black.Changed.NO)
539 self.assertEqual(len(out_lines), 1)
540 self.assertEqual(len(err_lines), 0)
541 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
542 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
543 self.assertEqual(report.return_code, 0)
544 report.done(Path("f2"), black.Changed.YES)
545 self.assertEqual(len(out_lines), 2)
546 self.assertEqual(len(err_lines), 0)
547 self.assertEqual(out_lines[-1], "reformatted f2")
549 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
551 report.done(Path("f3"), black.Changed.CACHED)
552 self.assertEqual(len(out_lines), 3)
553 self.assertEqual(len(err_lines), 0)
555 out_lines[-1], "f3 wasn't modified on disk since last run."
558 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
560 self.assertEqual(report.return_code, 0)
562 self.assertEqual(report.return_code, 1)
564 report.failed(Path("e1"), "boom")
565 self.assertEqual(len(out_lines), 3)
566 self.assertEqual(len(err_lines), 1)
567 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
569 unstyle(str(report)),
571 "1 file reformatted, 2 files left unchanged, 1 file failed to"
575 self.assertEqual(report.return_code, 123)
576 report.done(Path("f3"), black.Changed.YES)
577 self.assertEqual(len(out_lines), 4)
578 self.assertEqual(len(err_lines), 1)
579 self.assertEqual(out_lines[-1], "reformatted f3")
581 unstyle(str(report)),
583 "2 files reformatted, 2 files left unchanged, 1 file failed to"
587 self.assertEqual(report.return_code, 123)
588 report.failed(Path("e2"), "boom")
589 self.assertEqual(len(out_lines), 4)
590 self.assertEqual(len(err_lines), 2)
591 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
593 unstyle(str(report)),
595 "2 files reformatted, 2 files left unchanged, 2 files failed to"
599 self.assertEqual(report.return_code, 123)
600 report.path_ignored(Path("wat"), "no match")
601 self.assertEqual(len(out_lines), 5)
602 self.assertEqual(len(err_lines), 2)
603 self.assertEqual(out_lines[-1], "wat ignored: no match")
605 unstyle(str(report)),
607 "2 files reformatted, 2 files left unchanged, 2 files failed to"
611 self.assertEqual(report.return_code, 123)
612 report.done(Path("f4"), black.Changed.NO)
613 self.assertEqual(len(out_lines), 6)
614 self.assertEqual(len(err_lines), 2)
615 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
617 unstyle(str(report)),
619 "2 files reformatted, 3 files left unchanged, 2 files failed to"
623 self.assertEqual(report.return_code, 123)
626 unstyle(str(report)),
628 "2 files would be reformatted, 3 files would be left unchanged, 2"
629 " files would fail to reformat."
635 unstyle(str(report)),
637 "2 files would be reformatted, 3 files would be left unchanged, 2"
638 " files would fail to reformat."
642 def test_report_quiet(self) -> None:
643 report = Report(quiet=True)
647 def out(msg: str, **kwargs: Any) -> None:
648 out_lines.append(msg)
650 def err(msg: str, **kwargs: Any) -> None:
651 err_lines.append(msg)
653 with patch("black.output._out", out), patch("black.output._err", err):
654 report.done(Path("f1"), black.Changed.NO)
655 self.assertEqual(len(out_lines), 0)
656 self.assertEqual(len(err_lines), 0)
657 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
658 self.assertEqual(report.return_code, 0)
659 report.done(Path("f2"), black.Changed.YES)
660 self.assertEqual(len(out_lines), 0)
661 self.assertEqual(len(err_lines), 0)
663 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
665 report.done(Path("f3"), black.Changed.CACHED)
666 self.assertEqual(len(out_lines), 0)
667 self.assertEqual(len(err_lines), 0)
669 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
671 self.assertEqual(report.return_code, 0)
673 self.assertEqual(report.return_code, 1)
675 report.failed(Path("e1"), "boom")
676 self.assertEqual(len(out_lines), 0)
677 self.assertEqual(len(err_lines), 1)
678 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
680 unstyle(str(report)),
682 "1 file reformatted, 2 files left unchanged, 1 file failed to"
686 self.assertEqual(report.return_code, 123)
687 report.done(Path("f3"), black.Changed.YES)
688 self.assertEqual(len(out_lines), 0)
689 self.assertEqual(len(err_lines), 1)
691 unstyle(str(report)),
693 "2 files reformatted, 2 files left unchanged, 1 file failed to"
697 self.assertEqual(report.return_code, 123)
698 report.failed(Path("e2"), "boom")
699 self.assertEqual(len(out_lines), 0)
700 self.assertEqual(len(err_lines), 2)
701 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
703 unstyle(str(report)),
705 "2 files reformatted, 2 files left unchanged, 2 files failed to"
709 self.assertEqual(report.return_code, 123)
710 report.path_ignored(Path("wat"), "no match")
711 self.assertEqual(len(out_lines), 0)
712 self.assertEqual(len(err_lines), 2)
714 unstyle(str(report)),
716 "2 files reformatted, 2 files left unchanged, 2 files failed to"
720 self.assertEqual(report.return_code, 123)
721 report.done(Path("f4"), black.Changed.NO)
722 self.assertEqual(len(out_lines), 0)
723 self.assertEqual(len(err_lines), 2)
725 unstyle(str(report)),
727 "2 files reformatted, 3 files left unchanged, 2 files failed to"
731 self.assertEqual(report.return_code, 123)
734 unstyle(str(report)),
736 "2 files would be reformatted, 3 files would be left unchanged, 2"
737 " files would fail to reformat."
743 unstyle(str(report)),
745 "2 files would be reformatted, 3 files would be left unchanged, 2"
746 " files would fail to reformat."
750 def test_report_normal(self) -> None:
751 report = black.Report()
755 def out(msg: str, **kwargs: Any) -> None:
756 out_lines.append(msg)
758 def err(msg: str, **kwargs: Any) -> None:
759 err_lines.append(msg)
761 with patch("black.output._out", out), patch("black.output._err", err):
762 report.done(Path("f1"), black.Changed.NO)
763 self.assertEqual(len(out_lines), 0)
764 self.assertEqual(len(err_lines), 0)
765 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
766 self.assertEqual(report.return_code, 0)
767 report.done(Path("f2"), black.Changed.YES)
768 self.assertEqual(len(out_lines), 1)
769 self.assertEqual(len(err_lines), 0)
770 self.assertEqual(out_lines[-1], "reformatted f2")
772 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
774 report.done(Path("f3"), black.Changed.CACHED)
775 self.assertEqual(len(out_lines), 1)
776 self.assertEqual(len(err_lines), 0)
777 self.assertEqual(out_lines[-1], "reformatted f2")
779 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
781 self.assertEqual(report.return_code, 0)
783 self.assertEqual(report.return_code, 1)
785 report.failed(Path("e1"), "boom")
786 self.assertEqual(len(out_lines), 1)
787 self.assertEqual(len(err_lines), 1)
788 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
790 unstyle(str(report)),
792 "1 file reformatted, 2 files left unchanged, 1 file failed to"
796 self.assertEqual(report.return_code, 123)
797 report.done(Path("f3"), black.Changed.YES)
798 self.assertEqual(len(out_lines), 2)
799 self.assertEqual(len(err_lines), 1)
800 self.assertEqual(out_lines[-1], "reformatted f3")
802 unstyle(str(report)),
804 "2 files reformatted, 2 files left unchanged, 1 file failed to"
808 self.assertEqual(report.return_code, 123)
809 report.failed(Path("e2"), "boom")
810 self.assertEqual(len(out_lines), 2)
811 self.assertEqual(len(err_lines), 2)
812 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
814 unstyle(str(report)),
816 "2 files reformatted, 2 files left unchanged, 2 files failed to"
820 self.assertEqual(report.return_code, 123)
821 report.path_ignored(Path("wat"), "no match")
822 self.assertEqual(len(out_lines), 2)
823 self.assertEqual(len(err_lines), 2)
825 unstyle(str(report)),
827 "2 files reformatted, 2 files left unchanged, 2 files failed to"
831 self.assertEqual(report.return_code, 123)
832 report.done(Path("f4"), black.Changed.NO)
833 self.assertEqual(len(out_lines), 2)
834 self.assertEqual(len(err_lines), 2)
836 unstyle(str(report)),
838 "2 files reformatted, 3 files left unchanged, 2 files failed to"
842 self.assertEqual(report.return_code, 123)
845 unstyle(str(report)),
847 "2 files would be reformatted, 3 files would be left unchanged, 2"
848 " files would fail to reformat."
854 unstyle(str(report)),
856 "2 files would be reformatted, 3 files would be left unchanged, 2"
857 " files would fail to reformat."
861 def test_lib2to3_parse(self) -> None:
862 with self.assertRaises(black.InvalidInput):
863 black.lib2to3_parse("invalid syntax")
866 black.lib2to3_parse(straddling)
867 black.lib2to3_parse(straddling, {TargetVersion.PY36})
870 with self.assertRaises(black.InvalidInput):
871 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
873 py3_only = "exec(x, end=y)"
874 black.lib2to3_parse(py3_only)
875 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
877 def test_get_features_used_decorator(self) -> None:
878 # Test the feature detection of new decorator syntax
879 # since this makes some test cases of test_get_features_used()
880 # fails if it fails, this is tested first so that a useful case
882 simples, relaxed = read_data("miscellaneous", "decorators")
883 # skip explanation comments at the top of the file
884 for simple_test in simples.split("##")[1:]:
885 node = black.lib2to3_parse(simple_test)
886 decorator = str(node.children[0].children[0]).strip()
888 Feature.RELAXED_DECORATORS,
889 black.get_features_used(node),
891 f"decorator '{decorator}' follows python<=3.8 syntax"
892 "but is detected as 3.9+"
893 # f"The full node is\n{node!r}"
896 # skip the '# output' comment at the top of the output part
897 for relaxed_test in relaxed.split("##")[1:]:
898 node = black.lib2to3_parse(relaxed_test)
899 decorator = str(node.children[0].children[0]).strip()
901 Feature.RELAXED_DECORATORS,
902 black.get_features_used(node),
904 f"decorator '{decorator}' uses python3.9+ syntax"
905 "but is detected as python<=3.8"
906 # f"The full node is\n{node!r}"
910 def test_get_features_used(self) -> None:
911 node = black.lib2to3_parse("def f(*, arg): ...\n")
912 self.assertEqual(black.get_features_used(node), set())
913 node = black.lib2to3_parse("def f(*, arg,): ...\n")
914 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
915 node = black.lib2to3_parse("f(*arg,)\n")
917 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
919 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
920 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
921 node = black.lib2to3_parse("123_456\n")
922 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
923 node = black.lib2to3_parse("123456\n")
924 self.assertEqual(black.get_features_used(node), set())
925 source, expected = read_data("simple_cases", "function")
926 node = black.lib2to3_parse(source)
927 expected_features = {
928 Feature.TRAILING_COMMA_IN_CALL,
929 Feature.TRAILING_COMMA_IN_DEF,
932 self.assertEqual(black.get_features_used(node), expected_features)
933 node = black.lib2to3_parse(expected)
934 self.assertEqual(black.get_features_used(node), expected_features)
935 source, expected = read_data("simple_cases", "expression")
936 node = black.lib2to3_parse(source)
937 self.assertEqual(black.get_features_used(node), set())
938 node = black.lib2to3_parse(expected)
939 self.assertEqual(black.get_features_used(node), set())
940 node = black.lib2to3_parse("lambda a, /, b: ...")
941 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
942 node = black.lib2to3_parse("def fn(a, /, b): ...")
943 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
944 node = black.lib2to3_parse("def fn(): yield a, b")
945 self.assertEqual(black.get_features_used(node), set())
946 node = black.lib2to3_parse("def fn(): return a, b")
947 self.assertEqual(black.get_features_used(node), set())
948 node = black.lib2to3_parse("def fn(): yield *b, c")
949 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
950 node = black.lib2to3_parse("def fn(): return a, *b, c")
951 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
952 node = black.lib2to3_parse("x = a, *b, c")
953 self.assertEqual(black.get_features_used(node), set())
954 node = black.lib2to3_parse("x: Any = regular")
955 self.assertEqual(black.get_features_used(node), set())
956 node = black.lib2to3_parse("x: Any = (regular, regular)")
957 self.assertEqual(black.get_features_used(node), set())
958 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
959 self.assertEqual(black.get_features_used(node), set())
960 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
962 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
964 node = black.lib2to3_parse("try: pass\nexcept Something: pass")
965 self.assertEqual(black.get_features_used(node), set())
966 node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass")
967 self.assertEqual(black.get_features_used(node), set())
968 node = black.lib2to3_parse("try: pass\nexcept *Group: pass")
969 self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR})
970 node = black.lib2to3_parse("a[*b]")
971 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
972 node = black.lib2to3_parse("a[x, *y(), z] = t")
973 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
974 node = black.lib2to3_parse("def fn(*args: *T): pass")
975 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
977 def test_get_features_used_for_future_flags(self) -> None:
978 for src, features in [
979 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
981 "from __future__ import (other, annotations)",
982 {Feature.FUTURE_ANNOTATIONS},
984 ("a = 1 + 2\nfrom something import annotations", set()),
985 ("from __future__ import x, y", set()),
987 with self.subTest(src=src, features=features):
988 node = black.lib2to3_parse(src)
989 future_imports = black.get_future_imports(node)
991 black.get_features_used(node, future_imports=future_imports),
995 def test_get_future_imports(self) -> None:
996 node = black.lib2to3_parse("\n")
997 self.assertEqual(set(), black.get_future_imports(node))
998 node = black.lib2to3_parse("from __future__ import black\n")
999 self.assertEqual({"black"}, black.get_future_imports(node))
1000 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
1001 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
1002 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
1003 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
1004 node = black.lib2to3_parse(
1005 "from __future__ import multiple\nfrom __future__ import imports\n"
1007 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
1008 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
1009 self.assertEqual({"black"}, black.get_future_imports(node))
1010 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
1011 self.assertEqual({"black"}, black.get_future_imports(node))
1012 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
1013 self.assertEqual(set(), black.get_future_imports(node))
1014 node = black.lib2to3_parse("from some.module import black\n")
1015 self.assertEqual(set(), black.get_future_imports(node))
1016 node = black.lib2to3_parse(
1017 "from __future__ import unicode_literals as _unicode_literals"
1019 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
1020 node = black.lib2to3_parse(
1021 "from __future__ import unicode_literals as _lol, print"
1023 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
1025 @pytest.mark.incompatible_with_mypyc
1026 def test_debug_visitor(self) -> None:
1027 source, _ = read_data("miscellaneous", "debug_visitor")
1028 expected, _ = read_data("miscellaneous", "debug_visitor.out")
1032 def out(msg: str, **kwargs: Any) -> None:
1033 out_lines.append(msg)
1035 def err(msg: str, **kwargs: Any) -> None:
1036 err_lines.append(msg)
1038 with patch("black.debug.out", out):
1039 DebugVisitor.show(source)
1040 actual = "\n".join(out_lines) + "\n"
1042 if expected != actual:
1043 log_name = black.dump_to_file(*out_lines)
1047 f"AST print out is different. Actual version dumped to {log_name}",
1050 def test_format_file_contents(self) -> None:
1053 with self.assertRaises(black.NothingChanged):
1054 black.format_file_contents(empty, mode=mode, fast=False)
1056 with self.assertRaises(black.NothingChanged):
1057 black.format_file_contents(just_nl, mode=mode, fast=False)
1058 same = "j = [1, 2, 3]\n"
1059 with self.assertRaises(black.NothingChanged):
1060 black.format_file_contents(same, mode=mode, fast=False)
1061 different = "j = [1,2,3]"
1063 actual = black.format_file_contents(different, mode=mode, fast=False)
1064 self.assertEqual(expected, actual)
1065 invalid = "return if you can"
1066 with self.assertRaises(black.InvalidInput) as e:
1067 black.format_file_contents(invalid, mode=mode, fast=False)
1068 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
1070 mode = black.Mode(preview=True)
1072 with self.assertRaises(black.NothingChanged):
1073 black.format_file_contents(just_crlf, mode=mode, fast=False)
1074 just_whitespace_nl = "\n\t\n \n\t \n \t\n\n"
1075 actual = black.format_file_contents(just_whitespace_nl, mode=mode, fast=False)
1076 self.assertEqual("\n", actual)
1077 just_whitespace_crlf = "\r\n\t\r\n \r\n\t \r\n \t\r\n\r\n"
1078 actual = black.format_file_contents(just_whitespace_crlf, mode=mode, fast=False)
1079 self.assertEqual("\r\n", actual)
1081 def test_endmarker(self) -> None:
1082 n = black.lib2to3_parse("\n")
1083 self.assertEqual(n.type, black.syms.file_input)
1084 self.assertEqual(len(n.children), 1)
1085 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
1087 @pytest.mark.incompatible_with_mypyc
1088 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
1089 def test_assertFormatEqual(self) -> None:
1093 def out(msg: str, **kwargs: Any) -> None:
1094 out_lines.append(msg)
1096 def err(msg: str, **kwargs: Any) -> None:
1097 err_lines.append(msg)
1099 with patch("black.output._out", out), patch("black.output._err", err):
1100 with self.assertRaises(AssertionError):
1101 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
1103 out_str = "".join(out_lines)
1104 self.assertIn("Expected tree:", out_str)
1105 self.assertIn("Actual tree:", out_str)
1106 self.assertEqual("".join(err_lines), "")
1109 @patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1110 def test_works_in_mono_process_only_environment(self) -> None:
1111 with cache_dir() as workspace:
1113 (workspace / "one.py").resolve(),
1114 (workspace / "two.py").resolve(),
1116 f.write_text('print("hello")\n')
1117 self.invokeBlack([str(workspace)])
1120 def test_check_diff_use_together(self) -> None:
1122 # Files which will be reformatted.
1123 src1 = get_case_path("miscellaneous", "string_quotes")
1124 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1125 # Files which will not be reformatted.
1126 src2 = get_case_path("simple_cases", "composition")
1127 self.invokeBlack([str(src2), "--diff", "--check"])
1128 # Multi file command.
1129 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1131 def test_no_src_fails(self) -> None:
1133 self.invokeBlack([], exit_code=1)
1135 def test_src_and_code_fails(self) -> None:
1137 self.invokeBlack([".", "-c", "0"], exit_code=1)
1139 def test_broken_symlink(self) -> None:
1140 with cache_dir() as workspace:
1141 symlink = workspace / "broken_link.py"
1143 symlink.symlink_to("nonexistent.py")
1144 except (OSError, NotImplementedError) as e:
1145 self.skipTest(f"Can't create symlinks: {e}")
1146 self.invokeBlack([str(workspace.resolve())])
1148 def test_single_file_force_pyi(self) -> None:
1149 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1150 contents, expected = read_data("miscellaneous", "force_pyi")
1151 with cache_dir() as workspace:
1152 path = (workspace / "file.py").resolve()
1153 with open(path, "w") as fh:
1155 self.invokeBlack([str(path), "--pyi"])
1156 with open(path, "r") as fh:
1158 # verify cache with --pyi is separate
1159 pyi_cache = black.read_cache(pyi_mode)
1160 self.assertIn(str(path), pyi_cache)
1161 normal_cache = black.read_cache(DEFAULT_MODE)
1162 self.assertNotIn(str(path), normal_cache)
1163 self.assertFormatEqual(expected, actual)
1164 black.assert_equivalent(contents, actual)
1165 black.assert_stable(contents, actual, pyi_mode)
1168 def test_multi_file_force_pyi(self) -> None:
1169 reg_mode = DEFAULT_MODE
1170 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1171 contents, expected = read_data("miscellaneous", "force_pyi")
1172 with cache_dir() as workspace:
1174 (workspace / "file1.py").resolve(),
1175 (workspace / "file2.py").resolve(),
1178 with open(path, "w") as fh:
1180 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1182 with open(path, "r") as fh:
1184 self.assertEqual(actual, expected)
1185 # verify cache with --pyi is separate
1186 pyi_cache = black.read_cache(pyi_mode)
1187 normal_cache = black.read_cache(reg_mode)
1189 self.assertIn(str(path), pyi_cache)
1190 self.assertNotIn(str(path), normal_cache)
1192 def test_pipe_force_pyi(self) -> None:
1193 source, expected = read_data("miscellaneous", "force_pyi")
1194 result = CliRunner().invoke(
1195 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1197 self.assertEqual(result.exit_code, 0)
1198 actual = result.output
1199 self.assertFormatEqual(actual, expected)
1201 def test_single_file_force_py36(self) -> None:
1202 reg_mode = DEFAULT_MODE
1203 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1204 source, expected = read_data("miscellaneous", "force_py36")
1205 with cache_dir() as workspace:
1206 path = (workspace / "file.py").resolve()
1207 with open(path, "w") as fh:
1209 self.invokeBlack([str(path), *PY36_ARGS])
1210 with open(path, "r") as fh:
1212 # verify cache with --target-version is separate
1213 py36_cache = black.read_cache(py36_mode)
1214 self.assertIn(str(path), py36_cache)
1215 normal_cache = black.read_cache(reg_mode)
1216 self.assertNotIn(str(path), normal_cache)
1217 self.assertEqual(actual, expected)
1220 def test_multi_file_force_py36(self) -> None:
1221 reg_mode = DEFAULT_MODE
1222 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1223 source, expected = read_data("miscellaneous", "force_py36")
1224 with cache_dir() as workspace:
1226 (workspace / "file1.py").resolve(),
1227 (workspace / "file2.py").resolve(),
1230 with open(path, "w") as fh:
1232 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1234 with open(path, "r") as fh:
1236 self.assertEqual(actual, expected)
1237 # verify cache with --target-version is separate
1238 pyi_cache = black.read_cache(py36_mode)
1239 normal_cache = black.read_cache(reg_mode)
1241 self.assertIn(str(path), pyi_cache)
1242 self.assertNotIn(str(path), normal_cache)
1244 def test_pipe_force_py36(self) -> None:
1245 source, expected = read_data("miscellaneous", "force_py36")
1246 result = CliRunner().invoke(
1248 ["-", "-q", "--target-version=py36"],
1249 input=BytesIO(source.encode("utf8")),
1251 self.assertEqual(result.exit_code, 0)
1252 actual = result.output
1253 self.assertFormatEqual(actual, expected)
1255 @pytest.mark.incompatible_with_mypyc
1256 def test_reformat_one_with_stdin(self) -> None:
1258 "black.format_stdin_to_stdout",
1259 return_value=lambda *args, **kwargs: black.Changed.YES,
1261 report = MagicMock()
1266 write_back=black.WriteBack.YES,
1270 fsts.assert_called_once()
1271 report.done.assert_called_with(path, black.Changed.YES)
1273 @pytest.mark.incompatible_with_mypyc
1274 def test_reformat_one_with_stdin_filename(self) -> None:
1276 "black.format_stdin_to_stdout",
1277 return_value=lambda *args, **kwargs: black.Changed.YES,
1279 report = MagicMock()
1281 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1286 write_back=black.WriteBack.YES,
1290 fsts.assert_called_once_with(
1291 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1293 # __BLACK_STDIN_FILENAME__ should have been stripped
1294 report.done.assert_called_with(expected, black.Changed.YES)
1296 @pytest.mark.incompatible_with_mypyc
1297 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1299 "black.format_stdin_to_stdout",
1300 return_value=lambda *args, **kwargs: black.Changed.YES,
1302 report = MagicMock()
1304 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1309 write_back=black.WriteBack.YES,
1313 fsts.assert_called_once_with(
1315 write_back=black.WriteBack.YES,
1316 mode=replace(DEFAULT_MODE, is_pyi=True),
1318 # __BLACK_STDIN_FILENAME__ should have been stripped
1319 report.done.assert_called_with(expected, black.Changed.YES)
1321 @pytest.mark.incompatible_with_mypyc
1322 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1324 "black.format_stdin_to_stdout",
1325 return_value=lambda *args, **kwargs: black.Changed.YES,
1327 report = MagicMock()
1329 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1334 write_back=black.WriteBack.YES,
1338 fsts.assert_called_once_with(
1340 write_back=black.WriteBack.YES,
1341 mode=replace(DEFAULT_MODE, is_ipynb=True),
1343 # __BLACK_STDIN_FILENAME__ should have been stripped
1344 report.done.assert_called_with(expected, black.Changed.YES)
1346 @pytest.mark.incompatible_with_mypyc
1347 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1349 "black.format_stdin_to_stdout",
1350 return_value=lambda *args, **kwargs: black.Changed.YES,
1352 report = MagicMock()
1353 # Even with an existing file, since we are forcing stdin, black
1354 # should output to stdout and not modify the file inplace
1355 p = THIS_DIR / "data" / "simple_cases" / "collections.py"
1356 # Make sure is_file actually returns True
1357 self.assertTrue(p.is_file())
1358 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1363 write_back=black.WriteBack.YES,
1367 fsts.assert_called_once()
1368 # __BLACK_STDIN_FILENAME__ should have been stripped
1369 report.done.assert_called_with(expected, black.Changed.YES)
1371 def test_reformat_one_with_stdin_empty(self) -> None:
1378 (" \t\r\n\t ", "\r\n"),
1382 output: io.StringIO, io_TextIOWrapper: Type[io.TextIOWrapper]
1383 ) -> Callable[[Any, Any], io.TextIOWrapper]:
1384 def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper:
1385 if args == (sys.stdout.buffer,):
1386 # It's `format_stdin_to_stdout()` calling `io.TextIOWrapper()`,
1387 # return our mock object.
1389 # It's something else (i.e. `decode_bytes()`) calling
1390 # `io.TextIOWrapper()`, pass through to the original implementation.
1391 # See discussion in https://github.com/psf/black/pull/2489
1392 return io_TextIOWrapper(*args, **kwargs)
1396 mode = black.Mode(preview=True)
1397 for content, expected in cases:
1398 output = io.StringIO()
1399 io_TextIOWrapper = io.TextIOWrapper
1401 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1403 black.format_stdin_to_stdout(
1406 write_back=black.WriteBack.YES,
1409 except io.UnsupportedOperation:
1410 pass # StringIO does not support detach
1411 assert output.getvalue() == expected
1413 # An empty string is the only test case for `preview=False`
1414 output = io.StringIO()
1415 io_TextIOWrapper = io.TextIOWrapper
1416 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1418 black.format_stdin_to_stdout(
1421 write_back=black.WriteBack.YES,
1424 except io.UnsupportedOperation:
1425 pass # StringIO does not support detach
1426 assert output.getvalue() == ""
1428 def test_invalid_cli_regex(self) -> None:
1429 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1430 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1432 def test_required_version_matches_version(self) -> None:
1434 ["--required-version", black.__version__, "-c", "0"],
1439 def test_required_version_matches_partial_version(self) -> None:
1441 ["--required-version", black.__version__.split(".")[0], "-c", "0"],
1446 def test_required_version_does_not_match_on_minor_version(self) -> None:
1448 ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
1453 def test_required_version_does_not_match_version(self) -> None:
1454 result = BlackRunner().invoke(
1456 ["--required-version", "20.99b", "-c", "0"],
1458 self.assertEqual(result.exit_code, 1)
1459 self.assertIn("required version", result.stderr)
1461 def test_preserves_line_endings(self) -> None:
1462 with TemporaryDirectory() as workspace:
1463 test_file = Path(workspace) / "test.py"
1464 for nl in ["\n", "\r\n"]:
1465 contents = nl.join(["def f( ):", " pass"])
1466 test_file.write_bytes(contents.encode())
1467 ff(test_file, write_back=black.WriteBack.YES)
1468 updated_contents: bytes = test_file.read_bytes()
1469 self.assertIn(nl.encode(), updated_contents)
1471 self.assertNotIn(b"\r\n", updated_contents)
1473 def test_preserves_line_endings_via_stdin(self) -> None:
1474 for nl in ["\n", "\r\n"]:
1475 contents = nl.join(["def f( ):", " pass"])
1476 runner = BlackRunner()
1477 result = runner.invoke(
1478 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1480 self.assertEqual(result.exit_code, 0)
1481 output = result.stdout_bytes
1482 self.assertIn(nl.encode("utf8"), output)
1484 self.assertNotIn(b"\r\n", output)
1486 def test_normalize_line_endings(self) -> None:
1487 with TemporaryDirectory() as workspace:
1488 test_file = Path(workspace) / "test.py"
1489 for data, expected in (
1490 (b"c\r\nc\n ", b"c\r\nc\r\n"),
1491 (b"l\nl\r\n ", b"l\nl\n"),
1493 test_file.write_bytes(data)
1494 ff(test_file, write_back=black.WriteBack.YES)
1495 self.assertEqual(test_file.read_bytes(), expected)
1497 def test_assert_equivalent_different_asts(self) -> None:
1498 with self.assertRaises(AssertionError):
1499 black.assert_equivalent("{}", "None")
1501 def test_shhh_click(self) -> None:
1503 from click import _unicodefun # type: ignore
1505 self.skipTest("Incompatible Click version")
1507 if not hasattr(_unicodefun, "_verify_python_env"):
1508 self.skipTest("Incompatible Click version")
1510 # First, let's see if Click is crashing with a preferred ASCII charset.
1511 with patch("locale.getpreferredencoding") as gpe:
1512 gpe.return_value = "ASCII"
1513 with self.assertRaises(RuntimeError):
1514 _unicodefun._verify_python_env()
1515 # Now, let's silence Click...
1517 # ...and confirm it's silent.
1518 with patch("locale.getpreferredencoding") as gpe:
1519 gpe.return_value = "ASCII"
1521 _unicodefun._verify_python_env()
1522 except RuntimeError as re:
1523 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1525 def test_root_logger_not_used_directly(self) -> None:
1526 def fail(*args: Any, **kwargs: Any) -> None:
1527 self.fail("Record created with root logger")
1529 with patch.multiple(
1538 ff(THIS_DIR / "util.py")
1540 def test_invalid_config_return_code(self) -> None:
1541 tmp_file = Path(black.dump_to_file())
1543 tmp_config = Path(black.dump_to_file())
1545 args = ["--config", str(tmp_config), str(tmp_file)]
1546 self.invokeBlack(args, exit_code=2, ignore_config=False)
1550 def test_parse_pyproject_toml(self) -> None:
1551 test_toml_file = THIS_DIR / "test.toml"
1552 config = black.parse_pyproject_toml(str(test_toml_file))
1553 self.assertEqual(config["verbose"], 1)
1554 self.assertEqual(config["check"], "no")
1555 self.assertEqual(config["diff"], "y")
1556 self.assertEqual(config["color"], True)
1557 self.assertEqual(config["line_length"], 79)
1558 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1559 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1560 self.assertEqual(config["exclude"], r"\.pyi?$")
1561 self.assertEqual(config["include"], r"\.py?$")
1563 def test_read_pyproject_toml(self) -> None:
1564 test_toml_file = THIS_DIR / "test.toml"
1565 fake_ctx = FakeContext()
1566 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1567 config = fake_ctx.default_map
1568 self.assertEqual(config["verbose"], "1")
1569 self.assertEqual(config["check"], "no")
1570 self.assertEqual(config["diff"], "y")
1571 self.assertEqual(config["color"], "True")
1572 self.assertEqual(config["line_length"], "79")
1573 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1574 self.assertEqual(config["exclude"], r"\.pyi?$")
1575 self.assertEqual(config["include"], r"\.py?$")
1577 @pytest.mark.incompatible_with_mypyc
1578 def test_find_project_root(self) -> None:
1579 with TemporaryDirectory() as workspace:
1580 root = Path(workspace)
1581 test_dir = root / "test"
1584 src_dir = root / "src"
1587 root_pyproject = root / "pyproject.toml"
1588 root_pyproject.touch()
1589 src_pyproject = src_dir / "pyproject.toml"
1590 src_pyproject.touch()
1591 src_python = src_dir / "foo.py"
1595 black.find_project_root((src_dir, test_dir)),
1596 (root.resolve(), "pyproject.toml"),
1599 black.find_project_root((src_dir,)),
1600 (src_dir.resolve(), "pyproject.toml"),
1603 black.find_project_root((src_python,)),
1604 (src_dir.resolve(), "pyproject.toml"),
1607 with change_directory(test_dir):
1609 black.find_project_root(("-",), stdin_filename="../src/a.py"),
1610 (src_dir.resolve(), "pyproject.toml"),
1614 "black.files.find_user_pyproject_toml",
1616 def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
1617 find_user_pyproject_toml.side_effect = RuntimeError()
1619 with redirect_stderr(io.StringIO()) as stderr:
1620 result = black.files.find_pyproject_toml(
1621 path_search_start=(str(Path.cwd().root),)
1624 assert result is None
1625 err = stderr.getvalue()
1626 assert "Ignoring user configuration" in err
1629 "black.files.find_user_pyproject_toml",
1630 black.files.find_user_pyproject_toml.__wrapped__,
1632 def test_find_user_pyproject_toml_linux(self) -> None:
1633 if system() == "Windows":
1636 # Test if XDG_CONFIG_HOME is checked
1637 with TemporaryDirectory() as workspace:
1638 tmp_user_config = Path(workspace) / "black"
1639 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1641 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1644 # Test fallback for XDG_CONFIG_HOME
1645 with patch.dict("os.environ"):
1646 os.environ.pop("XDG_CONFIG_HOME", None)
1647 fallback_user_config = Path("~/.config").expanduser() / "black"
1649 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1652 def test_find_user_pyproject_toml_windows(self) -> None:
1653 if system() != "Windows":
1656 user_config_path = Path.home() / ".black"
1658 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1661 def test_bpo_33660_workaround(self) -> None:
1662 if system() == "Windows":
1665 # https://bugs.python.org/issue33660
1667 with change_directory(root):
1668 path = Path("workspace") / "project"
1669 report = black.Report(verbose=True)
1670 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1671 self.assertEqual(normalized_path, "workspace/project")
1673 def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
1674 if system() != "Windows":
1677 with TemporaryDirectory() as workspace:
1678 root = Path(workspace)
1679 junction_dir = root / "junction"
1680 junction_target_outside_of_root = root / ".."
1681 os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
1683 report = black.Report(verbose=True)
1684 normalized_path = black.normalize_path_maybe_ignore(
1685 junction_dir, root, report
1687 # Manually delete for Python < 3.8
1688 os.system(f"rmdir {junction_dir}")
1690 self.assertEqual(normalized_path, None)
1692 def test_newline_comment_interaction(self) -> None:
1693 source = "class A:\\\r\n# type: ignore\n pass\n"
1694 output = black.format_str(source, mode=DEFAULT_MODE)
1695 black.assert_stable(source, output, mode=DEFAULT_MODE)
1697 def test_bpo_2142_workaround(self) -> None:
1698 # https://bugs.python.org/issue2142
1700 source, _ = read_data("miscellaneous", "missing_final_newline")
1701 # read_data adds a trailing newline
1702 source = source.rstrip()
1703 expected, _ = read_data("miscellaneous", "missing_final_newline.diff")
1704 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1705 diff_header = re.compile(
1706 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1707 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1710 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1711 self.assertEqual(result.exit_code, 0)
1714 actual = result.output
1715 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1716 self.assertEqual(actual, expected)
1719 def compare_results(
1720 result: click.testing.Result, expected_value: str, expected_exit_code: int
1722 """Helper method to test the value and exit code of a click Result."""
1724 result.output == expected_value
1725 ), "The output did not match the expected value."
1726 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1728 def test_code_option(self) -> None:
1729 """Test the code option with no changes."""
1730 code = 'print("Hello world")\n'
1731 args = ["--code", code]
1732 result = CliRunner().invoke(black.main, args)
1734 self.compare_results(result, code, 0)
1736 def test_code_option_changed(self) -> None:
1737 """Test the code option when changes are required."""
1738 code = "print('hello world')"
1739 formatted = black.format_str(code, mode=DEFAULT_MODE)
1741 args = ["--code", code]
1742 result = CliRunner().invoke(black.main, args)
1744 self.compare_results(result, formatted, 0)
1746 def test_code_option_check(self) -> None:
1747 """Test the code option when check is passed."""
1748 args = ["--check", "--code", 'print("Hello world")\n']
1749 result = CliRunner().invoke(black.main, args)
1750 self.compare_results(result, "", 0)
1752 def test_code_option_check_changed(self) -> None:
1753 """Test the code option when changes are required, and check is passed."""
1754 args = ["--check", "--code", "print('hello world')"]
1755 result = CliRunner().invoke(black.main, args)
1756 self.compare_results(result, "", 1)
1758 def test_code_option_diff(self) -> None:
1759 """Test the code option when diff is passed."""
1760 code = "print('hello world')"
1761 formatted = black.format_str(code, mode=DEFAULT_MODE)
1762 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1764 args = ["--diff", "--code", code]
1765 result = CliRunner().invoke(black.main, args)
1767 # Remove time from diff
1768 output = DIFF_TIME.sub("", result.output)
1770 assert output == result_diff, "The output did not match the expected value."
1771 assert result.exit_code == 0, "The exit code is incorrect."
1773 def test_code_option_color_diff(self) -> None:
1774 """Test the code option when color and diff are passed."""
1775 code = "print('hello world')"
1776 formatted = black.format_str(code, mode=DEFAULT_MODE)
1778 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1779 result_diff = color_diff(result_diff)
1781 args = ["--diff", "--color", "--code", code]
1782 result = CliRunner().invoke(black.main, args)
1784 # Remove time from diff
1785 output = DIFF_TIME.sub("", result.output)
1787 assert output == result_diff, "The output did not match the expected value."
1788 assert result.exit_code == 0, "The exit code is incorrect."
1790 @pytest.mark.incompatible_with_mypyc
1791 def test_code_option_safe(self) -> None:
1792 """Test that the code option throws an error when the sanity checks fail."""
1793 # Patch black.assert_equivalent to ensure the sanity checks fail
1794 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1795 code = 'print("Hello world")'
1796 error_msg = f"{code}\nerror: cannot format <string>: \n"
1798 args = ["--safe", "--code", code]
1799 result = CliRunner().invoke(black.main, args)
1801 self.compare_results(result, error_msg, 123)
1803 def test_code_option_fast(self) -> None:
1804 """Test that the code option ignores errors when the sanity checks fail."""
1805 # Patch black.assert_equivalent to ensure the sanity checks fail
1806 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1807 code = 'print("Hello world")'
1808 formatted = black.format_str(code, mode=DEFAULT_MODE)
1810 args = ["--fast", "--code", code]
1811 result = CliRunner().invoke(black.main, args)
1813 self.compare_results(result, formatted, 0)
1815 @pytest.mark.incompatible_with_mypyc
1816 def test_code_option_config(self) -> None:
1818 Test that the code option finds the pyproject.toml in the current directory.
1820 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1821 args = ["--code", "print"]
1822 # This is the only directory known to contain a pyproject.toml
1823 with change_directory(PROJECT_ROOT):
1824 CliRunner().invoke(black.main, args)
1825 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1828 len(parse.mock_calls) >= 1
1829 ), "Expected config parse to be called with the current directory."
1831 _, call_args, _ = parse.mock_calls[0]
1833 call_args[0].lower() == str(pyproject_path).lower()
1834 ), "Incorrect config loaded."
1836 @pytest.mark.incompatible_with_mypyc
1837 def test_code_option_parent_config(self) -> None:
1839 Test that the code option finds the pyproject.toml in the parent directory.
1841 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1842 with change_directory(THIS_DIR):
1843 args = ["--code", "print"]
1844 CliRunner().invoke(black.main, args)
1846 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1848 len(parse.mock_calls) >= 1
1849 ), "Expected config parse to be called with the current directory."
1851 _, call_args, _ = parse.mock_calls[0]
1853 call_args[0].lower() == str(pyproject_path).lower()
1854 ), "Incorrect config loaded."
1856 def test_for_handled_unexpected_eof_error(self) -> None:
1858 Test that an unexpected EOF SyntaxError is nicely presented.
1860 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1861 black.lib2to3_parse("print(", {})
1863 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1865 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1866 with pytest.raises(AssertionError) as err:
1867 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1870 # Unfortunately the SyntaxError message has changed in newer versions so we
1871 # can't match it directly.
1872 err.match("invalid character")
1873 err.match(r"\(<unknown>, line 1\)")
1877 def test_get_cache_dir(
1880 monkeypatch: pytest.MonkeyPatch,
1882 # Create multiple cache directories
1883 workspace1 = tmp_path / "ws1"
1885 workspace2 = tmp_path / "ws2"
1888 # Force user_cache_dir to use the temporary directory for easier assertions
1889 patch_user_cache_dir = patch(
1890 target="black.cache.user_cache_dir",
1892 return_value=str(workspace1),
1895 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1896 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1897 with patch_user_cache_dir:
1898 assert get_cache_dir() == workspace1
1900 # If it is set, use the path provided in the env var.
1901 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1902 assert get_cache_dir() == workspace2
1904 def test_cache_broken_file(self) -> None:
1906 with cache_dir() as workspace:
1907 cache_file = get_cache_file(mode)
1908 cache_file.write_text("this is not a pickle")
1909 assert black.read_cache(mode) == {}
1910 src = (workspace / "test.py").resolve()
1911 src.write_text("print('hello')")
1912 invokeBlack([str(src)])
1913 cache = black.read_cache(mode)
1914 assert str(src) in cache
1916 def test_cache_single_file_already_cached(self) -> None:
1918 with cache_dir() as workspace:
1919 src = (workspace / "test.py").resolve()
1920 src.write_text("print('hello')")
1921 black.write_cache({}, [src], mode)
1922 invokeBlack([str(src)])
1923 assert src.read_text() == "print('hello')"
1926 def test_cache_multiple_files(self) -> None:
1928 with cache_dir() as workspace, patch(
1929 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
1931 one = (workspace / "one.py").resolve()
1932 with one.open("w") as fobj:
1933 fobj.write("print('hello')")
1934 two = (workspace / "two.py").resolve()
1935 with two.open("w") as fobj:
1936 fobj.write("print('hello')")
1937 black.write_cache({}, [one], mode)
1938 invokeBlack([str(workspace)])
1939 with one.open("r") as fobj:
1940 assert fobj.read() == "print('hello')"
1941 with two.open("r") as fobj:
1942 assert fobj.read() == 'print("hello")\n'
1943 cache = black.read_cache(mode)
1944 assert str(one) in cache
1945 assert str(two) in cache
1947 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1948 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1950 with cache_dir() as workspace:
1951 src = (workspace / "test.py").resolve()
1952 with src.open("w") as fobj:
1953 fobj.write("print('hello')")
1954 with patch("black.read_cache") as read_cache, patch(
1957 cmd = [str(src), "--diff"]
1959 cmd.append("--color")
1961 cache_file = get_cache_file(mode)
1962 assert cache_file.exists() is False
1963 write_cache.assert_not_called()
1964 read_cache.assert_not_called()
1966 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1968 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1969 with cache_dir() as workspace:
1970 for tag in range(0, 4):
1971 src = (workspace / f"test{tag}.py").resolve()
1972 with src.open("w") as fobj:
1973 fobj.write("print('hello')")
1975 "black.concurrency.Manager", wraps=multiprocessing.Manager
1977 cmd = ["--diff", str(workspace)]
1979 cmd.append("--color")
1980 invokeBlack(cmd, exit_code=0)
1981 # this isn't quite doing what we want, but if it _isn't_
1982 # called then we cannot be using the lock it provides
1985 def test_no_cache_when_stdin(self) -> None:
1988 result = CliRunner().invoke(
1989 black.main, ["-"], input=BytesIO(b"print('hello')")
1991 assert not result.exit_code
1992 cache_file = get_cache_file(mode)
1993 assert not cache_file.exists()
1995 def test_read_cache_no_cachefile(self) -> None:
1998 assert black.read_cache(mode) == {}
2000 def test_write_cache_read_cache(self) -> None:
2002 with cache_dir() as workspace:
2003 src = (workspace / "test.py").resolve()
2005 black.write_cache({}, [src], mode)
2006 cache = black.read_cache(mode)
2007 assert str(src) in cache
2008 assert cache[str(src)] == black.get_cache_info(src)
2010 def test_filter_cached(self) -> None:
2011 with TemporaryDirectory() as workspace:
2012 path = Path(workspace)
2013 uncached = (path / "uncached").resolve()
2014 cached = (path / "cached").resolve()
2015 cached_but_changed = (path / "changed").resolve()
2018 cached_but_changed.touch()
2020 str(cached): black.get_cache_info(cached),
2021 str(cached_but_changed): (0.0, 0),
2023 todo, done = black.cache.filter_cached(
2024 cache, {uncached, cached, cached_but_changed}
2026 assert todo == {uncached, cached_but_changed}
2027 assert done == {cached}
2029 def test_write_cache_creates_directory_if_needed(self) -> None:
2031 with cache_dir(exists=False) as workspace:
2032 assert not workspace.exists()
2033 black.write_cache({}, [], mode)
2034 assert workspace.exists()
2037 def test_failed_formatting_does_not_get_cached(self) -> None:
2039 with cache_dir() as workspace, patch(
2040 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
2042 failing = (workspace / "failing.py").resolve()
2043 with failing.open("w") as fobj:
2044 fobj.write("not actually python")
2045 clean = (workspace / "clean.py").resolve()
2046 with clean.open("w") as fobj:
2047 fobj.write('print("hello")\n')
2048 invokeBlack([str(workspace)], exit_code=123)
2049 cache = black.read_cache(mode)
2050 assert str(failing) not in cache
2051 assert str(clean) in cache
2053 def test_write_cache_write_fail(self) -> None:
2055 with cache_dir(), patch.object(Path, "open") as mock:
2056 mock.side_effect = OSError
2057 black.write_cache({}, [], mode)
2059 def test_read_cache_line_lengths(self) -> None:
2061 short_mode = replace(DEFAULT_MODE, line_length=1)
2062 with cache_dir() as workspace:
2063 path = (workspace / "file.py").resolve()
2065 black.write_cache({}, [path], mode)
2066 one = black.read_cache(mode)
2067 assert str(path) in one
2068 two = black.read_cache(short_mode)
2069 assert str(path) not in two
2072 def assert_collected_sources(
2073 src: Sequence[Union[str, Path]],
2074 expected: Sequence[Union[str, Path]],
2076 ctx: Optional[FakeContext] = None,
2077 exclude: Optional[str] = None,
2078 include: Optional[str] = None,
2079 extend_exclude: Optional[str] = None,
2080 force_exclude: Optional[str] = None,
2081 stdin_filename: Optional[str] = None,
2083 gs_src = tuple(str(Path(s)) for s in src)
2084 gs_expected = [Path(s) for s in expected]
2085 gs_exclude = None if exclude is None else compile_pattern(exclude)
2086 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
2087 gs_extend_exclude = (
2088 None if extend_exclude is None else compile_pattern(extend_exclude)
2090 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
2091 collected = black.get_sources(
2092 ctx=ctx or FakeContext(),
2098 extend_exclude=gs_extend_exclude,
2099 force_exclude=gs_force_exclude,
2100 report=black.Report(),
2101 stdin_filename=stdin_filename,
2103 assert sorted(collected) == sorted(gs_expected)
2106 class TestFileCollection:
2107 def test_include_exclude(self) -> None:
2108 path = THIS_DIR / "data" / "include_exclude_tests"
2111 Path(path / "b/dont_exclude/a.py"),
2112 Path(path / "b/dont_exclude/a.pyi"),
2114 assert_collected_sources(
2118 exclude=r"/exclude/|/\.definitely_exclude/",
2121 def test_gitignore_used_as_default(self) -> None:
2122 base = Path(DATA_DIR / "include_exclude_tests")
2124 base / "b/.definitely_exclude/a.py",
2125 base / "b/.definitely_exclude/a.pyi",
2129 ctx.obj["root"] = base
2130 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
2132 def test_gitignore_used_on_multiple_sources(self) -> None:
2133 root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
2135 root / "dir1" / "b.py",
2136 root / "dir2" / "b.py",
2139 ctx.obj["root"] = root
2140 src = [root / "dir1", root / "dir2"]
2141 assert_collected_sources(src, expected, ctx=ctx)
2143 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2144 def test_exclude_for_issue_1572(self) -> None:
2145 # Exclude shouldn't touch files that were explicitly given to Black through the
2146 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
2147 # https://github.com/psf/black/issues/1572
2148 path = DATA_DIR / "include_exclude_tests"
2149 src = [path / "b/exclude/a.py"]
2150 expected = [path / "b/exclude/a.py"]
2151 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2153 def test_gitignore_exclude(self) -> None:
2154 path = THIS_DIR / "data" / "include_exclude_tests"
2155 include = re.compile(r"\.pyi?$")
2156 exclude = re.compile(r"")
2157 report = black.Report()
2158 gitignore = PathSpec.from_lines(
2159 "gitwildmatch", ["exclude/", ".definitely_exclude"]
2161 sources: List[Path] = []
2163 Path(path / "b/dont_exclude/a.py"),
2164 Path(path / "b/dont_exclude/a.pyi"),
2166 this_abs = THIS_DIR.resolve()
2168 black.gen_python_files(
2181 assert sorted(expected) == sorted(sources)
2183 def test_nested_gitignore(self) -> None:
2184 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
2185 include = re.compile(r"\.pyi?$")
2186 exclude = re.compile(r"")
2187 root_gitignore = black.files.get_gitignore(path)
2188 report = black.Report()
2189 expected: List[Path] = [
2190 Path(path / "x.py"),
2191 Path(path / "root/b.py"),
2192 Path(path / "root/c.py"),
2193 Path(path / "root/child/c.py"),
2195 this_abs = THIS_DIR.resolve()
2197 black.gen_python_files(
2205 {path: root_gitignore},
2210 assert sorted(expected) == sorted(sources)
2212 def test_nested_gitignore_directly_in_source_directory(self) -> None:
2213 # https://github.com/psf/black/issues/2598
2214 path = Path(DATA_DIR / "nested_gitignore_tests")
2215 src = Path(path / "root" / "child")
2216 expected = [src / "a.py", src / "c.py"]
2217 assert_collected_sources([src], expected)
2219 def test_invalid_gitignore(self) -> None:
2220 path = THIS_DIR / "data" / "invalid_gitignore_tests"
2221 empty_config = path / "pyproject.toml"
2222 result = BlackRunner().invoke(
2223 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2225 assert result.exit_code == 1
2226 assert result.stderr_bytes is not None
2228 gitignore = path / ".gitignore"
2229 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2231 def test_invalid_nested_gitignore(self) -> None:
2232 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
2233 empty_config = path / "pyproject.toml"
2234 result = BlackRunner().invoke(
2235 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2237 assert result.exit_code == 1
2238 assert result.stderr_bytes is not None
2240 gitignore = path / "a" / ".gitignore"
2241 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2243 def test_gitignore_that_ignores_subfolders(self) -> None:
2244 # If gitignore with */* is in root
2245 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir")
2246 expected = [root / "b.py"]
2248 ctx.obj["root"] = root
2249 assert_collected_sources([root], expected, ctx=ctx)
2251 # If .gitignore with */* is nested
2252 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2255 root / "subdir" / "b.py",
2258 ctx.obj["root"] = root
2259 assert_collected_sources([root], expected, ctx=ctx)
2261 # If command is executed from outer dir
2262 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2263 target = root / "subdir"
2264 expected = [target / "b.py"]
2266 ctx.obj["root"] = root
2267 assert_collected_sources([target], expected, ctx=ctx)
2269 def test_empty_include(self) -> None:
2270 path = DATA_DIR / "include_exclude_tests"
2273 Path(path / "b/exclude/a.pie"),
2274 Path(path / "b/exclude/a.py"),
2275 Path(path / "b/exclude/a.pyi"),
2276 Path(path / "b/dont_exclude/a.pie"),
2277 Path(path / "b/dont_exclude/a.py"),
2278 Path(path / "b/dont_exclude/a.pyi"),
2279 Path(path / "b/.definitely_exclude/a.pie"),
2280 Path(path / "b/.definitely_exclude/a.py"),
2281 Path(path / "b/.definitely_exclude/a.pyi"),
2282 Path(path / ".gitignore"),
2283 Path(path / "pyproject.toml"),
2285 # Setting exclude explicitly to an empty string to block .gitignore usage.
2286 assert_collected_sources(src, expected, include="", exclude="")
2288 def test_extend_exclude(self) -> None:
2289 path = DATA_DIR / "include_exclude_tests"
2292 Path(path / "b/exclude/a.py"),
2293 Path(path / "b/dont_exclude/a.py"),
2295 assert_collected_sources(
2296 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
2299 @pytest.mark.incompatible_with_mypyc
2300 def test_symlink_out_of_root_directory(self) -> None:
2302 root = THIS_DIR.resolve()
2304 include = re.compile(black.DEFAULT_INCLUDES)
2305 exclude = re.compile(black.DEFAULT_EXCLUDES)
2306 report = black.Report()
2307 gitignore = PathSpec.from_lines("gitwildmatch", [])
2308 # `child` should behave like a symlink which resolved path is clearly
2309 # outside of the `root` directory.
2310 path.iterdir.return_value = [child]
2311 child.resolve.return_value = Path("/a/b/c")
2312 child.as_posix.return_value = "/a/b/c"
2315 black.gen_python_files(
2328 except ValueError as ve:
2329 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
2330 path.iterdir.assert_called_once()
2331 child.resolve.assert_called_once()
2333 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2334 def test_get_sources_with_stdin(self) -> None:
2337 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2339 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2340 def test_get_sources_with_stdin_filename(self) -> None:
2342 stdin_filename = str(THIS_DIR / "data/collections.py")
2343 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2344 assert_collected_sources(
2347 exclude=r"/exclude/a\.py",
2348 stdin_filename=stdin_filename,
2351 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2352 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2353 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2354 # file being passed directly. This is the same as
2355 # test_exclude_for_issue_1572
2356 path = DATA_DIR / "include_exclude_tests"
2358 stdin_filename = str(path / "b/exclude/a.py")
2359 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2360 assert_collected_sources(
2363 exclude=r"/exclude/|a\.py",
2364 stdin_filename=stdin_filename,
2367 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2368 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2369 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2370 # file being passed directly. This is the same as
2371 # test_exclude_for_issue_1572
2373 path = THIS_DIR / "data" / "include_exclude_tests"
2374 stdin_filename = str(path / "b/exclude/a.py")
2375 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2376 assert_collected_sources(
2379 extend_exclude=r"/exclude/|a\.py",
2380 stdin_filename=stdin_filename,
2383 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2384 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2385 # Force exclude should exclude the file when passing it through
2387 path = THIS_DIR / "data" / "include_exclude_tests"
2388 stdin_filename = str(path / "b/exclude/a.py")
2389 assert_collected_sources(
2392 force_exclude=r"/exclude/|a\.py",
2393 stdin_filename=stdin_filename,
2398 with open(black.__file__, "r", encoding="utf-8") as _bf:
2399 black_source_lines = _bf.readlines()
2400 except UnicodeDecodeError:
2401 if not black.COMPILED:
2406 frame: types.FrameType, event: str, arg: Any
2407 ) -> Callable[[types.FrameType, str, Any], Any]:
2408 """Show function calls `from black/__init__.py` as they happen.
2410 Register this with `sys.settrace()` in a test you're debugging.
2415 stack = len(inspect.stack()) - 19
2417 filename = frame.f_code.co_filename
2418 lineno = frame.f_lineno
2419 func_sig_lineno = lineno - 1
2420 funcname = black_source_lines[func_sig_lineno].strip()
2421 while funcname.startswith("@"):
2422 func_sig_lineno += 1
2423 funcname = black_source_lines[func_sig_lineno].strip()
2424 if "black/__init__.py" in filename:
2425 print(f"{' ' * stack}{lineno}:{funcname}")