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 FileData, 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 self.params: Dict[str, Any] = {}
108 # Dummy root, since most of the tests don't care about it
109 self.obj: Dict[str, Any] = {"root": PROJECT_ROOT}
112 class FakeParameter(click.Parameter):
113 """A fake click Parameter for when calling functions that need it."""
115 def __init__(self) -> None:
119 class BlackRunner(CliRunner):
120 """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
122 def __init__(self) -> None:
123 super().__init__(mix_stderr=False)
127 args: List[str], exit_code: int = 0, ignore_config: bool = True
129 runner = BlackRunner()
131 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
132 result = runner.invoke(black.main, args, catch_exceptions=False)
133 assert result.stdout_bytes is not None
134 assert result.stderr_bytes is not None
136 f"Failed with args: {args}\n"
137 f"stdout: {result.stdout_bytes.decode()!r}\n"
138 f"stderr: {result.stderr_bytes.decode()!r}\n"
139 f"exception: {result.exception}"
141 assert result.exit_code == exit_code, msg
144 class BlackTestCase(BlackBaseTestCase):
145 invokeBlack = staticmethod(invokeBlack)
147 def test_empty_ff(self) -> None:
149 tmp_file = Path(black.dump_to_file())
151 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
152 actual = tmp_file.read_text(encoding="utf-8")
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("utf-8")
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("utf-8")),
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("utf-8"))
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("utf-8"))
243 actual = result.output
244 # Again, the contents are checked in a different test, so only look for colors.
245 self.assertIn("\033[1m", actual)
246 self.assertIn("\033[36m", actual)
247 self.assertIn("\033[32m", actual)
248 self.assertIn("\033[31m", actual)
249 self.assertIn("\033[0m", actual)
251 @patch("black.dump_to_file", dump_to_stderr)
252 def _test_wip(self) -> None:
253 source, expected = read_data("miscellaneous", "wip")
254 sys.settrace(tracefunc)
257 experimental_string_processing=False,
258 target_versions={black.TargetVersion.PY38},
260 actual = fs(source, mode=mode)
262 self.assertFormatEqual(expected, actual)
263 black.assert_equivalent(source, actual)
264 black.assert_stable(source, actual, black.FileMode())
266 def test_pep_572_version_detection(self) -> None:
267 source, _ = read_data("py_38", "pep_572")
268 root = black.lib2to3_parse(source)
269 features = black.get_features_used(root)
270 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
271 versions = black.detect_target_versions(root)
272 self.assertIn(black.TargetVersion.PY38, versions)
274 def test_pep_695_version_detection(self) -> None:
275 for file in ("type_aliases", "type_params"):
276 source, _ = read_data("py_312", file)
277 root = black.lib2to3_parse(source)
278 features = black.get_features_used(root)
279 self.assertIn(black.Feature.TYPE_PARAMS, features)
280 versions = black.detect_target_versions(root)
281 self.assertIn(black.TargetVersion.PY312, versions)
283 def test_expression_ff(self) -> None:
284 source, expected = read_data("simple_cases", "expression.py")
285 tmp_file = Path(black.dump_to_file(source))
287 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
288 actual = tmp_file.read_text(encoding="utf-8")
291 self.assertFormatEqual(expected, actual)
292 with patch("black.dump_to_file", dump_to_stderr):
293 black.assert_equivalent(source, actual)
294 black.assert_stable(source, actual, DEFAULT_MODE)
296 def test_expression_diff(self) -> None:
297 source, _ = read_data("simple_cases", "expression.py")
298 expected, _ = read_data("simple_cases", "expression.diff")
299 tmp_file = Path(black.dump_to_file(source))
300 diff_header = re.compile(
301 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
302 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
305 result = BlackRunner().invoke(
306 black.main, ["--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
308 self.assertEqual(result.exit_code, 0)
311 actual = result.output
312 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
313 if expected != actual:
314 dump = black.dump_to_file(actual)
316 "Expected diff isn't equal to the actual. If you made changes to"
317 " expression.py and this is an anticipated difference, overwrite"
318 f" tests/data/expression.diff with {dump}"
320 self.assertEqual(expected, actual, msg)
322 def test_expression_diff_with_color(self) -> None:
323 source, _ = read_data("simple_cases", "expression.py")
324 expected, _ = read_data("simple_cases", "expression.diff")
325 tmp_file = Path(black.dump_to_file(source))
327 result = BlackRunner().invoke(
329 ["--diff", "--color", str(tmp_file), f"--config={EMPTY_CONFIG}"],
333 actual = result.output
334 # We check the contents of the diff in `test_expression_diff`. All
335 # we need to check here is that color codes exist in the result.
336 self.assertIn("\033[1m", actual)
337 self.assertIn("\033[36m", actual)
338 self.assertIn("\033[32m", actual)
339 self.assertIn("\033[31m", actual)
340 self.assertIn("\033[0m", actual)
342 def test_detect_pos_only_arguments(self) -> None:
343 source, _ = read_data("py_38", "pep_570")
344 root = black.lib2to3_parse(source)
345 features = black.get_features_used(root)
346 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
347 versions = black.detect_target_versions(root)
348 self.assertIn(black.TargetVersion.PY38, versions)
350 def test_detect_debug_f_strings(self) -> None:
351 root = black.lib2to3_parse("""f"{x=}" """)
352 features = black.get_features_used(root)
353 self.assertIn(black.Feature.DEBUG_F_STRINGS, features)
354 versions = black.detect_target_versions(root)
355 self.assertIn(black.TargetVersion.PY38, versions)
357 root = black.lib2to3_parse(
358 """f"{x}"\nf'{"="}'\nf'{(x:=5)}'\nf'{f(a="3=")}'\nf'{x:=10}'\n"""
360 features = black.get_features_used(root)
361 self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
363 # We don't yet support feature version detection in nested f-strings
364 root = black.lib2to3_parse(
365 """f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """
367 features = black.get_features_used(root)
368 self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
370 @patch("black.dump_to_file", dump_to_stderr)
371 def test_string_quotes(self) -> None:
372 source, expected = read_data("miscellaneous", "string_quotes")
373 mode = black.Mode(preview=True)
374 assert_format(source, expected, mode)
375 mode = replace(mode, string_normalization=False)
376 not_normalized = fs(source, mode=mode)
377 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
378 black.assert_equivalent(source, not_normalized)
379 black.assert_stable(source, not_normalized, mode=mode)
381 def test_skip_source_first_line(self) -> None:
382 source, _ = read_data("miscellaneous", "invalid_header")
383 tmp_file = Path(black.dump_to_file(source))
384 # Full source should fail (invalid syntax at header)
385 self.invokeBlack([str(tmp_file), "--diff", "--check"], exit_code=123)
386 # So, skipping the first line should work
387 result = BlackRunner().invoke(
388 black.main, [str(tmp_file), "-x", f"--config={EMPTY_CONFIG}"]
390 self.assertEqual(result.exit_code, 0)
391 actual = tmp_file.read_text(encoding="utf-8")
392 self.assertFormatEqual(source, actual)
394 def test_skip_source_first_line_when_mixing_newlines(self) -> None:
395 code_mixing_newlines = b"Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n"
396 expected = b"Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n"
397 with TemporaryDirectory() as workspace:
398 test_file = Path(workspace) / "skip_header.py"
399 test_file.write_bytes(code_mixing_newlines)
400 mode = replace(DEFAULT_MODE, skip_source_first_line=True)
401 ff(test_file, mode=mode, write_back=black.WriteBack.YES)
402 self.assertEqual(test_file.read_bytes(), expected)
404 def test_skip_magic_trailing_comma(self) -> None:
405 source, _ = read_data("simple_cases", "expression")
406 expected, _ = read_data(
407 "miscellaneous", "expression_skip_magic_trailing_comma.diff"
409 tmp_file = Path(black.dump_to_file(source))
410 diff_header = re.compile(
411 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
412 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
415 result = BlackRunner().invoke(
416 black.main, ["-C", "--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
418 self.assertEqual(result.exit_code, 0)
421 actual = result.output
422 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
423 actual = actual.rstrip() + "\n" # the diff output has a trailing space
424 if expected != actual:
425 dump = black.dump_to_file(actual)
427 "Expected diff isn't equal to the actual. If you made changes to"
428 " expression.py and this is an anticipated difference, overwrite"
429 " tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff"
432 self.assertEqual(expected, actual, msg)
434 @patch("black.dump_to_file", dump_to_stderr)
435 def test_async_as_identifier(self) -> None:
436 source_path = get_case_path("miscellaneous", "async_as_identifier")
437 source, expected = read_data_from_file(source_path)
439 self.assertFormatEqual(expected, actual)
440 major, minor = sys.version_info[:2]
441 if major < 3 or (major <= 3 and minor < 7):
442 black.assert_equivalent(source, actual)
443 black.assert_stable(source, actual, DEFAULT_MODE)
444 # ensure black can parse this when the target is 3.6
445 self.invokeBlack([str(source_path), "--target-version", "py36"])
446 # but not on 3.7, because async/await is no longer an identifier
447 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
449 @patch("black.dump_to_file", dump_to_stderr)
450 def test_python37(self) -> None:
451 source_path = get_case_path("py_37", "python37")
452 source, expected = read_data_from_file(source_path)
454 self.assertFormatEqual(expected, actual)
455 major, minor = sys.version_info[:2]
456 if major > 3 or (major == 3 and minor >= 7):
457 black.assert_equivalent(source, actual)
458 black.assert_stable(source, actual, DEFAULT_MODE)
459 # ensure black can parse this when the target is 3.7
460 self.invokeBlack([str(source_path), "--target-version", "py37"])
461 # but not on 3.6, because we use async as a reserved keyword
462 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
464 def test_tab_comment_indentation(self) -> None:
465 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
466 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
467 self.assertFormatEqual(contents_spc, fs(contents_spc))
468 self.assertFormatEqual(contents_spc, fs(contents_tab))
470 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
471 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
472 self.assertFormatEqual(contents_spc, fs(contents_spc))
473 self.assertFormatEqual(contents_spc, fs(contents_tab))
475 # mixed tabs and spaces (valid Python 2 code)
476 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
477 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
478 self.assertFormatEqual(contents_spc, fs(contents_spc))
479 self.assertFormatEqual(contents_spc, fs(contents_tab))
481 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
482 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
483 self.assertFormatEqual(contents_spc, fs(contents_spc))
484 self.assertFormatEqual(contents_spc, fs(contents_tab))
486 def test_false_positive_symlink_output_issue_3384(self) -> None:
487 # Emulate the behavior when using the CLI (`black ./child --verbose`), which
488 # involves patching some `pathlib.Path` methods. In particular, `is_dir` is
489 # patched only on its first call: when checking if "./child" is a directory it
490 # should return True. The "./child" folder exists relative to the cwd when
491 # running from CLI, but fails when running the tests because cwd is different
492 project_root = Path(THIS_DIR / "data" / "nested_gitignore_tests")
493 working_directory = project_root / "root"
494 target_abspath = working_directory / "child"
495 target_contents = list(target_abspath.iterdir())
497 def mock_n_calls(responses: List[bool]) -> Callable[[], bool]:
498 def _mocked_calls() -> bool:
500 return responses.pop(0)
505 with patch("pathlib.Path.iterdir", return_value=target_contents), patch(
506 "pathlib.Path.cwd", return_value=working_directory
507 ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])):
508 # Note that the root folder (project_root) isn't the folder
509 # named "root" (aka working_directory)
510 report = MagicMock(verbose=True)
516 include=DEFAULT_INCLUDE,
524 mock_args[1].startswith("is a symbolic link that points outside")
525 for _, mock_args, _ in report.path_ignored.mock_calls
526 ), "A symbolic link was reported."
527 report.path_ignored.assert_called_once_with(
528 Path("root", "child", "b.py"), "matches a .gitignore file content"
531 def test_report_verbose(self) -> None:
532 report = Report(verbose=True)
536 def out(msg: str, **kwargs: Any) -> None:
537 out_lines.append(msg)
539 def err(msg: str, **kwargs: Any) -> None:
540 err_lines.append(msg)
542 with patch("black.output._out", out), patch("black.output._err", err):
543 report.done(Path("f1"), black.Changed.NO)
544 self.assertEqual(len(out_lines), 1)
545 self.assertEqual(len(err_lines), 0)
546 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
547 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
548 self.assertEqual(report.return_code, 0)
549 report.done(Path("f2"), black.Changed.YES)
550 self.assertEqual(len(out_lines), 2)
551 self.assertEqual(len(err_lines), 0)
552 self.assertEqual(out_lines[-1], "reformatted f2")
554 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
556 report.done(Path("f3"), black.Changed.CACHED)
557 self.assertEqual(len(out_lines), 3)
558 self.assertEqual(len(err_lines), 0)
560 out_lines[-1], "f3 wasn't modified on disk since last run."
563 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
565 self.assertEqual(report.return_code, 0)
567 self.assertEqual(report.return_code, 1)
569 report.failed(Path("e1"), "boom")
570 self.assertEqual(len(out_lines), 3)
571 self.assertEqual(len(err_lines), 1)
572 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
574 unstyle(str(report)),
575 "1 file reformatted, 2 files left unchanged, 1 file failed to"
578 self.assertEqual(report.return_code, 123)
579 report.done(Path("f3"), black.Changed.YES)
580 self.assertEqual(len(out_lines), 4)
581 self.assertEqual(len(err_lines), 1)
582 self.assertEqual(out_lines[-1], "reformatted f3")
584 unstyle(str(report)),
585 "2 files reformatted, 2 files left unchanged, 1 file failed to"
588 self.assertEqual(report.return_code, 123)
589 report.failed(Path("e2"), "boom")
590 self.assertEqual(len(out_lines), 4)
591 self.assertEqual(len(err_lines), 2)
592 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
594 unstyle(str(report)),
595 "2 files reformatted, 2 files left unchanged, 2 files failed to"
598 self.assertEqual(report.return_code, 123)
599 report.path_ignored(Path("wat"), "no match")
600 self.assertEqual(len(out_lines), 5)
601 self.assertEqual(len(err_lines), 2)
602 self.assertEqual(out_lines[-1], "wat ignored: no match")
604 unstyle(str(report)),
605 "2 files reformatted, 2 files left unchanged, 2 files failed to"
608 self.assertEqual(report.return_code, 123)
609 report.done(Path("f4"), black.Changed.NO)
610 self.assertEqual(len(out_lines), 6)
611 self.assertEqual(len(err_lines), 2)
612 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
614 unstyle(str(report)),
615 "2 files reformatted, 3 files left unchanged, 2 files failed to"
618 self.assertEqual(report.return_code, 123)
621 unstyle(str(report)),
622 "2 files would be reformatted, 3 files would be left unchanged, 2"
623 " files would fail to reformat.",
628 unstyle(str(report)),
629 "2 files would be reformatted, 3 files would be left unchanged, 2"
630 " files would fail to reformat.",
633 def test_report_quiet(self) -> None:
634 report = Report(quiet=True)
638 def out(msg: str, **kwargs: Any) -> None:
639 out_lines.append(msg)
641 def err(msg: str, **kwargs: Any) -> None:
642 err_lines.append(msg)
644 with patch("black.output._out", out), patch("black.output._err", err):
645 report.done(Path("f1"), black.Changed.NO)
646 self.assertEqual(len(out_lines), 0)
647 self.assertEqual(len(err_lines), 0)
648 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
649 self.assertEqual(report.return_code, 0)
650 report.done(Path("f2"), black.Changed.YES)
651 self.assertEqual(len(out_lines), 0)
652 self.assertEqual(len(err_lines), 0)
654 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
656 report.done(Path("f3"), black.Changed.CACHED)
657 self.assertEqual(len(out_lines), 0)
658 self.assertEqual(len(err_lines), 0)
660 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
662 self.assertEqual(report.return_code, 0)
664 self.assertEqual(report.return_code, 1)
666 report.failed(Path("e1"), "boom")
667 self.assertEqual(len(out_lines), 0)
668 self.assertEqual(len(err_lines), 1)
669 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
671 unstyle(str(report)),
672 "1 file reformatted, 2 files left unchanged, 1 file failed to"
675 self.assertEqual(report.return_code, 123)
676 report.done(Path("f3"), black.Changed.YES)
677 self.assertEqual(len(out_lines), 0)
678 self.assertEqual(len(err_lines), 1)
680 unstyle(str(report)),
681 "2 files reformatted, 2 files left unchanged, 1 file failed to"
684 self.assertEqual(report.return_code, 123)
685 report.failed(Path("e2"), "boom")
686 self.assertEqual(len(out_lines), 0)
687 self.assertEqual(len(err_lines), 2)
688 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
690 unstyle(str(report)),
691 "2 files reformatted, 2 files left unchanged, 2 files failed to"
694 self.assertEqual(report.return_code, 123)
695 report.path_ignored(Path("wat"), "no match")
696 self.assertEqual(len(out_lines), 0)
697 self.assertEqual(len(err_lines), 2)
699 unstyle(str(report)),
700 "2 files reformatted, 2 files left unchanged, 2 files failed to"
703 self.assertEqual(report.return_code, 123)
704 report.done(Path("f4"), black.Changed.NO)
705 self.assertEqual(len(out_lines), 0)
706 self.assertEqual(len(err_lines), 2)
708 unstyle(str(report)),
709 "2 files reformatted, 3 files left unchanged, 2 files failed to"
712 self.assertEqual(report.return_code, 123)
715 unstyle(str(report)),
716 "2 files would be reformatted, 3 files would be left unchanged, 2"
717 " files would fail to reformat.",
722 unstyle(str(report)),
723 "2 files would be reformatted, 3 files would be left unchanged, 2"
724 " files would fail to reformat.",
727 def test_report_normal(self) -> None:
728 report = black.Report()
732 def out(msg: str, **kwargs: Any) -> None:
733 out_lines.append(msg)
735 def err(msg: str, **kwargs: Any) -> None:
736 err_lines.append(msg)
738 with patch("black.output._out", out), patch("black.output._err", err):
739 report.done(Path("f1"), black.Changed.NO)
740 self.assertEqual(len(out_lines), 0)
741 self.assertEqual(len(err_lines), 0)
742 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
743 self.assertEqual(report.return_code, 0)
744 report.done(Path("f2"), black.Changed.YES)
745 self.assertEqual(len(out_lines), 1)
746 self.assertEqual(len(err_lines), 0)
747 self.assertEqual(out_lines[-1], "reformatted f2")
749 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
751 report.done(Path("f3"), black.Changed.CACHED)
752 self.assertEqual(len(out_lines), 1)
753 self.assertEqual(len(err_lines), 0)
754 self.assertEqual(out_lines[-1], "reformatted f2")
756 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
758 self.assertEqual(report.return_code, 0)
760 self.assertEqual(report.return_code, 1)
762 report.failed(Path("e1"), "boom")
763 self.assertEqual(len(out_lines), 1)
764 self.assertEqual(len(err_lines), 1)
765 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
767 unstyle(str(report)),
768 "1 file reformatted, 2 files left unchanged, 1 file failed to"
771 self.assertEqual(report.return_code, 123)
772 report.done(Path("f3"), black.Changed.YES)
773 self.assertEqual(len(out_lines), 2)
774 self.assertEqual(len(err_lines), 1)
775 self.assertEqual(out_lines[-1], "reformatted f3")
777 unstyle(str(report)),
778 "2 files reformatted, 2 files left unchanged, 1 file failed to"
781 self.assertEqual(report.return_code, 123)
782 report.failed(Path("e2"), "boom")
783 self.assertEqual(len(out_lines), 2)
784 self.assertEqual(len(err_lines), 2)
785 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
787 unstyle(str(report)),
788 "2 files reformatted, 2 files left unchanged, 2 files failed to"
791 self.assertEqual(report.return_code, 123)
792 report.path_ignored(Path("wat"), "no match")
793 self.assertEqual(len(out_lines), 2)
794 self.assertEqual(len(err_lines), 2)
796 unstyle(str(report)),
797 "2 files reformatted, 2 files left unchanged, 2 files failed to"
800 self.assertEqual(report.return_code, 123)
801 report.done(Path("f4"), black.Changed.NO)
802 self.assertEqual(len(out_lines), 2)
803 self.assertEqual(len(err_lines), 2)
805 unstyle(str(report)),
806 "2 files reformatted, 3 files left unchanged, 2 files failed to"
809 self.assertEqual(report.return_code, 123)
812 unstyle(str(report)),
813 "2 files would be reformatted, 3 files would be left unchanged, 2"
814 " files would fail to reformat.",
819 unstyle(str(report)),
820 "2 files would be reformatted, 3 files would be left unchanged, 2"
821 " files would fail to reformat.",
824 def test_lib2to3_parse(self) -> None:
825 with self.assertRaises(black.InvalidInput):
826 black.lib2to3_parse("invalid syntax")
829 black.lib2to3_parse(straddling)
830 black.lib2to3_parse(straddling, {TargetVersion.PY36})
833 with self.assertRaises(black.InvalidInput):
834 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
836 py3_only = "exec(x, end=y)"
837 black.lib2to3_parse(py3_only)
838 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
840 def test_get_features_used_decorator(self) -> None:
841 # Test the feature detection of new decorator syntax
842 # since this makes some test cases of test_get_features_used()
843 # fails if it fails, this is tested first so that a useful case
845 simples, relaxed = read_data("miscellaneous", "decorators")
846 # skip explanation comments at the top of the file
847 for simple_test in simples.split("##")[1:]:
848 node = black.lib2to3_parse(simple_test)
849 decorator = str(node.children[0].children[0]).strip()
851 Feature.RELAXED_DECORATORS,
852 black.get_features_used(node),
854 f"decorator '{decorator}' follows python<=3.8 syntax"
855 "but is detected as 3.9+"
856 # f"The full node is\n{node!r}"
859 # skip the '# output' comment at the top of the output part
860 for relaxed_test in relaxed.split("##")[1:]:
861 node = black.lib2to3_parse(relaxed_test)
862 decorator = str(node.children[0].children[0]).strip()
864 Feature.RELAXED_DECORATORS,
865 black.get_features_used(node),
867 f"decorator '{decorator}' uses python3.9+ syntax"
868 "but is detected as python<=3.8"
869 # f"The full node is\n{node!r}"
873 def test_get_features_used(self) -> None:
874 node = black.lib2to3_parse("def f(*, arg): ...\n")
875 self.assertEqual(black.get_features_used(node), set())
876 node = black.lib2to3_parse("def f(*, arg,): ...\n")
877 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
878 node = black.lib2to3_parse("f(*arg,)\n")
880 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
882 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
883 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
884 node = black.lib2to3_parse("123_456\n")
885 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
886 node = black.lib2to3_parse("123456\n")
887 self.assertEqual(black.get_features_used(node), set())
888 source, expected = read_data("simple_cases", "function")
889 node = black.lib2to3_parse(source)
890 expected_features = {
891 Feature.TRAILING_COMMA_IN_CALL,
892 Feature.TRAILING_COMMA_IN_DEF,
895 self.assertEqual(black.get_features_used(node), expected_features)
896 node = black.lib2to3_parse(expected)
897 self.assertEqual(black.get_features_used(node), expected_features)
898 source, expected = read_data("simple_cases", "expression")
899 node = black.lib2to3_parse(source)
900 self.assertEqual(black.get_features_used(node), set())
901 node = black.lib2to3_parse(expected)
902 self.assertEqual(black.get_features_used(node), set())
903 node = black.lib2to3_parse("lambda a, /, b: ...")
904 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
905 node = black.lib2to3_parse("def fn(a, /, b): ...")
906 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
907 node = black.lib2to3_parse("def fn(): yield a, b")
908 self.assertEqual(black.get_features_used(node), set())
909 node = black.lib2to3_parse("def fn(): return a, b")
910 self.assertEqual(black.get_features_used(node), set())
911 node = black.lib2to3_parse("def fn(): yield *b, c")
912 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
913 node = black.lib2to3_parse("def fn(): return a, *b, c")
914 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
915 node = black.lib2to3_parse("x = a, *b, c")
916 self.assertEqual(black.get_features_used(node), set())
917 node = black.lib2to3_parse("x: Any = regular")
918 self.assertEqual(black.get_features_used(node), set())
919 node = black.lib2to3_parse("x: Any = (regular, regular)")
920 self.assertEqual(black.get_features_used(node), set())
921 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
922 self.assertEqual(black.get_features_used(node), set())
923 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
925 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
927 node = black.lib2to3_parse("try: pass\nexcept Something: pass")
928 self.assertEqual(black.get_features_used(node), set())
929 node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass")
930 self.assertEqual(black.get_features_used(node), set())
931 node = black.lib2to3_parse("try: pass\nexcept *Group: pass")
932 self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR})
933 node = black.lib2to3_parse("a[*b]")
934 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
935 node = black.lib2to3_parse("a[x, *y(), z] = t")
936 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
937 node = black.lib2to3_parse("def fn(*args: *T): pass")
938 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
940 def test_get_features_used_for_future_flags(self) -> None:
941 for src, features in [
942 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
944 "from __future__ import (other, annotations)",
945 {Feature.FUTURE_ANNOTATIONS},
947 ("a = 1 + 2\nfrom something import annotations", set()),
948 ("from __future__ import x, y", set()),
950 with self.subTest(src=src, features=features):
951 node = black.lib2to3_parse(src)
952 future_imports = black.get_future_imports(node)
954 black.get_features_used(node, future_imports=future_imports),
958 def test_get_future_imports(self) -> None:
959 node = black.lib2to3_parse("\n")
960 self.assertEqual(set(), black.get_future_imports(node))
961 node = black.lib2to3_parse("from __future__ import black\n")
962 self.assertEqual({"black"}, black.get_future_imports(node))
963 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
964 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
965 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
966 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
967 node = black.lib2to3_parse(
968 "from __future__ import multiple\nfrom __future__ import imports\n"
970 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
971 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
972 self.assertEqual({"black"}, black.get_future_imports(node))
973 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
974 self.assertEqual({"black"}, black.get_future_imports(node))
975 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
976 self.assertEqual(set(), black.get_future_imports(node))
977 node = black.lib2to3_parse("from some.module import black\n")
978 self.assertEqual(set(), black.get_future_imports(node))
979 node = black.lib2to3_parse(
980 "from __future__ import unicode_literals as _unicode_literals"
982 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
983 node = black.lib2to3_parse(
984 "from __future__ import unicode_literals as _lol, print"
986 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
988 @pytest.mark.incompatible_with_mypyc
989 def test_debug_visitor(self) -> None:
990 source, _ = read_data("miscellaneous", "debug_visitor")
991 expected, _ = read_data("miscellaneous", "debug_visitor.out")
995 def out(msg: str, **kwargs: Any) -> None:
996 out_lines.append(msg)
998 def err(msg: str, **kwargs: Any) -> None:
999 err_lines.append(msg)
1001 with patch("black.debug.out", out):
1002 DebugVisitor.show(source)
1003 actual = "\n".join(out_lines) + "\n"
1005 if expected != actual:
1006 log_name = black.dump_to_file(*out_lines)
1010 f"AST print out is different. Actual version dumped to {log_name}",
1013 def test_format_file_contents(self) -> None:
1016 with self.assertRaises(black.NothingChanged):
1017 black.format_file_contents(empty, mode=mode, fast=False)
1019 with self.assertRaises(black.NothingChanged):
1020 black.format_file_contents(just_nl, mode=mode, fast=False)
1021 same = "j = [1, 2, 3]\n"
1022 with self.assertRaises(black.NothingChanged):
1023 black.format_file_contents(same, mode=mode, fast=False)
1024 different = "j = [1,2,3]"
1026 actual = black.format_file_contents(different, mode=mode, fast=False)
1027 self.assertEqual(expected, actual)
1028 invalid = "return if you can"
1029 with self.assertRaises(black.InvalidInput) as e:
1030 black.format_file_contents(invalid, mode=mode, fast=False)
1031 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
1033 mode = black.Mode(preview=True)
1035 with self.assertRaises(black.NothingChanged):
1036 black.format_file_contents(just_crlf, mode=mode, fast=False)
1037 just_whitespace_nl = "\n\t\n \n\t \n \t\n\n"
1038 actual = black.format_file_contents(just_whitespace_nl, mode=mode, fast=False)
1039 self.assertEqual("\n", actual)
1040 just_whitespace_crlf = "\r\n\t\r\n \r\n\t \r\n \t\r\n\r\n"
1041 actual = black.format_file_contents(just_whitespace_crlf, mode=mode, fast=False)
1042 self.assertEqual("\r\n", actual)
1044 def test_endmarker(self) -> None:
1045 n = black.lib2to3_parse("\n")
1046 self.assertEqual(n.type, black.syms.file_input)
1047 self.assertEqual(len(n.children), 1)
1048 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
1050 @pytest.mark.incompatible_with_mypyc
1051 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
1052 def test_assertFormatEqual(self) -> None:
1056 def out(msg: str, **kwargs: Any) -> None:
1057 out_lines.append(msg)
1059 def err(msg: str, **kwargs: Any) -> None:
1060 err_lines.append(msg)
1062 with patch("black.output._out", out), patch("black.output._err", err):
1063 with self.assertRaises(AssertionError):
1064 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
1066 out_str = "".join(out_lines)
1067 self.assertIn("Expected tree:", out_str)
1068 self.assertIn("Actual tree:", out_str)
1069 self.assertEqual("".join(err_lines), "")
1072 @patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1073 def test_works_in_mono_process_only_environment(self) -> None:
1074 with cache_dir() as workspace:
1076 (workspace / "one.py").resolve(),
1077 (workspace / "two.py").resolve(),
1079 f.write_text('print("hello")\n', encoding="utf-8")
1080 self.invokeBlack([str(workspace)])
1083 def test_check_diff_use_together(self) -> None:
1085 # Files which will be reformatted.
1086 src1 = get_case_path("miscellaneous", "string_quotes")
1087 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1088 # Files which will not be reformatted.
1089 src2 = get_case_path("simple_cases", "composition")
1090 self.invokeBlack([str(src2), "--diff", "--check"])
1091 # Multi file command.
1092 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1094 def test_no_src_fails(self) -> None:
1096 self.invokeBlack([], exit_code=1)
1098 def test_src_and_code_fails(self) -> None:
1100 self.invokeBlack([".", "-c", "0"], exit_code=1)
1102 def test_broken_symlink(self) -> None:
1103 with cache_dir() as workspace:
1104 symlink = workspace / "broken_link.py"
1106 symlink.symlink_to("nonexistent.py")
1107 except (OSError, NotImplementedError) as e:
1108 self.skipTest(f"Can't create symlinks: {e}")
1109 self.invokeBlack([str(workspace.resolve())])
1111 def test_single_file_force_pyi(self) -> None:
1112 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1113 contents, expected = read_data("miscellaneous", "force_pyi")
1114 with cache_dir() as workspace:
1115 path = (workspace / "file.py").resolve()
1116 path.write_text(contents, encoding="utf-8")
1117 self.invokeBlack([str(path), "--pyi"])
1118 actual = path.read_text(encoding="utf-8")
1119 # verify cache with --pyi is separate
1120 pyi_cache = black.Cache.read(pyi_mode)
1121 assert not pyi_cache.is_changed(path)
1122 normal_cache = black.Cache.read(DEFAULT_MODE)
1123 assert normal_cache.is_changed(path)
1124 self.assertFormatEqual(expected, actual)
1125 black.assert_equivalent(contents, actual)
1126 black.assert_stable(contents, actual, pyi_mode)
1129 def test_multi_file_force_pyi(self) -> None:
1130 reg_mode = DEFAULT_MODE
1131 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1132 contents, expected = read_data("miscellaneous", "force_pyi")
1133 with cache_dir() as workspace:
1135 (workspace / "file1.py").resolve(),
1136 (workspace / "file2.py").resolve(),
1139 path.write_text(contents, encoding="utf-8")
1140 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1142 actual = path.read_text(encoding="utf-8")
1143 self.assertEqual(actual, expected)
1144 # verify cache with --pyi is separate
1145 pyi_cache = black.Cache.read(pyi_mode)
1146 normal_cache = black.Cache.read(reg_mode)
1148 assert not pyi_cache.is_changed(path)
1149 assert normal_cache.is_changed(path)
1151 def test_pipe_force_pyi(self) -> None:
1152 source, expected = read_data("miscellaneous", "force_pyi")
1153 result = CliRunner().invoke(
1154 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf-8"))
1156 self.assertEqual(result.exit_code, 0)
1157 actual = result.output
1158 self.assertFormatEqual(actual, expected)
1160 def test_single_file_force_py36(self) -> None:
1161 reg_mode = DEFAULT_MODE
1162 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1163 source, expected = read_data("miscellaneous", "force_py36")
1164 with cache_dir() as workspace:
1165 path = (workspace / "file.py").resolve()
1166 path.write_text(source, encoding="utf-8")
1167 self.invokeBlack([str(path), *PY36_ARGS])
1168 actual = path.read_text(encoding="utf-8")
1169 # verify cache with --target-version is separate
1170 py36_cache = black.Cache.read(py36_mode)
1171 assert not py36_cache.is_changed(path)
1172 normal_cache = black.Cache.read(reg_mode)
1173 assert normal_cache.is_changed(path)
1174 self.assertEqual(actual, expected)
1177 def test_multi_file_force_py36(self) -> None:
1178 reg_mode = DEFAULT_MODE
1179 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1180 source, expected = read_data("miscellaneous", "force_py36")
1181 with cache_dir() as workspace:
1183 (workspace / "file1.py").resolve(),
1184 (workspace / "file2.py").resolve(),
1187 path.write_text(source, encoding="utf-8")
1188 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1190 actual = path.read_text(encoding="utf-8")
1191 self.assertEqual(actual, expected)
1192 # verify cache with --target-version is separate
1193 pyi_cache = black.Cache.read(py36_mode)
1194 normal_cache = black.Cache.read(reg_mode)
1196 assert not pyi_cache.is_changed(path)
1197 assert normal_cache.is_changed(path)
1199 def test_pipe_force_py36(self) -> None:
1200 source, expected = read_data("miscellaneous", "force_py36")
1201 result = CliRunner().invoke(
1203 ["-", "-q", "--target-version=py36"],
1204 input=BytesIO(source.encode("utf-8")),
1206 self.assertEqual(result.exit_code, 0)
1207 actual = result.output
1208 self.assertFormatEqual(actual, expected)
1210 @pytest.mark.incompatible_with_mypyc
1211 def test_reformat_one_with_stdin(self) -> None:
1213 "black.format_stdin_to_stdout",
1214 return_value=lambda *args, **kwargs: black.Changed.YES,
1216 report = MagicMock()
1221 write_back=black.WriteBack.YES,
1225 fsts.assert_called_once()
1226 report.done.assert_called_with(path, black.Changed.YES)
1228 @pytest.mark.incompatible_with_mypyc
1229 def test_reformat_one_with_stdin_filename(self) -> None:
1231 "black.format_stdin_to_stdout",
1232 return_value=lambda *args, **kwargs: black.Changed.YES,
1234 report = MagicMock()
1236 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1241 write_back=black.WriteBack.YES,
1245 fsts.assert_called_once_with(
1246 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1248 # __BLACK_STDIN_FILENAME__ should have been stripped
1249 report.done.assert_called_with(expected, black.Changed.YES)
1251 @pytest.mark.incompatible_with_mypyc
1252 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1254 "black.format_stdin_to_stdout",
1255 return_value=lambda *args, **kwargs: black.Changed.YES,
1257 report = MagicMock()
1259 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1264 write_back=black.WriteBack.YES,
1268 fsts.assert_called_once_with(
1270 write_back=black.WriteBack.YES,
1271 mode=replace(DEFAULT_MODE, is_pyi=True),
1273 # __BLACK_STDIN_FILENAME__ should have been stripped
1274 report.done.assert_called_with(expected, black.Changed.YES)
1276 @pytest.mark.incompatible_with_mypyc
1277 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1279 "black.format_stdin_to_stdout",
1280 return_value=lambda *args, **kwargs: black.Changed.YES,
1282 report = MagicMock()
1284 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1289 write_back=black.WriteBack.YES,
1293 fsts.assert_called_once_with(
1295 write_back=black.WriteBack.YES,
1296 mode=replace(DEFAULT_MODE, is_ipynb=True),
1298 # __BLACK_STDIN_FILENAME__ should have been stripped
1299 report.done.assert_called_with(expected, black.Changed.YES)
1301 @pytest.mark.incompatible_with_mypyc
1302 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1304 "black.format_stdin_to_stdout",
1305 return_value=lambda *args, **kwargs: black.Changed.YES,
1307 report = MagicMock()
1308 # Even with an existing file, since we are forcing stdin, black
1309 # should output to stdout and not modify the file inplace
1310 p = THIS_DIR / "data" / "simple_cases" / "collections.py"
1311 # Make sure is_file actually returns True
1312 self.assertTrue(p.is_file())
1313 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1318 write_back=black.WriteBack.YES,
1322 fsts.assert_called_once()
1323 # __BLACK_STDIN_FILENAME__ should have been stripped
1324 report.done.assert_called_with(expected, black.Changed.YES)
1326 def test_reformat_one_with_stdin_empty(self) -> None:
1333 (" \t\r\n\t ", "\r\n"),
1337 output: io.StringIO, io_TextIOWrapper: Type[io.TextIOWrapper]
1338 ) -> Callable[[Any, Any], io.TextIOWrapper]:
1339 def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper:
1340 if args == (sys.stdout.buffer,):
1341 # It's `format_stdin_to_stdout()` calling `io.TextIOWrapper()`,
1342 # return our mock object.
1344 # It's something else (i.e. `decode_bytes()`) calling
1345 # `io.TextIOWrapper()`, pass through to the original implementation.
1346 # See discussion in https://github.com/psf/black/pull/2489
1347 return io_TextIOWrapper(*args, **kwargs)
1351 mode = black.Mode(preview=True)
1352 for content, expected in cases:
1353 output = io.StringIO()
1354 io_TextIOWrapper = io.TextIOWrapper
1356 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1358 black.format_stdin_to_stdout(
1361 write_back=black.WriteBack.YES,
1364 except io.UnsupportedOperation:
1365 pass # StringIO does not support detach
1366 assert output.getvalue() == expected
1368 # An empty string is the only test case for `preview=False`
1369 output = io.StringIO()
1370 io_TextIOWrapper = io.TextIOWrapper
1371 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1373 black.format_stdin_to_stdout(
1376 write_back=black.WriteBack.YES,
1379 except io.UnsupportedOperation:
1380 pass # StringIO does not support detach
1381 assert output.getvalue() == ""
1383 def test_invalid_cli_regex(self) -> None:
1384 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1385 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1387 def test_required_version_matches_version(self) -> None:
1389 ["--required-version", black.__version__, "-c", "0"],
1394 def test_required_version_matches_partial_version(self) -> None:
1396 ["--required-version", black.__version__.split(".")[0], "-c", "0"],
1401 def test_required_version_does_not_match_on_minor_version(self) -> None:
1403 ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
1408 def test_required_version_does_not_match_version(self) -> None:
1409 result = BlackRunner().invoke(
1411 ["--required-version", "20.99b", "-c", "0"],
1413 self.assertEqual(result.exit_code, 1)
1414 self.assertIn("required version", result.stderr)
1416 def test_preserves_line_endings(self) -> None:
1417 with TemporaryDirectory() as workspace:
1418 test_file = Path(workspace) / "test.py"
1419 for nl in ["\n", "\r\n"]:
1420 contents = nl.join(["def f( ):", " pass"])
1421 test_file.write_bytes(contents.encode())
1422 ff(test_file, write_back=black.WriteBack.YES)
1423 updated_contents: bytes = test_file.read_bytes()
1424 self.assertIn(nl.encode(), updated_contents)
1426 self.assertNotIn(b"\r\n", updated_contents)
1428 def test_preserves_line_endings_via_stdin(self) -> None:
1429 for nl in ["\n", "\r\n"]:
1430 contents = nl.join(["def f( ):", " pass"])
1431 runner = BlackRunner()
1432 result = runner.invoke(
1433 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf-8"))
1435 self.assertEqual(result.exit_code, 0)
1436 output = result.stdout_bytes
1437 self.assertIn(nl.encode("utf-8"), output)
1439 self.assertNotIn(b"\r\n", output)
1441 def test_normalize_line_endings(self) -> None:
1442 with TemporaryDirectory() as workspace:
1443 test_file = Path(workspace) / "test.py"
1444 for data, expected in (
1445 (b"c\r\nc\n ", b"c\r\nc\r\n"),
1446 (b"l\nl\r\n ", b"l\nl\n"),
1448 test_file.write_bytes(data)
1449 ff(test_file, write_back=black.WriteBack.YES)
1450 self.assertEqual(test_file.read_bytes(), expected)
1452 def test_assert_equivalent_different_asts(self) -> None:
1453 with self.assertRaises(AssertionError):
1454 black.assert_equivalent("{}", "None")
1456 def test_root_logger_not_used_directly(self) -> None:
1457 def fail(*args: Any, **kwargs: Any) -> None:
1458 self.fail("Record created with root logger")
1460 with patch.multiple(
1469 ff(THIS_DIR / "util.py")
1471 def test_invalid_config_return_code(self) -> None:
1472 tmp_file = Path(black.dump_to_file())
1474 tmp_config = Path(black.dump_to_file())
1476 args = ["--config", str(tmp_config), str(tmp_file)]
1477 self.invokeBlack(args, exit_code=2, ignore_config=False)
1481 def test_parse_pyproject_toml(self) -> None:
1482 test_toml_file = THIS_DIR / "test.toml"
1483 config = black.parse_pyproject_toml(str(test_toml_file))
1484 self.assertEqual(config["verbose"], 1)
1485 self.assertEqual(config["check"], "no")
1486 self.assertEqual(config["diff"], "y")
1487 self.assertEqual(config["color"], True)
1488 self.assertEqual(config["line_length"], 79)
1489 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1490 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1491 self.assertEqual(config["exclude"], r"\.pyi?$")
1492 self.assertEqual(config["include"], r"\.py?$")
1494 def test_parse_pyproject_toml_project_metadata(self) -> None:
1495 for test_toml, expected in [
1496 ("only_black_pyproject.toml", ["py310"]),
1497 ("only_metadata_pyproject.toml", ["py37", "py38", "py39", "py310"]),
1498 ("neither_pyproject.toml", None),
1499 ("both_pyproject.toml", ["py310"]),
1501 test_toml_file = THIS_DIR / "data" / "project_metadata" / test_toml
1502 config = black.parse_pyproject_toml(str(test_toml_file))
1503 self.assertEqual(config.get("target_version"), expected)
1505 def test_infer_target_version(self) -> None:
1506 for version, expected in [
1507 ("3.6", [TargetVersion.PY36]),
1508 ("3.11.0rc1", [TargetVersion.PY311]),
1509 (">=3.10", [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312]),
1512 [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
1514 ("<3.6", [TargetVersion.PY33, TargetVersion.PY34, TargetVersion.PY35]),
1515 (">3.7,<3.10", [TargetVersion.PY38, TargetVersion.PY39]),
1518 [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
1521 "> 3.9.4, != 3.10.3",
1524 TargetVersion.PY310,
1525 TargetVersion.PY311,
1526 TargetVersion.PY312,
1537 TargetVersion.PY310,
1538 TargetVersion.PY311,
1539 TargetVersion.PY312,
1552 TargetVersion.PY310,
1553 TargetVersion.PY311,
1554 TargetVersion.PY312,
1557 ("==3.8.*", [TargetVersion.PY38]),
1561 ("==invalid", None),
1562 (">3.9,!=invalid", None),
1567 (">3.10,<3.11", None),
1569 test_toml = {"project": {"requires-python": version}}
1570 result = black.files.infer_target_version(test_toml)
1571 self.assertEqual(result, expected)
1573 def test_read_pyproject_toml(self) -> None:
1574 test_toml_file = THIS_DIR / "test.toml"
1575 fake_ctx = FakeContext()
1576 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1577 config = fake_ctx.default_map
1578 self.assertEqual(config["verbose"], "1")
1579 self.assertEqual(config["check"], "no")
1580 self.assertEqual(config["diff"], "y")
1581 self.assertEqual(config["color"], "True")
1582 self.assertEqual(config["line_length"], "79")
1583 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1584 self.assertEqual(config["exclude"], r"\.pyi?$")
1585 self.assertEqual(config["include"], r"\.py?$")
1587 def test_read_pyproject_toml_from_stdin(self) -> None:
1588 with TemporaryDirectory() as workspace:
1589 root = Path(workspace)
1591 src_dir = root / "src"
1594 src_pyproject = src_dir / "pyproject.toml"
1595 src_pyproject.touch()
1597 test_toml_content = (THIS_DIR / "test.toml").read_text(encoding="utf-8")
1598 src_pyproject.write_text(test_toml_content, encoding="utf-8")
1600 src_python = src_dir / "foo.py"
1603 fake_ctx = FakeContext()
1604 fake_ctx.params["src"] = ("-",)
1605 fake_ctx.params["stdin_filename"] = str(src_python)
1607 with change_directory(root):
1608 black.read_pyproject_toml(fake_ctx, FakeParameter(), None)
1610 config = fake_ctx.default_map
1611 self.assertEqual(config["verbose"], "1")
1612 self.assertEqual(config["check"], "no")
1613 self.assertEqual(config["diff"], "y")
1614 self.assertEqual(config["color"], "True")
1615 self.assertEqual(config["line_length"], "79")
1616 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1617 self.assertEqual(config["exclude"], r"\.pyi?$")
1618 self.assertEqual(config["include"], r"\.py?$")
1620 @pytest.mark.incompatible_with_mypyc
1621 def test_find_project_root(self) -> None:
1622 with TemporaryDirectory() as workspace:
1623 root = Path(workspace)
1624 test_dir = root / "test"
1627 src_dir = root / "src"
1630 root_pyproject = root / "pyproject.toml"
1631 root_pyproject.touch()
1632 src_pyproject = src_dir / "pyproject.toml"
1633 src_pyproject.touch()
1634 src_python = src_dir / "foo.py"
1638 black.find_project_root((src_dir, test_dir)),
1639 (root.resolve(), "pyproject.toml"),
1642 black.find_project_root((src_dir,)),
1643 (src_dir.resolve(), "pyproject.toml"),
1646 black.find_project_root((src_python,)),
1647 (src_dir.resolve(), "pyproject.toml"),
1650 with change_directory(test_dir):
1652 black.find_project_root(("-",), stdin_filename="../src/a.py"),
1653 (src_dir.resolve(), "pyproject.toml"),
1657 "black.files.find_user_pyproject_toml",
1659 def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
1660 find_user_pyproject_toml.side_effect = RuntimeError()
1662 with redirect_stderr(io.StringIO()) as stderr:
1663 result = black.files.find_pyproject_toml(
1664 path_search_start=(str(Path.cwd().root),)
1667 assert result is None
1668 err = stderr.getvalue()
1669 assert "Ignoring user configuration" in err
1672 "black.files.find_user_pyproject_toml",
1673 black.files.find_user_pyproject_toml.__wrapped__,
1675 def test_find_user_pyproject_toml_linux(self) -> None:
1676 if system() == "Windows":
1679 # Test if XDG_CONFIG_HOME is checked
1680 with TemporaryDirectory() as workspace:
1681 tmp_user_config = Path(workspace) / "black"
1682 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1684 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1687 # Test fallback for XDG_CONFIG_HOME
1688 with patch.dict("os.environ"):
1689 os.environ.pop("XDG_CONFIG_HOME", None)
1690 fallback_user_config = Path("~/.config").expanduser() / "black"
1692 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1695 def test_find_user_pyproject_toml_windows(self) -> None:
1696 if system() != "Windows":
1699 user_config_path = Path.home() / ".black"
1701 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1704 def test_bpo_33660_workaround(self) -> None:
1705 if system() == "Windows":
1708 # https://bugs.python.org/issue33660
1710 with change_directory(root):
1711 path = Path("workspace") / "project"
1712 report = black.Report(verbose=True)
1713 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1714 self.assertEqual(normalized_path, "workspace/project")
1716 def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
1717 if system() != "Windows":
1720 with TemporaryDirectory() as workspace:
1721 root = Path(workspace)
1722 junction_dir = root / "junction"
1723 junction_target_outside_of_root = root / ".."
1724 os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
1726 report = black.Report(verbose=True)
1727 normalized_path = black.normalize_path_maybe_ignore(
1728 junction_dir, root, report
1730 # Manually delete for Python < 3.8
1731 os.system(f"rmdir {junction_dir}")
1733 self.assertEqual(normalized_path, None)
1735 def test_newline_comment_interaction(self) -> None:
1736 source = "class A:\\\r\n# type: ignore\n pass\n"
1737 output = black.format_str(source, mode=DEFAULT_MODE)
1738 black.assert_stable(source, output, mode=DEFAULT_MODE)
1740 def test_bpo_2142_workaround(self) -> None:
1741 # https://bugs.python.org/issue2142
1743 source, _ = read_data("miscellaneous", "missing_final_newline")
1744 # read_data adds a trailing newline
1745 source = source.rstrip()
1746 expected, _ = read_data("miscellaneous", "missing_final_newline.diff")
1747 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1748 diff_header = re.compile(
1749 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1750 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
1753 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1754 self.assertEqual(result.exit_code, 0)
1757 actual = result.output
1758 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1759 self.assertEqual(actual, expected)
1762 def compare_results(
1763 result: click.testing.Result, expected_value: str, expected_exit_code: int
1765 """Helper method to test the value and exit code of a click Result."""
1767 result.output == expected_value
1768 ), "The output did not match the expected value."
1769 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1771 def test_code_option(self) -> None:
1772 """Test the code option with no changes."""
1773 code = 'print("Hello world")\n'
1774 args = ["--code", code]
1775 result = CliRunner().invoke(black.main, args)
1777 self.compare_results(result, code, 0)
1779 def test_code_option_changed(self) -> None:
1780 """Test the code option when changes are required."""
1781 code = "print('hello world')"
1782 formatted = black.format_str(code, mode=DEFAULT_MODE)
1784 args = ["--code", code]
1785 result = CliRunner().invoke(black.main, args)
1787 self.compare_results(result, formatted, 0)
1789 def test_code_option_check(self) -> None:
1790 """Test the code option when check is passed."""
1791 args = ["--check", "--code", 'print("Hello world")\n']
1792 result = CliRunner().invoke(black.main, args)
1793 self.compare_results(result, "", 0)
1795 def test_code_option_check_changed(self) -> None:
1796 """Test the code option when changes are required, and check is passed."""
1797 args = ["--check", "--code", "print('hello world')"]
1798 result = CliRunner().invoke(black.main, args)
1799 self.compare_results(result, "", 1)
1801 def test_code_option_diff(self) -> None:
1802 """Test the code option when diff is passed."""
1803 code = "print('hello world')"
1804 formatted = black.format_str(code, mode=DEFAULT_MODE)
1805 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1807 args = ["--diff", "--code", code]
1808 result = CliRunner().invoke(black.main, args)
1810 # Remove time from diff
1811 output = DIFF_TIME.sub("", result.output)
1813 assert output == result_diff, "The output did not match the expected value."
1814 assert result.exit_code == 0, "The exit code is incorrect."
1816 def test_code_option_color_diff(self) -> None:
1817 """Test the code option when color and diff are passed."""
1818 code = "print('hello world')"
1819 formatted = black.format_str(code, mode=DEFAULT_MODE)
1821 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1822 result_diff = color_diff(result_diff)
1824 args = ["--diff", "--color", "--code", code]
1825 result = CliRunner().invoke(black.main, args)
1827 # Remove time from diff
1828 output = DIFF_TIME.sub("", result.output)
1830 assert output == result_diff, "The output did not match the expected value."
1831 assert result.exit_code == 0, "The exit code is incorrect."
1833 @pytest.mark.incompatible_with_mypyc
1834 def test_code_option_safe(self) -> None:
1835 """Test that the code option throws an error when the sanity checks fail."""
1836 # Patch black.assert_equivalent to ensure the sanity checks fail
1837 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1838 code = 'print("Hello world")'
1839 error_msg = f"{code}\nerror: cannot format <string>: \n"
1841 args = ["--safe", "--code", code]
1842 result = CliRunner().invoke(black.main, args)
1844 self.compare_results(result, error_msg, 123)
1846 def test_code_option_fast(self) -> None:
1847 """Test that the code option ignores errors when the sanity checks fail."""
1848 # Patch black.assert_equivalent to ensure the sanity checks fail
1849 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1850 code = 'print("Hello world")'
1851 formatted = black.format_str(code, mode=DEFAULT_MODE)
1853 args = ["--fast", "--code", code]
1854 result = CliRunner().invoke(black.main, args)
1856 self.compare_results(result, formatted, 0)
1858 @pytest.mark.incompatible_with_mypyc
1859 def test_code_option_config(self) -> None:
1861 Test that the code option finds the pyproject.toml in the current directory.
1863 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1864 args = ["--code", "print"]
1865 # This is the only directory known to contain a pyproject.toml
1866 with change_directory(PROJECT_ROOT):
1867 CliRunner().invoke(black.main, args)
1868 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1871 len(parse.mock_calls) >= 1
1872 ), "Expected config parse to be called with the current directory."
1874 _, call_args, _ = parse.mock_calls[0]
1876 call_args[0].lower() == str(pyproject_path).lower()
1877 ), "Incorrect config loaded."
1879 @pytest.mark.incompatible_with_mypyc
1880 def test_code_option_parent_config(self) -> None:
1882 Test that the code option finds the pyproject.toml in the parent directory.
1884 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1885 with change_directory(THIS_DIR):
1886 args = ["--code", "print"]
1887 CliRunner().invoke(black.main, args)
1889 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1891 len(parse.mock_calls) >= 1
1892 ), "Expected config parse to be called with the current directory."
1894 _, call_args, _ = parse.mock_calls[0]
1896 call_args[0].lower() == str(pyproject_path).lower()
1897 ), "Incorrect config loaded."
1899 def test_for_handled_unexpected_eof_error(self) -> None:
1901 Test that an unexpected EOF SyntaxError is nicely presented.
1903 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1904 black.lib2to3_parse("print(", {})
1906 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1908 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1909 with pytest.raises(AssertionError) as err:
1910 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1913 # Unfortunately the SyntaxError message has changed in newer versions so we
1914 # can't match it directly.
1915 err.match("invalid character")
1916 err.match(r"\(<unknown>, line 1\)")
1920 def test_get_cache_dir(
1923 monkeypatch: pytest.MonkeyPatch,
1925 # Create multiple cache directories
1926 workspace1 = tmp_path / "ws1"
1928 workspace2 = tmp_path / "ws2"
1931 # Force user_cache_dir to use the temporary directory for easier assertions
1932 patch_user_cache_dir = patch(
1933 target="black.cache.user_cache_dir",
1935 return_value=str(workspace1),
1938 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1939 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1940 with patch_user_cache_dir:
1941 assert get_cache_dir() == workspace1
1943 # If it is set, use the path provided in the env var.
1944 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1945 assert get_cache_dir() == workspace2
1947 def test_cache_broken_file(self) -> None:
1949 with cache_dir() as workspace:
1950 cache_file = get_cache_file(mode)
1951 cache_file.write_text("this is not a pickle", encoding="utf-8")
1952 assert black.Cache.read(mode).file_data == {}
1953 src = (workspace / "test.py").resolve()
1954 src.write_text("print('hello')", encoding="utf-8")
1955 invokeBlack([str(src)])
1956 cache = black.Cache.read(mode)
1957 assert not cache.is_changed(src)
1959 def test_cache_single_file_already_cached(self) -> None:
1961 with cache_dir() as workspace:
1962 src = (workspace / "test.py").resolve()
1963 src.write_text("print('hello')", encoding="utf-8")
1964 cache = black.Cache.read(mode)
1966 invokeBlack([str(src)])
1967 assert src.read_text(encoding="utf-8") == "print('hello')"
1970 def test_cache_multiple_files(self) -> None:
1972 with cache_dir() as workspace, patch(
1973 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
1975 one = (workspace / "one.py").resolve()
1976 one.write_text("print('hello')", encoding="utf-8")
1977 two = (workspace / "two.py").resolve()
1978 two.write_text("print('hello')", encoding="utf-8")
1979 cache = black.Cache.read(mode)
1981 invokeBlack([str(workspace)])
1982 assert one.read_text(encoding="utf-8") == "print('hello')"
1983 assert two.read_text(encoding="utf-8") == 'print("hello")\n'
1984 cache = black.Cache.read(mode)
1985 assert not cache.is_changed(one)
1986 assert not cache.is_changed(two)
1988 @pytest.mark.incompatible_with_mypyc
1989 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1990 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1992 with cache_dir() as workspace:
1993 src = (workspace / "test.py").resolve()
1994 src.write_text("print('hello')", encoding="utf-8")
1995 with patch.object(black.Cache, "read") as read_cache, patch.object(
1996 black.Cache, "write"
1998 cmd = [str(src), "--diff"]
2000 cmd.append("--color")
2002 cache_file = get_cache_file(mode)
2003 assert cache_file.exists() is False
2004 read_cache.assert_called_once()
2005 write_cache.assert_not_called()
2007 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
2009 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
2010 with cache_dir() as workspace:
2011 for tag in range(0, 4):
2012 src = (workspace / f"test{tag}.py").resolve()
2013 src.write_text("print('hello')", encoding="utf-8")
2015 "black.concurrency.Manager", wraps=multiprocessing.Manager
2017 cmd = ["--diff", str(workspace)]
2019 cmd.append("--color")
2020 invokeBlack(cmd, exit_code=0)
2021 # this isn't quite doing what we want, but if it _isn't_
2022 # called then we cannot be using the lock it provides
2025 def test_no_cache_when_stdin(self) -> None:
2028 result = CliRunner().invoke(
2029 black.main, ["-"], input=BytesIO(b"print('hello')")
2031 assert not result.exit_code
2032 cache_file = get_cache_file(mode)
2033 assert not cache_file.exists()
2035 def test_read_cache_no_cachefile(self) -> None:
2038 assert black.Cache.read(mode).file_data == {}
2040 def test_write_cache_read_cache(self) -> None:
2042 with cache_dir() as workspace:
2043 src = (workspace / "test.py").resolve()
2045 write_cache = black.Cache.read(mode)
2046 write_cache.write([src])
2047 read_cache = black.Cache.read(mode)
2048 assert not read_cache.is_changed(src)
2050 @pytest.mark.incompatible_with_mypyc
2051 def test_filter_cached(self) -> None:
2052 with TemporaryDirectory() as workspace:
2053 path = Path(workspace)
2054 uncached = (path / "uncached").resolve()
2055 cached = (path / "cached").resolve()
2056 cached_but_changed = (path / "changed").resolve()
2059 cached_but_changed.touch()
2060 cache = black.Cache.read(DEFAULT_MODE)
2062 orig_func = black.Cache.get_file_data
2064 def wrapped_func(path: Path) -> FileData:
2066 return orig_func(path)
2067 if path == cached_but_changed:
2068 return FileData(0.0, 0, "")
2069 raise AssertionError
2071 with patch.object(black.Cache, "get_file_data", side_effect=wrapped_func):
2072 cache.write([cached, cached_but_changed])
2073 todo, done = cache.filtered_cached({uncached, cached, cached_but_changed})
2074 assert todo == {uncached, cached_but_changed}
2075 assert done == {cached}
2077 def test_filter_cached_hash(self) -> None:
2078 with TemporaryDirectory() as workspace:
2079 path = Path(workspace)
2080 src = (path / "test.py").resolve()
2081 src.write_text("print('hello')", encoding="utf-8")
2083 cache = black.Cache.read(DEFAULT_MODE)
2085 cached_file_data = cache.file_data[str(src)]
2087 todo, done = cache.filtered_cached([src])
2088 assert todo == set()
2089 assert done == {src}
2090 assert cached_file_data.st_mtime == st.st_mtime
2093 cached_file_data = cache.file_data[str(src)] = FileData(
2094 cached_file_data.st_mtime - 1,
2095 cached_file_data.st_size,
2096 cached_file_data.hash,
2098 todo, done = cache.filtered_cached([src])
2099 assert todo == set()
2100 assert done == {src}
2101 assert cached_file_data.st_mtime < st.st_mtime
2102 assert cached_file_data.st_size == st.st_size
2103 assert cached_file_data.hash == black.Cache.hash_digest(src)
2106 src.write_text("print('hello world')", encoding="utf-8")
2108 todo, done = cache.filtered_cached([src])
2109 assert todo == {src}
2110 assert done == set()
2111 assert cached_file_data.st_mtime < new_st.st_mtime
2112 assert cached_file_data.st_size != new_st.st_size
2113 assert cached_file_data.hash != black.Cache.hash_digest(src)
2115 def test_write_cache_creates_directory_if_needed(self) -> None:
2117 with cache_dir(exists=False) as workspace:
2118 assert not workspace.exists()
2119 cache = black.Cache.read(mode)
2121 assert workspace.exists()
2124 def test_failed_formatting_does_not_get_cached(self) -> None:
2126 with cache_dir() as workspace, patch(
2127 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
2129 failing = (workspace / "failing.py").resolve()
2130 failing.write_text("not actually python", encoding="utf-8")
2131 clean = (workspace / "clean.py").resolve()
2132 clean.write_text('print("hello")\n', encoding="utf-8")
2133 invokeBlack([str(workspace)], exit_code=123)
2134 cache = black.Cache.read(mode)
2135 assert cache.is_changed(failing)
2136 assert not cache.is_changed(clean)
2138 def test_write_cache_write_fail(self) -> None:
2141 cache = black.Cache.read(mode)
2142 with patch.object(Path, "open") as mock:
2143 mock.side_effect = OSError
2146 def test_read_cache_line_lengths(self) -> None:
2148 short_mode = replace(DEFAULT_MODE, line_length=1)
2149 with cache_dir() as workspace:
2150 path = (workspace / "file.py").resolve()
2152 cache = black.Cache.read(mode)
2154 one = black.Cache.read(mode)
2155 assert not one.is_changed(path)
2156 two = black.Cache.read(short_mode)
2157 assert two.is_changed(path)
2160 def assert_collected_sources(
2161 src: Sequence[Union[str, Path]],
2162 expected: Sequence[Union[str, Path]],
2164 root: Optional[Path] = None,
2165 exclude: Optional[str] = None,
2166 include: Optional[str] = None,
2167 extend_exclude: Optional[str] = None,
2168 force_exclude: Optional[str] = None,
2169 stdin_filename: Optional[str] = None,
2171 gs_src = tuple(str(Path(s)) for s in src)
2172 gs_expected = [Path(s) for s in expected]
2173 gs_exclude = None if exclude is None else compile_pattern(exclude)
2174 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
2175 gs_extend_exclude = (
2176 None if extend_exclude is None else compile_pattern(extend_exclude)
2178 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
2179 collected = black.get_sources(
2180 root=root or THIS_DIR,
2186 extend_exclude=gs_extend_exclude,
2187 force_exclude=gs_force_exclude,
2188 report=black.Report(),
2189 stdin_filename=stdin_filename,
2191 assert sorted(collected) == sorted(gs_expected)
2194 class TestFileCollection:
2195 def test_include_exclude(self) -> None:
2196 path = THIS_DIR / "data" / "include_exclude_tests"
2199 Path(path / "b/dont_exclude/a.py"),
2200 Path(path / "b/dont_exclude/a.pyi"),
2202 assert_collected_sources(
2206 exclude=r"/exclude/|/\.definitely_exclude/",
2209 def test_gitignore_used_as_default(self) -> None:
2210 base = Path(DATA_DIR / "include_exclude_tests")
2212 base / "b/.definitely_exclude/a.py",
2213 base / "b/.definitely_exclude/a.pyi",
2216 assert_collected_sources(src, expected, root=base, extend_exclude=r"/exclude/")
2218 def test_gitignore_used_on_multiple_sources(self) -> None:
2219 root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
2221 root / "dir1" / "b.py",
2222 root / "dir2" / "b.py",
2224 src = [root / "dir1", root / "dir2"]
2225 assert_collected_sources(src, expected, root=root)
2227 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2228 def test_exclude_for_issue_1572(self) -> None:
2229 # Exclude shouldn't touch files that were explicitly given to Black through the
2230 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
2231 # https://github.com/psf/black/issues/1572
2232 path = DATA_DIR / "include_exclude_tests"
2233 src = [path / "b/exclude/a.py"]
2234 expected = [path / "b/exclude/a.py"]
2235 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2237 def test_gitignore_exclude(self) -> None:
2238 path = THIS_DIR / "data" / "include_exclude_tests"
2239 include = re.compile(r"\.pyi?$")
2240 exclude = re.compile(r"")
2241 report = black.Report()
2242 gitignore = PathSpec.from_lines(
2243 "gitwildmatch", ["exclude/", ".definitely_exclude"]
2245 sources: List[Path] = []
2247 Path(path / "b/dont_exclude/a.py"),
2248 Path(path / "b/dont_exclude/a.pyi"),
2250 this_abs = THIS_DIR.resolve()
2252 black.gen_python_files(
2265 assert sorted(expected) == sorted(sources)
2267 def test_nested_gitignore(self) -> None:
2268 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
2269 include = re.compile(r"\.pyi?$")
2270 exclude = re.compile(r"")
2271 root_gitignore = black.files.get_gitignore(path)
2272 report = black.Report()
2273 expected: List[Path] = [
2274 Path(path / "x.py"),
2275 Path(path / "root/b.py"),
2276 Path(path / "root/c.py"),
2277 Path(path / "root/child/c.py"),
2279 this_abs = THIS_DIR.resolve()
2281 black.gen_python_files(
2289 {path: root_gitignore},
2294 assert sorted(expected) == sorted(sources)
2296 def test_nested_gitignore_directly_in_source_directory(self) -> None:
2297 # https://github.com/psf/black/issues/2598
2298 path = Path(DATA_DIR / "nested_gitignore_tests")
2299 src = Path(path / "root" / "child")
2300 expected = [src / "a.py", src / "c.py"]
2301 assert_collected_sources([src], expected)
2303 def test_invalid_gitignore(self) -> None:
2304 path = THIS_DIR / "data" / "invalid_gitignore_tests"
2305 empty_config = path / "pyproject.toml"
2306 result = BlackRunner().invoke(
2307 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2309 assert result.exit_code == 1
2310 assert result.stderr_bytes is not None
2312 gitignore = path / ".gitignore"
2313 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2315 def test_invalid_nested_gitignore(self) -> None:
2316 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
2317 empty_config = path / "pyproject.toml"
2318 result = BlackRunner().invoke(
2319 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2321 assert result.exit_code == 1
2322 assert result.stderr_bytes is not None
2324 gitignore = path / "a" / ".gitignore"
2325 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2327 def test_gitignore_that_ignores_subfolders(self) -> None:
2328 # If gitignore with */* is in root
2329 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir")
2330 expected = [root / "b.py"]
2331 assert_collected_sources([root], expected, root=root)
2333 # If .gitignore with */* is nested
2334 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2337 root / "subdir" / "b.py",
2339 assert_collected_sources([root], expected, root=root)
2341 # If command is executed from outer dir
2342 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2343 target = root / "subdir"
2344 expected = [target / "b.py"]
2345 assert_collected_sources([target], expected, root=root)
2347 def test_empty_include(self) -> None:
2348 path = DATA_DIR / "include_exclude_tests"
2351 Path(path / "b/exclude/a.pie"),
2352 Path(path / "b/exclude/a.py"),
2353 Path(path / "b/exclude/a.pyi"),
2354 Path(path / "b/dont_exclude/a.pie"),
2355 Path(path / "b/dont_exclude/a.py"),
2356 Path(path / "b/dont_exclude/a.pyi"),
2357 Path(path / "b/.definitely_exclude/a.pie"),
2358 Path(path / "b/.definitely_exclude/a.py"),
2359 Path(path / "b/.definitely_exclude/a.pyi"),
2360 Path(path / ".gitignore"),
2361 Path(path / "pyproject.toml"),
2363 # Setting exclude explicitly to an empty string to block .gitignore usage.
2364 assert_collected_sources(src, expected, include="", exclude="")
2366 def test_extend_exclude(self) -> None:
2367 path = DATA_DIR / "include_exclude_tests"
2370 Path(path / "b/exclude/a.py"),
2371 Path(path / "b/dont_exclude/a.py"),
2373 assert_collected_sources(
2374 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
2377 @pytest.mark.incompatible_with_mypyc
2378 def test_symlinks(self) -> None:
2380 root = THIS_DIR.resolve()
2381 include = re.compile(black.DEFAULT_INCLUDES)
2382 exclude = re.compile(black.DEFAULT_EXCLUDES)
2383 report = black.Report()
2384 gitignore = PathSpec.from_lines("gitwildmatch", [])
2386 regular = MagicMock()
2387 outside_root_symlink = MagicMock()
2388 ignored_symlink = MagicMock()
2390 path.iterdir.return_value = [regular, outside_root_symlink, ignored_symlink]
2392 regular.absolute.return_value = root / "regular.py"
2393 regular.resolve.return_value = root / "regular.py"
2394 regular.is_dir.return_value = False
2396 outside_root_symlink.absolute.return_value = root / "symlink.py"
2397 outside_root_symlink.resolve.return_value = Path("/nowhere")
2399 ignored_symlink.absolute.return_value = root / ".mypy_cache" / "symlink.py"
2402 black.gen_python_files(
2415 assert files == [regular]
2417 path.iterdir.assert_called_once()
2418 outside_root_symlink.resolve.assert_called_once()
2419 ignored_symlink.resolve.assert_not_called()
2421 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2422 def test_get_sources_with_stdin(self) -> None:
2425 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2427 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2428 def test_get_sources_with_stdin_filename(self) -> None:
2430 stdin_filename = str(THIS_DIR / "data/collections.py")
2431 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2432 assert_collected_sources(
2435 exclude=r"/exclude/a\.py",
2436 stdin_filename=stdin_filename,
2439 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2440 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2441 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2442 # file being passed directly. This is the same as
2443 # test_exclude_for_issue_1572
2444 path = DATA_DIR / "include_exclude_tests"
2446 stdin_filename = str(path / "b/exclude/a.py")
2447 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2448 assert_collected_sources(
2451 exclude=r"/exclude/|a\.py",
2452 stdin_filename=stdin_filename,
2455 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2456 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2457 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2458 # file being passed directly. This is the same as
2459 # test_exclude_for_issue_1572
2461 path = THIS_DIR / "data" / "include_exclude_tests"
2462 stdin_filename = str(path / "b/exclude/a.py")
2463 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2464 assert_collected_sources(
2467 extend_exclude=r"/exclude/|a\.py",
2468 stdin_filename=stdin_filename,
2471 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2472 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2473 # Force exclude should exclude the file when passing it through
2475 path = THIS_DIR / "data" / "include_exclude_tests"
2476 stdin_filename = str(path / "b/exclude/a.py")
2477 assert_collected_sources(
2480 force_exclude=r"/exclude/|a\.py",
2481 stdin_filename=stdin_filename,
2486 with open(black.__file__, "r", encoding="utf-8") as _bf:
2487 black_source_lines = _bf.readlines()
2488 except UnicodeDecodeError:
2489 if not black.COMPILED:
2494 frame: types.FrameType, event: str, arg: Any
2495 ) -> Callable[[types.FrameType, str, Any], Any]:
2496 """Show function calls `from black/__init__.py` as they happen.
2498 Register this with `sys.settrace()` in a test you're debugging.
2503 stack = len(inspect.stack()) - 19
2505 filename = frame.f_code.co_filename
2506 lineno = frame.f_lineno
2507 func_sig_lineno = lineno - 1
2508 funcname = black_source_lines[func_sig_lineno].strip()
2509 while funcname.startswith("@"):
2510 func_sig_lineno += 1
2511 funcname = black_source_lines[func_sig_lineno].strip()
2512 if "black/__init__.py" in filename:
2513 print(f"{' ' * stack}{lineno}:{funcname}")