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_parse_pyproject_toml_project_metadata(self) -> None:
1564 for test_toml, expected in [
1565 ("only_black_pyproject.toml", ["py310"]),
1566 ("only_metadata_pyproject.toml", ["py37", "py38", "py39", "py310"]),
1567 ("neither_pyproject.toml", None),
1568 ("both_pyproject.toml", ["py310"]),
1570 test_toml_file = THIS_DIR / "data" / "project_metadata" / test_toml
1571 config = black.parse_pyproject_toml(str(test_toml_file))
1572 self.assertEqual(config.get("target_version"), expected)
1574 def test_infer_target_version(self) -> None:
1575 for version, expected in [
1576 ("3.6", [TargetVersion.PY36]),
1577 ("3.11.0rc1", [TargetVersion.PY311]),
1578 (">=3.10", [TargetVersion.PY310, TargetVersion.PY311]),
1579 (">=3.10.6", [TargetVersion.PY310, TargetVersion.PY311]),
1580 ("<3.6", [TargetVersion.PY33, TargetVersion.PY34, TargetVersion.PY35]),
1581 (">3.7,<3.10", [TargetVersion.PY38, TargetVersion.PY39]),
1582 (">3.7,!=3.8,!=3.9", [TargetVersion.PY310, TargetVersion.PY311]),
1584 "> 3.9.4, != 3.10.3",
1585 [TargetVersion.PY39, TargetVersion.PY310, TargetVersion.PY311],
1595 TargetVersion.PY310,
1596 TargetVersion.PY311,
1609 TargetVersion.PY310,
1610 TargetVersion.PY311,
1613 ("==3.8.*", [TargetVersion.PY38]),
1617 ("==invalid", None),
1618 (">3.9,!=invalid", None),
1623 (">3.10,<3.11", None),
1625 test_toml = {"project": {"requires-python": version}}
1626 result = black.files.infer_target_version(test_toml)
1627 self.assertEqual(result, expected)
1629 def test_read_pyproject_toml(self) -> None:
1630 test_toml_file = THIS_DIR / "test.toml"
1631 fake_ctx = FakeContext()
1632 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1633 config = fake_ctx.default_map
1634 self.assertEqual(config["verbose"], "1")
1635 self.assertEqual(config["check"], "no")
1636 self.assertEqual(config["diff"], "y")
1637 self.assertEqual(config["color"], "True")
1638 self.assertEqual(config["line_length"], "79")
1639 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1640 self.assertEqual(config["exclude"], r"\.pyi?$")
1641 self.assertEqual(config["include"], r"\.py?$")
1643 @pytest.mark.incompatible_with_mypyc
1644 def test_find_project_root(self) -> None:
1645 with TemporaryDirectory() as workspace:
1646 root = Path(workspace)
1647 test_dir = root / "test"
1650 src_dir = root / "src"
1653 root_pyproject = root / "pyproject.toml"
1654 root_pyproject.touch()
1655 src_pyproject = src_dir / "pyproject.toml"
1656 src_pyproject.touch()
1657 src_python = src_dir / "foo.py"
1661 black.find_project_root((src_dir, test_dir)),
1662 (root.resolve(), "pyproject.toml"),
1665 black.find_project_root((src_dir,)),
1666 (src_dir.resolve(), "pyproject.toml"),
1669 black.find_project_root((src_python,)),
1670 (src_dir.resolve(), "pyproject.toml"),
1673 with change_directory(test_dir):
1675 black.find_project_root(("-",), stdin_filename="../src/a.py"),
1676 (src_dir.resolve(), "pyproject.toml"),
1680 "black.files.find_user_pyproject_toml",
1682 def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
1683 find_user_pyproject_toml.side_effect = RuntimeError()
1685 with redirect_stderr(io.StringIO()) as stderr:
1686 result = black.files.find_pyproject_toml(
1687 path_search_start=(str(Path.cwd().root),)
1690 assert result is None
1691 err = stderr.getvalue()
1692 assert "Ignoring user configuration" in err
1695 "black.files.find_user_pyproject_toml",
1696 black.files.find_user_pyproject_toml.__wrapped__,
1698 def test_find_user_pyproject_toml_linux(self) -> None:
1699 if system() == "Windows":
1702 # Test if XDG_CONFIG_HOME is checked
1703 with TemporaryDirectory() as workspace:
1704 tmp_user_config = Path(workspace) / "black"
1705 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1707 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1710 # Test fallback for XDG_CONFIG_HOME
1711 with patch.dict("os.environ"):
1712 os.environ.pop("XDG_CONFIG_HOME", None)
1713 fallback_user_config = Path("~/.config").expanduser() / "black"
1715 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1718 def test_find_user_pyproject_toml_windows(self) -> None:
1719 if system() != "Windows":
1722 user_config_path = Path.home() / ".black"
1724 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1727 def test_bpo_33660_workaround(self) -> None:
1728 if system() == "Windows":
1731 # https://bugs.python.org/issue33660
1733 with change_directory(root):
1734 path = Path("workspace") / "project"
1735 report = black.Report(verbose=True)
1736 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1737 self.assertEqual(normalized_path, "workspace/project")
1739 def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
1740 if system() != "Windows":
1743 with TemporaryDirectory() as workspace:
1744 root = Path(workspace)
1745 junction_dir = root / "junction"
1746 junction_target_outside_of_root = root / ".."
1747 os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
1749 report = black.Report(verbose=True)
1750 normalized_path = black.normalize_path_maybe_ignore(
1751 junction_dir, root, report
1753 # Manually delete for Python < 3.8
1754 os.system(f"rmdir {junction_dir}")
1756 self.assertEqual(normalized_path, None)
1758 def test_newline_comment_interaction(self) -> None:
1759 source = "class A:\\\r\n# type: ignore\n pass\n"
1760 output = black.format_str(source, mode=DEFAULT_MODE)
1761 black.assert_stable(source, output, mode=DEFAULT_MODE)
1763 def test_bpo_2142_workaround(self) -> None:
1764 # https://bugs.python.org/issue2142
1766 source, _ = read_data("miscellaneous", "missing_final_newline")
1767 # read_data adds a trailing newline
1768 source = source.rstrip()
1769 expected, _ = read_data("miscellaneous", "missing_final_newline.diff")
1770 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1771 diff_header = re.compile(
1772 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1773 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1776 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1777 self.assertEqual(result.exit_code, 0)
1780 actual = result.output
1781 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1782 self.assertEqual(actual, expected)
1785 def compare_results(
1786 result: click.testing.Result, expected_value: str, expected_exit_code: int
1788 """Helper method to test the value and exit code of a click Result."""
1790 result.output == expected_value
1791 ), "The output did not match the expected value."
1792 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1794 def test_code_option(self) -> None:
1795 """Test the code option with no changes."""
1796 code = 'print("Hello world")\n'
1797 args = ["--code", code]
1798 result = CliRunner().invoke(black.main, args)
1800 self.compare_results(result, code, 0)
1802 def test_code_option_changed(self) -> None:
1803 """Test the code option when changes are required."""
1804 code = "print('hello world')"
1805 formatted = black.format_str(code, mode=DEFAULT_MODE)
1807 args = ["--code", code]
1808 result = CliRunner().invoke(black.main, args)
1810 self.compare_results(result, formatted, 0)
1812 def test_code_option_check(self) -> None:
1813 """Test the code option when check is passed."""
1814 args = ["--check", "--code", 'print("Hello world")\n']
1815 result = CliRunner().invoke(black.main, args)
1816 self.compare_results(result, "", 0)
1818 def test_code_option_check_changed(self) -> None:
1819 """Test the code option when changes are required, and check is passed."""
1820 args = ["--check", "--code", "print('hello world')"]
1821 result = CliRunner().invoke(black.main, args)
1822 self.compare_results(result, "", 1)
1824 def test_code_option_diff(self) -> None:
1825 """Test the code option when diff is passed."""
1826 code = "print('hello world')"
1827 formatted = black.format_str(code, mode=DEFAULT_MODE)
1828 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1830 args = ["--diff", "--code", code]
1831 result = CliRunner().invoke(black.main, args)
1833 # Remove time from diff
1834 output = DIFF_TIME.sub("", result.output)
1836 assert output == result_diff, "The output did not match the expected value."
1837 assert result.exit_code == 0, "The exit code is incorrect."
1839 def test_code_option_color_diff(self) -> None:
1840 """Test the code option when color and diff are passed."""
1841 code = "print('hello world')"
1842 formatted = black.format_str(code, mode=DEFAULT_MODE)
1844 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1845 result_diff = color_diff(result_diff)
1847 args = ["--diff", "--color", "--code", code]
1848 result = CliRunner().invoke(black.main, args)
1850 # Remove time from diff
1851 output = DIFF_TIME.sub("", result.output)
1853 assert output == result_diff, "The output did not match the expected value."
1854 assert result.exit_code == 0, "The exit code is incorrect."
1856 @pytest.mark.incompatible_with_mypyc
1857 def test_code_option_safe(self) -> None:
1858 """Test that the code option throws an error when the sanity checks fail."""
1859 # Patch black.assert_equivalent to ensure the sanity checks fail
1860 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1861 code = 'print("Hello world")'
1862 error_msg = f"{code}\nerror: cannot format <string>: \n"
1864 args = ["--safe", "--code", code]
1865 result = CliRunner().invoke(black.main, args)
1867 self.compare_results(result, error_msg, 123)
1869 def test_code_option_fast(self) -> None:
1870 """Test that the code option ignores errors when the sanity checks fail."""
1871 # Patch black.assert_equivalent to ensure the sanity checks fail
1872 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1873 code = 'print("Hello world")'
1874 formatted = black.format_str(code, mode=DEFAULT_MODE)
1876 args = ["--fast", "--code", code]
1877 result = CliRunner().invoke(black.main, args)
1879 self.compare_results(result, formatted, 0)
1881 @pytest.mark.incompatible_with_mypyc
1882 def test_code_option_config(self) -> None:
1884 Test that the code option finds the pyproject.toml in the current directory.
1886 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1887 args = ["--code", "print"]
1888 # This is the only directory known to contain a pyproject.toml
1889 with change_directory(PROJECT_ROOT):
1890 CliRunner().invoke(black.main, args)
1891 pyproject_path = Path(Path.cwd(), "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 @pytest.mark.incompatible_with_mypyc
1903 def test_code_option_parent_config(self) -> None:
1905 Test that the code option finds the pyproject.toml in the parent directory.
1907 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1908 with change_directory(THIS_DIR):
1909 args = ["--code", "print"]
1910 CliRunner().invoke(black.main, args)
1912 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1914 len(parse.mock_calls) >= 1
1915 ), "Expected config parse to be called with the current directory."
1917 _, call_args, _ = parse.mock_calls[0]
1919 call_args[0].lower() == str(pyproject_path).lower()
1920 ), "Incorrect config loaded."
1922 def test_for_handled_unexpected_eof_error(self) -> None:
1924 Test that an unexpected EOF SyntaxError is nicely presented.
1926 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1927 black.lib2to3_parse("print(", {})
1929 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1931 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1932 with pytest.raises(AssertionError) as err:
1933 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1936 # Unfortunately the SyntaxError message has changed in newer versions so we
1937 # can't match it directly.
1938 err.match("invalid character")
1939 err.match(r"\(<unknown>, line 1\)")
1943 def test_get_cache_dir(
1946 monkeypatch: pytest.MonkeyPatch,
1948 # Create multiple cache directories
1949 workspace1 = tmp_path / "ws1"
1951 workspace2 = tmp_path / "ws2"
1954 # Force user_cache_dir to use the temporary directory for easier assertions
1955 patch_user_cache_dir = patch(
1956 target="black.cache.user_cache_dir",
1958 return_value=str(workspace1),
1961 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1962 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1963 with patch_user_cache_dir:
1964 assert get_cache_dir() == workspace1
1966 # If it is set, use the path provided in the env var.
1967 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1968 assert get_cache_dir() == workspace2
1970 def test_cache_broken_file(self) -> None:
1972 with cache_dir() as workspace:
1973 cache_file = get_cache_file(mode)
1974 cache_file.write_text("this is not a pickle")
1975 assert black.read_cache(mode) == {}
1976 src = (workspace / "test.py").resolve()
1977 src.write_text("print('hello')")
1978 invokeBlack([str(src)])
1979 cache = black.read_cache(mode)
1980 assert str(src) in cache
1982 def test_cache_single_file_already_cached(self) -> None:
1984 with cache_dir() as workspace:
1985 src = (workspace / "test.py").resolve()
1986 src.write_text("print('hello')")
1987 black.write_cache({}, [src], mode)
1988 invokeBlack([str(src)])
1989 assert src.read_text() == "print('hello')"
1992 def test_cache_multiple_files(self) -> None:
1994 with cache_dir() as workspace, patch(
1995 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
1997 one = (workspace / "one.py").resolve()
1998 with one.open("w") as fobj:
1999 fobj.write("print('hello')")
2000 two = (workspace / "two.py").resolve()
2001 with two.open("w") as fobj:
2002 fobj.write("print('hello')")
2003 black.write_cache({}, [one], mode)
2004 invokeBlack([str(workspace)])
2005 with one.open("r") as fobj:
2006 assert fobj.read() == "print('hello')"
2007 with two.open("r") as fobj:
2008 assert fobj.read() == 'print("hello")\n'
2009 cache = black.read_cache(mode)
2010 assert str(one) in cache
2011 assert str(two) in cache
2013 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
2014 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
2016 with cache_dir() as workspace:
2017 src = (workspace / "test.py").resolve()
2018 with src.open("w") as fobj:
2019 fobj.write("print('hello')")
2020 with patch("black.read_cache") as read_cache, patch(
2023 cmd = [str(src), "--diff"]
2025 cmd.append("--color")
2027 cache_file = get_cache_file(mode)
2028 assert cache_file.exists() is False
2029 write_cache.assert_not_called()
2030 read_cache.assert_not_called()
2032 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
2034 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
2035 with cache_dir() as workspace:
2036 for tag in range(0, 4):
2037 src = (workspace / f"test{tag}.py").resolve()
2038 with src.open("w") as fobj:
2039 fobj.write("print('hello')")
2041 "black.concurrency.Manager", wraps=multiprocessing.Manager
2043 cmd = ["--diff", str(workspace)]
2045 cmd.append("--color")
2046 invokeBlack(cmd, exit_code=0)
2047 # this isn't quite doing what we want, but if it _isn't_
2048 # called then we cannot be using the lock it provides
2051 def test_no_cache_when_stdin(self) -> None:
2054 result = CliRunner().invoke(
2055 black.main, ["-"], input=BytesIO(b"print('hello')")
2057 assert not result.exit_code
2058 cache_file = get_cache_file(mode)
2059 assert not cache_file.exists()
2061 def test_read_cache_no_cachefile(self) -> None:
2064 assert black.read_cache(mode) == {}
2066 def test_write_cache_read_cache(self) -> None:
2068 with cache_dir() as workspace:
2069 src = (workspace / "test.py").resolve()
2071 black.write_cache({}, [src], mode)
2072 cache = black.read_cache(mode)
2073 assert str(src) in cache
2074 assert cache[str(src)] == black.get_cache_info(src)
2076 def test_filter_cached(self) -> None:
2077 with TemporaryDirectory() as workspace:
2078 path = Path(workspace)
2079 uncached = (path / "uncached").resolve()
2080 cached = (path / "cached").resolve()
2081 cached_but_changed = (path / "changed").resolve()
2084 cached_but_changed.touch()
2086 str(cached): black.get_cache_info(cached),
2087 str(cached_but_changed): (0.0, 0),
2089 todo, done = black.cache.filter_cached(
2090 cache, {uncached, cached, cached_but_changed}
2092 assert todo == {uncached, cached_but_changed}
2093 assert done == {cached}
2095 def test_write_cache_creates_directory_if_needed(self) -> None:
2097 with cache_dir(exists=False) as workspace:
2098 assert not workspace.exists()
2099 black.write_cache({}, [], mode)
2100 assert workspace.exists()
2103 def test_failed_formatting_does_not_get_cached(self) -> None:
2105 with cache_dir() as workspace, patch(
2106 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
2108 failing = (workspace / "failing.py").resolve()
2109 with failing.open("w") as fobj:
2110 fobj.write("not actually python")
2111 clean = (workspace / "clean.py").resolve()
2112 with clean.open("w") as fobj:
2113 fobj.write('print("hello")\n')
2114 invokeBlack([str(workspace)], exit_code=123)
2115 cache = black.read_cache(mode)
2116 assert str(failing) not in cache
2117 assert str(clean) in cache
2119 def test_write_cache_write_fail(self) -> None:
2121 with cache_dir(), patch.object(Path, "open") as mock:
2122 mock.side_effect = OSError
2123 black.write_cache({}, [], mode)
2125 def test_read_cache_line_lengths(self) -> None:
2127 short_mode = replace(DEFAULT_MODE, line_length=1)
2128 with cache_dir() as workspace:
2129 path = (workspace / "file.py").resolve()
2131 black.write_cache({}, [path], mode)
2132 one = black.read_cache(mode)
2133 assert str(path) in one
2134 two = black.read_cache(short_mode)
2135 assert str(path) not in two
2138 def assert_collected_sources(
2139 src: Sequence[Union[str, Path]],
2140 expected: Sequence[Union[str, Path]],
2142 ctx: Optional[FakeContext] = None,
2143 exclude: Optional[str] = None,
2144 include: Optional[str] = None,
2145 extend_exclude: Optional[str] = None,
2146 force_exclude: Optional[str] = None,
2147 stdin_filename: Optional[str] = None,
2149 gs_src = tuple(str(Path(s)) for s in src)
2150 gs_expected = [Path(s) for s in expected]
2151 gs_exclude = None if exclude is None else compile_pattern(exclude)
2152 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
2153 gs_extend_exclude = (
2154 None if extend_exclude is None else compile_pattern(extend_exclude)
2156 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
2157 collected = black.get_sources(
2158 ctx=ctx or FakeContext(),
2164 extend_exclude=gs_extend_exclude,
2165 force_exclude=gs_force_exclude,
2166 report=black.Report(),
2167 stdin_filename=stdin_filename,
2169 assert sorted(collected) == sorted(gs_expected)
2172 class TestFileCollection:
2173 def test_include_exclude(self) -> None:
2174 path = THIS_DIR / "data" / "include_exclude_tests"
2177 Path(path / "b/dont_exclude/a.py"),
2178 Path(path / "b/dont_exclude/a.pyi"),
2180 assert_collected_sources(
2184 exclude=r"/exclude/|/\.definitely_exclude/",
2187 def test_gitignore_used_as_default(self) -> None:
2188 base = Path(DATA_DIR / "include_exclude_tests")
2190 base / "b/.definitely_exclude/a.py",
2191 base / "b/.definitely_exclude/a.pyi",
2195 ctx.obj["root"] = base
2196 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
2198 def test_gitignore_used_on_multiple_sources(self) -> None:
2199 root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
2201 root / "dir1" / "b.py",
2202 root / "dir2" / "b.py",
2205 ctx.obj["root"] = root
2206 src = [root / "dir1", root / "dir2"]
2207 assert_collected_sources(src, expected, ctx=ctx)
2209 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2210 def test_exclude_for_issue_1572(self) -> None:
2211 # Exclude shouldn't touch files that were explicitly given to Black through the
2212 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
2213 # https://github.com/psf/black/issues/1572
2214 path = DATA_DIR / "include_exclude_tests"
2215 src = [path / "b/exclude/a.py"]
2216 expected = [path / "b/exclude/a.py"]
2217 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2219 def test_gitignore_exclude(self) -> None:
2220 path = THIS_DIR / "data" / "include_exclude_tests"
2221 include = re.compile(r"\.pyi?$")
2222 exclude = re.compile(r"")
2223 report = black.Report()
2224 gitignore = PathSpec.from_lines(
2225 "gitwildmatch", ["exclude/", ".definitely_exclude"]
2227 sources: List[Path] = []
2229 Path(path / "b/dont_exclude/a.py"),
2230 Path(path / "b/dont_exclude/a.pyi"),
2232 this_abs = THIS_DIR.resolve()
2234 black.gen_python_files(
2247 assert sorted(expected) == sorted(sources)
2249 def test_nested_gitignore(self) -> None:
2250 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
2251 include = re.compile(r"\.pyi?$")
2252 exclude = re.compile(r"")
2253 root_gitignore = black.files.get_gitignore(path)
2254 report = black.Report()
2255 expected: List[Path] = [
2256 Path(path / "x.py"),
2257 Path(path / "root/b.py"),
2258 Path(path / "root/c.py"),
2259 Path(path / "root/child/c.py"),
2261 this_abs = THIS_DIR.resolve()
2263 black.gen_python_files(
2271 {path: root_gitignore},
2276 assert sorted(expected) == sorted(sources)
2278 def test_nested_gitignore_directly_in_source_directory(self) -> None:
2279 # https://github.com/psf/black/issues/2598
2280 path = Path(DATA_DIR / "nested_gitignore_tests")
2281 src = Path(path / "root" / "child")
2282 expected = [src / "a.py", src / "c.py"]
2283 assert_collected_sources([src], expected)
2285 def test_invalid_gitignore(self) -> None:
2286 path = THIS_DIR / "data" / "invalid_gitignore_tests"
2287 empty_config = path / "pyproject.toml"
2288 result = BlackRunner().invoke(
2289 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2291 assert result.exit_code == 1
2292 assert result.stderr_bytes is not None
2294 gitignore = path / ".gitignore"
2295 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2297 def test_invalid_nested_gitignore(self) -> None:
2298 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
2299 empty_config = path / "pyproject.toml"
2300 result = BlackRunner().invoke(
2301 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2303 assert result.exit_code == 1
2304 assert result.stderr_bytes is not None
2306 gitignore = path / "a" / ".gitignore"
2307 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2309 def test_gitignore_that_ignores_subfolders(self) -> None:
2310 # If gitignore with */* is in root
2311 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir")
2312 expected = [root / "b.py"]
2314 ctx.obj["root"] = root
2315 assert_collected_sources([root], expected, ctx=ctx)
2317 # If .gitignore with */* is nested
2318 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2321 root / "subdir" / "b.py",
2324 ctx.obj["root"] = root
2325 assert_collected_sources([root], expected, ctx=ctx)
2327 # If command is executed from outer dir
2328 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2329 target = root / "subdir"
2330 expected = [target / "b.py"]
2332 ctx.obj["root"] = root
2333 assert_collected_sources([target], expected, ctx=ctx)
2335 def test_empty_include(self) -> None:
2336 path = DATA_DIR / "include_exclude_tests"
2339 Path(path / "b/exclude/a.pie"),
2340 Path(path / "b/exclude/a.py"),
2341 Path(path / "b/exclude/a.pyi"),
2342 Path(path / "b/dont_exclude/a.pie"),
2343 Path(path / "b/dont_exclude/a.py"),
2344 Path(path / "b/dont_exclude/a.pyi"),
2345 Path(path / "b/.definitely_exclude/a.pie"),
2346 Path(path / "b/.definitely_exclude/a.py"),
2347 Path(path / "b/.definitely_exclude/a.pyi"),
2348 Path(path / ".gitignore"),
2349 Path(path / "pyproject.toml"),
2351 # Setting exclude explicitly to an empty string to block .gitignore usage.
2352 assert_collected_sources(src, expected, include="", exclude="")
2354 def test_extend_exclude(self) -> None:
2355 path = DATA_DIR / "include_exclude_tests"
2358 Path(path / "b/exclude/a.py"),
2359 Path(path / "b/dont_exclude/a.py"),
2361 assert_collected_sources(
2362 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
2365 @pytest.mark.incompatible_with_mypyc
2366 def test_symlink_out_of_root_directory(self) -> None:
2368 root = THIS_DIR.resolve()
2370 include = re.compile(black.DEFAULT_INCLUDES)
2371 exclude = re.compile(black.DEFAULT_EXCLUDES)
2372 report = black.Report()
2373 gitignore = PathSpec.from_lines("gitwildmatch", [])
2374 # `child` should behave like a symlink which resolved path is clearly
2375 # outside of the `root` directory.
2376 path.iterdir.return_value = [child]
2377 child.resolve.return_value = Path("/a/b/c")
2378 child.as_posix.return_value = "/a/b/c"
2381 black.gen_python_files(
2394 except ValueError as ve:
2395 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
2396 path.iterdir.assert_called_once()
2397 child.resolve.assert_called_once()
2399 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2400 def test_get_sources_with_stdin(self) -> None:
2403 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2405 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2406 def test_get_sources_with_stdin_filename(self) -> None:
2408 stdin_filename = str(THIS_DIR / "data/collections.py")
2409 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2410 assert_collected_sources(
2413 exclude=r"/exclude/a\.py",
2414 stdin_filename=stdin_filename,
2417 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2418 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2419 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2420 # file being passed directly. This is the same as
2421 # test_exclude_for_issue_1572
2422 path = DATA_DIR / "include_exclude_tests"
2424 stdin_filename = str(path / "b/exclude/a.py")
2425 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2426 assert_collected_sources(
2429 exclude=r"/exclude/|a\.py",
2430 stdin_filename=stdin_filename,
2433 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2434 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2435 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2436 # file being passed directly. This is the same as
2437 # test_exclude_for_issue_1572
2439 path = THIS_DIR / "data" / "include_exclude_tests"
2440 stdin_filename = str(path / "b/exclude/a.py")
2441 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2442 assert_collected_sources(
2445 extend_exclude=r"/exclude/|a\.py",
2446 stdin_filename=stdin_filename,
2449 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2450 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2451 # Force exclude should exclude the file when passing it through
2453 path = THIS_DIR / "data" / "include_exclude_tests"
2454 stdin_filename = str(path / "b/exclude/a.py")
2455 assert_collected_sources(
2458 force_exclude=r"/exclude/|a\.py",
2459 stdin_filename=stdin_filename,
2464 with open(black.__file__, "r", encoding="utf-8") as _bf:
2465 black_source_lines = _bf.readlines()
2466 except UnicodeDecodeError:
2467 if not black.COMPILED:
2472 frame: types.FrameType, event: str, arg: Any
2473 ) -> Callable[[types.FrameType, str, Any], Any]:
2474 """Show function calls `from black/__init__.py` as they happen.
2476 Register this with `sys.settrace()` in a test you're debugging.
2481 stack = len(inspect.stack()) - 19
2483 filename = frame.f_code.co_filename
2484 lineno = frame.f_lineno
2485 func_sig_lineno = lineno - 1
2486 funcname = black_source_lines[func_sig_lineno].strip()
2487 while funcname.startswith("@"):
2488 func_sig_lineno += 1
2489 funcname = black_source_lines[func_sig_lineno].strip()
2490 if "black/__init__.py" in filename:
2491 print(f"{' ' * stack}{lineno}:{funcname}")