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"
496 src.relative_to(working_directory) for src in target_abspath.iterdir()
499 def mock_n_calls(responses: List[bool]) -> Callable[[], bool]:
500 def _mocked_calls() -> bool:
502 return responses.pop(0)
507 with patch("pathlib.Path.iterdir", return_value=target_contents), patch(
508 "pathlib.Path.cwd", return_value=working_directory
509 ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])):
510 # Note that the root folder (project_root) isn't the folder
511 # named "root" (aka working_directory)
512 report = MagicMock(verbose=True)
518 include=DEFAULT_INCLUDE,
526 mock_args[1].startswith("is a symbolic link that points outside")
527 for _, mock_args, _ in report.path_ignored.mock_calls
528 ), "A symbolic link was reported."
529 report.path_ignored.assert_called_once_with(
530 Path("root", "child", "b.py"), "matches a .gitignore file content"
533 def test_report_verbose(self) -> None:
534 report = Report(verbose=True)
538 def out(msg: str, **kwargs: Any) -> None:
539 out_lines.append(msg)
541 def err(msg: str, **kwargs: Any) -> None:
542 err_lines.append(msg)
544 with patch("black.output._out", out), patch("black.output._err", err):
545 report.done(Path("f1"), black.Changed.NO)
546 self.assertEqual(len(out_lines), 1)
547 self.assertEqual(len(err_lines), 0)
548 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
549 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
550 self.assertEqual(report.return_code, 0)
551 report.done(Path("f2"), black.Changed.YES)
552 self.assertEqual(len(out_lines), 2)
553 self.assertEqual(len(err_lines), 0)
554 self.assertEqual(out_lines[-1], "reformatted f2")
556 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
558 report.done(Path("f3"), black.Changed.CACHED)
559 self.assertEqual(len(out_lines), 3)
560 self.assertEqual(len(err_lines), 0)
562 out_lines[-1], "f3 wasn't modified on disk since last run."
565 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
567 self.assertEqual(report.return_code, 0)
569 self.assertEqual(report.return_code, 1)
571 report.failed(Path("e1"), "boom")
572 self.assertEqual(len(out_lines), 3)
573 self.assertEqual(len(err_lines), 1)
574 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
576 unstyle(str(report)),
577 "1 file reformatted, 2 files left unchanged, 1 file failed to"
580 self.assertEqual(report.return_code, 123)
581 report.done(Path("f3"), black.Changed.YES)
582 self.assertEqual(len(out_lines), 4)
583 self.assertEqual(len(err_lines), 1)
584 self.assertEqual(out_lines[-1], "reformatted f3")
586 unstyle(str(report)),
587 "2 files reformatted, 2 files left unchanged, 1 file failed to"
590 self.assertEqual(report.return_code, 123)
591 report.failed(Path("e2"), "boom")
592 self.assertEqual(len(out_lines), 4)
593 self.assertEqual(len(err_lines), 2)
594 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
596 unstyle(str(report)),
597 "2 files reformatted, 2 files left unchanged, 2 files failed to"
600 self.assertEqual(report.return_code, 123)
601 report.path_ignored(Path("wat"), "no match")
602 self.assertEqual(len(out_lines), 5)
603 self.assertEqual(len(err_lines), 2)
604 self.assertEqual(out_lines[-1], "wat ignored: no match")
606 unstyle(str(report)),
607 "2 files reformatted, 2 files left unchanged, 2 files failed to"
610 self.assertEqual(report.return_code, 123)
611 report.done(Path("f4"), black.Changed.NO)
612 self.assertEqual(len(out_lines), 6)
613 self.assertEqual(len(err_lines), 2)
614 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
616 unstyle(str(report)),
617 "2 files reformatted, 3 files left unchanged, 2 files failed to"
620 self.assertEqual(report.return_code, 123)
623 unstyle(str(report)),
624 "2 files would be reformatted, 3 files would be left unchanged, 2"
625 " files would fail to reformat.",
630 unstyle(str(report)),
631 "2 files would be reformatted, 3 files would be left unchanged, 2"
632 " files would fail to reformat.",
635 def test_report_quiet(self) -> None:
636 report = Report(quiet=True)
640 def out(msg: str, **kwargs: Any) -> None:
641 out_lines.append(msg)
643 def err(msg: str, **kwargs: Any) -> None:
644 err_lines.append(msg)
646 with patch("black.output._out", out), patch("black.output._err", err):
647 report.done(Path("f1"), black.Changed.NO)
648 self.assertEqual(len(out_lines), 0)
649 self.assertEqual(len(err_lines), 0)
650 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
651 self.assertEqual(report.return_code, 0)
652 report.done(Path("f2"), black.Changed.YES)
653 self.assertEqual(len(out_lines), 0)
654 self.assertEqual(len(err_lines), 0)
656 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
658 report.done(Path("f3"), black.Changed.CACHED)
659 self.assertEqual(len(out_lines), 0)
660 self.assertEqual(len(err_lines), 0)
662 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
664 self.assertEqual(report.return_code, 0)
666 self.assertEqual(report.return_code, 1)
668 report.failed(Path("e1"), "boom")
669 self.assertEqual(len(out_lines), 0)
670 self.assertEqual(len(err_lines), 1)
671 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
673 unstyle(str(report)),
674 "1 file reformatted, 2 files left unchanged, 1 file failed to"
677 self.assertEqual(report.return_code, 123)
678 report.done(Path("f3"), black.Changed.YES)
679 self.assertEqual(len(out_lines), 0)
680 self.assertEqual(len(err_lines), 1)
682 unstyle(str(report)),
683 "2 files reformatted, 2 files left unchanged, 1 file failed to"
686 self.assertEqual(report.return_code, 123)
687 report.failed(Path("e2"), "boom")
688 self.assertEqual(len(out_lines), 0)
689 self.assertEqual(len(err_lines), 2)
690 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
692 unstyle(str(report)),
693 "2 files reformatted, 2 files left unchanged, 2 files failed to"
696 self.assertEqual(report.return_code, 123)
697 report.path_ignored(Path("wat"), "no match")
698 self.assertEqual(len(out_lines), 0)
699 self.assertEqual(len(err_lines), 2)
701 unstyle(str(report)),
702 "2 files reformatted, 2 files left unchanged, 2 files failed to"
705 self.assertEqual(report.return_code, 123)
706 report.done(Path("f4"), black.Changed.NO)
707 self.assertEqual(len(out_lines), 0)
708 self.assertEqual(len(err_lines), 2)
710 unstyle(str(report)),
711 "2 files reformatted, 3 files left unchanged, 2 files failed to"
714 self.assertEqual(report.return_code, 123)
717 unstyle(str(report)),
718 "2 files would be reformatted, 3 files would be left unchanged, 2"
719 " files would fail to reformat.",
724 unstyle(str(report)),
725 "2 files would be reformatted, 3 files would be left unchanged, 2"
726 " files would fail to reformat.",
729 def test_report_normal(self) -> None:
730 report = black.Report()
734 def out(msg: str, **kwargs: Any) -> None:
735 out_lines.append(msg)
737 def err(msg: str, **kwargs: Any) -> None:
738 err_lines.append(msg)
740 with patch("black.output._out", out), patch("black.output._err", err):
741 report.done(Path("f1"), black.Changed.NO)
742 self.assertEqual(len(out_lines), 0)
743 self.assertEqual(len(err_lines), 0)
744 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
745 self.assertEqual(report.return_code, 0)
746 report.done(Path("f2"), black.Changed.YES)
747 self.assertEqual(len(out_lines), 1)
748 self.assertEqual(len(err_lines), 0)
749 self.assertEqual(out_lines[-1], "reformatted f2")
751 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
753 report.done(Path("f3"), black.Changed.CACHED)
754 self.assertEqual(len(out_lines), 1)
755 self.assertEqual(len(err_lines), 0)
756 self.assertEqual(out_lines[-1], "reformatted f2")
758 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
760 self.assertEqual(report.return_code, 0)
762 self.assertEqual(report.return_code, 1)
764 report.failed(Path("e1"), "boom")
765 self.assertEqual(len(out_lines), 1)
766 self.assertEqual(len(err_lines), 1)
767 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
769 unstyle(str(report)),
770 "1 file reformatted, 2 files left unchanged, 1 file failed to"
773 self.assertEqual(report.return_code, 123)
774 report.done(Path("f3"), black.Changed.YES)
775 self.assertEqual(len(out_lines), 2)
776 self.assertEqual(len(err_lines), 1)
777 self.assertEqual(out_lines[-1], "reformatted f3")
779 unstyle(str(report)),
780 "2 files reformatted, 2 files left unchanged, 1 file failed to"
783 self.assertEqual(report.return_code, 123)
784 report.failed(Path("e2"), "boom")
785 self.assertEqual(len(out_lines), 2)
786 self.assertEqual(len(err_lines), 2)
787 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
789 unstyle(str(report)),
790 "2 files reformatted, 2 files left unchanged, 2 files failed to"
793 self.assertEqual(report.return_code, 123)
794 report.path_ignored(Path("wat"), "no match")
795 self.assertEqual(len(out_lines), 2)
796 self.assertEqual(len(err_lines), 2)
798 unstyle(str(report)),
799 "2 files reformatted, 2 files left unchanged, 2 files failed to"
802 self.assertEqual(report.return_code, 123)
803 report.done(Path("f4"), black.Changed.NO)
804 self.assertEqual(len(out_lines), 2)
805 self.assertEqual(len(err_lines), 2)
807 unstyle(str(report)),
808 "2 files reformatted, 3 files left unchanged, 2 files failed to"
811 self.assertEqual(report.return_code, 123)
814 unstyle(str(report)),
815 "2 files would be reformatted, 3 files would be left unchanged, 2"
816 " files would fail to reformat.",
821 unstyle(str(report)),
822 "2 files would be reformatted, 3 files would be left unchanged, 2"
823 " files would fail to reformat.",
826 def test_lib2to3_parse(self) -> None:
827 with self.assertRaises(black.InvalidInput):
828 black.lib2to3_parse("invalid syntax")
831 black.lib2to3_parse(straddling)
832 black.lib2to3_parse(straddling, {TargetVersion.PY36})
835 with self.assertRaises(black.InvalidInput):
836 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
838 py3_only = "exec(x, end=y)"
839 black.lib2to3_parse(py3_only)
840 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
842 def test_get_features_used_decorator(self) -> None:
843 # Test the feature detection of new decorator syntax
844 # since this makes some test cases of test_get_features_used()
845 # fails if it fails, this is tested first so that a useful case
847 simples, relaxed = read_data("miscellaneous", "decorators")
848 # skip explanation comments at the top of the file
849 for simple_test in simples.split("##")[1:]:
850 node = black.lib2to3_parse(simple_test)
851 decorator = str(node.children[0].children[0]).strip()
853 Feature.RELAXED_DECORATORS,
854 black.get_features_used(node),
856 f"decorator '{decorator}' follows python<=3.8 syntax"
857 "but is detected as 3.9+"
858 # f"The full node is\n{node!r}"
861 # skip the '# output' comment at the top of the output part
862 for relaxed_test in relaxed.split("##")[1:]:
863 node = black.lib2to3_parse(relaxed_test)
864 decorator = str(node.children[0].children[0]).strip()
866 Feature.RELAXED_DECORATORS,
867 black.get_features_used(node),
869 f"decorator '{decorator}' uses python3.9+ syntax"
870 "but is detected as python<=3.8"
871 # f"The full node is\n{node!r}"
875 def test_get_features_used(self) -> None:
876 node = black.lib2to3_parse("def f(*, arg): ...\n")
877 self.assertEqual(black.get_features_used(node), set())
878 node = black.lib2to3_parse("def f(*, arg,): ...\n")
879 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
880 node = black.lib2to3_parse("f(*arg,)\n")
882 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
884 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
885 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
886 node = black.lib2to3_parse("123_456\n")
887 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
888 node = black.lib2to3_parse("123456\n")
889 self.assertEqual(black.get_features_used(node), set())
890 source, expected = read_data("simple_cases", "function")
891 node = black.lib2to3_parse(source)
892 expected_features = {
893 Feature.TRAILING_COMMA_IN_CALL,
894 Feature.TRAILING_COMMA_IN_DEF,
897 self.assertEqual(black.get_features_used(node), expected_features)
898 node = black.lib2to3_parse(expected)
899 self.assertEqual(black.get_features_used(node), expected_features)
900 source, expected = read_data("simple_cases", "expression")
901 node = black.lib2to3_parse(source)
902 self.assertEqual(black.get_features_used(node), set())
903 node = black.lib2to3_parse(expected)
904 self.assertEqual(black.get_features_used(node), set())
905 node = black.lib2to3_parse("lambda a, /, b: ...")
906 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
907 node = black.lib2to3_parse("def fn(a, /, b): ...")
908 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
909 node = black.lib2to3_parse("def fn(): yield a, b")
910 self.assertEqual(black.get_features_used(node), set())
911 node = black.lib2to3_parse("def fn(): return a, b")
912 self.assertEqual(black.get_features_used(node), set())
913 node = black.lib2to3_parse("def fn(): yield *b, c")
914 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
915 node = black.lib2to3_parse("def fn(): return a, *b, c")
916 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
917 node = black.lib2to3_parse("x = a, *b, c")
918 self.assertEqual(black.get_features_used(node), set())
919 node = black.lib2to3_parse("x: Any = regular")
920 self.assertEqual(black.get_features_used(node), set())
921 node = black.lib2to3_parse("x: Any = (regular, regular)")
922 self.assertEqual(black.get_features_used(node), set())
923 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
924 self.assertEqual(black.get_features_used(node), set())
925 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
927 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
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 (*Something,): pass")
932 self.assertEqual(black.get_features_used(node), set())
933 node = black.lib2to3_parse("try: pass\nexcept *Group: pass")
934 self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR})
935 node = black.lib2to3_parse("a[*b]")
936 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
937 node = black.lib2to3_parse("a[x, *y(), z] = t")
938 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
939 node = black.lib2to3_parse("def fn(*args: *T): pass")
940 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
942 def test_get_features_used_for_future_flags(self) -> None:
943 for src, features in [
944 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
946 "from __future__ import (other, annotations)",
947 {Feature.FUTURE_ANNOTATIONS},
949 ("a = 1 + 2\nfrom something import annotations", set()),
950 ("from __future__ import x, y", set()),
952 with self.subTest(src=src, features=features):
953 node = black.lib2to3_parse(src)
954 future_imports = black.get_future_imports(node)
956 black.get_features_used(node, future_imports=future_imports),
960 def test_get_future_imports(self) -> None:
961 node = black.lib2to3_parse("\n")
962 self.assertEqual(set(), black.get_future_imports(node))
963 node = black.lib2to3_parse("from __future__ import black\n")
964 self.assertEqual({"black"}, black.get_future_imports(node))
965 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
966 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
967 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
968 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
969 node = black.lib2to3_parse(
970 "from __future__ import multiple\nfrom __future__ import imports\n"
972 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
973 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
974 self.assertEqual({"black"}, black.get_future_imports(node))
975 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
976 self.assertEqual({"black"}, black.get_future_imports(node))
977 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
978 self.assertEqual(set(), black.get_future_imports(node))
979 node = black.lib2to3_parse("from some.module import black\n")
980 self.assertEqual(set(), black.get_future_imports(node))
981 node = black.lib2to3_parse(
982 "from __future__ import unicode_literals as _unicode_literals"
984 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
985 node = black.lib2to3_parse(
986 "from __future__ import unicode_literals as _lol, print"
988 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
990 @pytest.mark.incompatible_with_mypyc
991 def test_debug_visitor(self) -> None:
992 source, _ = read_data("miscellaneous", "debug_visitor")
993 expected, _ = read_data("miscellaneous", "debug_visitor.out")
997 def out(msg: str, **kwargs: Any) -> None:
998 out_lines.append(msg)
1000 def err(msg: str, **kwargs: Any) -> None:
1001 err_lines.append(msg)
1003 with patch("black.debug.out", out):
1004 DebugVisitor.show(source)
1005 actual = "\n".join(out_lines) + "\n"
1007 if expected != actual:
1008 log_name = black.dump_to_file(*out_lines)
1012 f"AST print out is different. Actual version dumped to {log_name}",
1015 def test_format_file_contents(self) -> None:
1018 with self.assertRaises(black.NothingChanged):
1019 black.format_file_contents(empty, mode=mode, fast=False)
1021 with self.assertRaises(black.NothingChanged):
1022 black.format_file_contents(just_nl, mode=mode, fast=False)
1023 same = "j = [1, 2, 3]\n"
1024 with self.assertRaises(black.NothingChanged):
1025 black.format_file_contents(same, mode=mode, fast=False)
1026 different = "j = [1,2,3]"
1028 actual = black.format_file_contents(different, mode=mode, fast=False)
1029 self.assertEqual(expected, actual)
1030 invalid = "return if you can"
1031 with self.assertRaises(black.InvalidInput) as e:
1032 black.format_file_contents(invalid, mode=mode, fast=False)
1033 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
1035 mode = black.Mode(preview=True)
1037 with self.assertRaises(black.NothingChanged):
1038 black.format_file_contents(just_crlf, mode=mode, fast=False)
1039 just_whitespace_nl = "\n\t\n \n\t \n \t\n\n"
1040 actual = black.format_file_contents(just_whitespace_nl, mode=mode, fast=False)
1041 self.assertEqual("\n", actual)
1042 just_whitespace_crlf = "\r\n\t\r\n \r\n\t \r\n \t\r\n\r\n"
1043 actual = black.format_file_contents(just_whitespace_crlf, mode=mode, fast=False)
1044 self.assertEqual("\r\n", actual)
1046 def test_endmarker(self) -> None:
1047 n = black.lib2to3_parse("\n")
1048 self.assertEqual(n.type, black.syms.file_input)
1049 self.assertEqual(len(n.children), 1)
1050 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
1052 @pytest.mark.incompatible_with_mypyc
1053 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
1054 def test_assertFormatEqual(self) -> None:
1058 def out(msg: str, **kwargs: Any) -> None:
1059 out_lines.append(msg)
1061 def err(msg: str, **kwargs: Any) -> None:
1062 err_lines.append(msg)
1064 with patch("black.output._out", out), patch("black.output._err", err):
1065 with self.assertRaises(AssertionError):
1066 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
1068 out_str = "".join(out_lines)
1069 self.assertIn("Expected tree:", out_str)
1070 self.assertIn("Actual tree:", out_str)
1071 self.assertEqual("".join(err_lines), "")
1074 @patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1075 def test_works_in_mono_process_only_environment(self) -> None:
1076 with cache_dir() as workspace:
1078 (workspace / "one.py").resolve(),
1079 (workspace / "two.py").resolve(),
1081 f.write_text('print("hello")\n', encoding="utf-8")
1082 self.invokeBlack([str(workspace)])
1085 def test_check_diff_use_together(self) -> None:
1087 # Files which will be reformatted.
1088 src1 = get_case_path("miscellaneous", "string_quotes")
1089 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1090 # Files which will not be reformatted.
1091 src2 = get_case_path("simple_cases", "composition")
1092 self.invokeBlack([str(src2), "--diff", "--check"])
1093 # Multi file command.
1094 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1096 def test_no_src_fails(self) -> None:
1098 self.invokeBlack([], exit_code=1)
1100 def test_src_and_code_fails(self) -> None:
1102 self.invokeBlack([".", "-c", "0"], exit_code=1)
1104 def test_broken_symlink(self) -> None:
1105 with cache_dir() as workspace:
1106 symlink = workspace / "broken_link.py"
1108 symlink.symlink_to("nonexistent.py")
1109 except (OSError, NotImplementedError) as e:
1110 self.skipTest(f"Can't create symlinks: {e}")
1111 self.invokeBlack([str(workspace.resolve())])
1113 def test_single_file_force_pyi(self) -> None:
1114 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1115 contents, expected = read_data("miscellaneous", "force_pyi")
1116 with cache_dir() as workspace:
1117 path = (workspace / "file.py").resolve()
1118 path.write_text(contents, encoding="utf-8")
1119 self.invokeBlack([str(path), "--pyi"])
1120 actual = path.read_text(encoding="utf-8")
1121 # verify cache with --pyi is separate
1122 pyi_cache = black.Cache.read(pyi_mode)
1123 assert not pyi_cache.is_changed(path)
1124 normal_cache = black.Cache.read(DEFAULT_MODE)
1125 assert normal_cache.is_changed(path)
1126 self.assertFormatEqual(expected, actual)
1127 black.assert_equivalent(contents, actual)
1128 black.assert_stable(contents, actual, pyi_mode)
1131 def test_multi_file_force_pyi(self) -> None:
1132 reg_mode = DEFAULT_MODE
1133 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1134 contents, expected = read_data("miscellaneous", "force_pyi")
1135 with cache_dir() as workspace:
1137 (workspace / "file1.py").resolve(),
1138 (workspace / "file2.py").resolve(),
1141 path.write_text(contents, encoding="utf-8")
1142 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1144 actual = path.read_text(encoding="utf-8")
1145 self.assertEqual(actual, expected)
1146 # verify cache with --pyi is separate
1147 pyi_cache = black.Cache.read(pyi_mode)
1148 normal_cache = black.Cache.read(reg_mode)
1150 assert not pyi_cache.is_changed(path)
1151 assert normal_cache.is_changed(path)
1153 def test_pipe_force_pyi(self) -> None:
1154 source, expected = read_data("miscellaneous", "force_pyi")
1155 result = CliRunner().invoke(
1156 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf-8"))
1158 self.assertEqual(result.exit_code, 0)
1159 actual = result.output
1160 self.assertFormatEqual(actual, expected)
1162 def test_single_file_force_py36(self) -> None:
1163 reg_mode = DEFAULT_MODE
1164 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1165 source, expected = read_data("miscellaneous", "force_py36")
1166 with cache_dir() as workspace:
1167 path = (workspace / "file.py").resolve()
1168 path.write_text(source, encoding="utf-8")
1169 self.invokeBlack([str(path), *PY36_ARGS])
1170 actual = path.read_text(encoding="utf-8")
1171 # verify cache with --target-version is separate
1172 py36_cache = black.Cache.read(py36_mode)
1173 assert not py36_cache.is_changed(path)
1174 normal_cache = black.Cache.read(reg_mode)
1175 assert normal_cache.is_changed(path)
1176 self.assertEqual(actual, expected)
1179 def test_multi_file_force_py36(self) -> None:
1180 reg_mode = DEFAULT_MODE
1181 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1182 source, expected = read_data("miscellaneous", "force_py36")
1183 with cache_dir() as workspace:
1185 (workspace / "file1.py").resolve(),
1186 (workspace / "file2.py").resolve(),
1189 path.write_text(source, encoding="utf-8")
1190 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1192 actual = path.read_text(encoding="utf-8")
1193 self.assertEqual(actual, expected)
1194 # verify cache with --target-version is separate
1195 pyi_cache = black.Cache.read(py36_mode)
1196 normal_cache = black.Cache.read(reg_mode)
1198 assert not pyi_cache.is_changed(path)
1199 assert normal_cache.is_changed(path)
1201 def test_pipe_force_py36(self) -> None:
1202 source, expected = read_data("miscellaneous", "force_py36")
1203 result = CliRunner().invoke(
1205 ["-", "-q", "--target-version=py36"],
1206 input=BytesIO(source.encode("utf-8")),
1208 self.assertEqual(result.exit_code, 0)
1209 actual = result.output
1210 self.assertFormatEqual(actual, expected)
1212 @pytest.mark.incompatible_with_mypyc
1213 def test_reformat_one_with_stdin(self) -> None:
1215 "black.format_stdin_to_stdout",
1216 return_value=lambda *args, **kwargs: black.Changed.YES,
1218 report = MagicMock()
1223 write_back=black.WriteBack.YES,
1227 fsts.assert_called_once()
1228 report.done.assert_called_with(path, black.Changed.YES)
1230 @pytest.mark.incompatible_with_mypyc
1231 def test_reformat_one_with_stdin_filename(self) -> None:
1233 "black.format_stdin_to_stdout",
1234 return_value=lambda *args, **kwargs: black.Changed.YES,
1236 report = MagicMock()
1238 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1243 write_back=black.WriteBack.YES,
1247 fsts.assert_called_once_with(
1248 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1250 # __BLACK_STDIN_FILENAME__ should have been stripped
1251 report.done.assert_called_with(expected, black.Changed.YES)
1253 @pytest.mark.incompatible_with_mypyc
1254 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1256 "black.format_stdin_to_stdout",
1257 return_value=lambda *args, **kwargs: black.Changed.YES,
1259 report = MagicMock()
1261 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1266 write_back=black.WriteBack.YES,
1270 fsts.assert_called_once_with(
1272 write_back=black.WriteBack.YES,
1273 mode=replace(DEFAULT_MODE, is_pyi=True),
1275 # __BLACK_STDIN_FILENAME__ should have been stripped
1276 report.done.assert_called_with(expected, black.Changed.YES)
1278 @pytest.mark.incompatible_with_mypyc
1279 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1281 "black.format_stdin_to_stdout",
1282 return_value=lambda *args, **kwargs: black.Changed.YES,
1284 report = MagicMock()
1286 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1291 write_back=black.WriteBack.YES,
1295 fsts.assert_called_once_with(
1297 write_back=black.WriteBack.YES,
1298 mode=replace(DEFAULT_MODE, is_ipynb=True),
1300 # __BLACK_STDIN_FILENAME__ should have been stripped
1301 report.done.assert_called_with(expected, black.Changed.YES)
1303 @pytest.mark.incompatible_with_mypyc
1304 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1306 "black.format_stdin_to_stdout",
1307 return_value=lambda *args, **kwargs: black.Changed.YES,
1309 report = MagicMock()
1310 # Even with an existing file, since we are forcing stdin, black
1311 # should output to stdout and not modify the file inplace
1312 p = THIS_DIR / "data" / "simple_cases" / "collections.py"
1313 # Make sure is_file actually returns True
1314 self.assertTrue(p.is_file())
1315 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1320 write_back=black.WriteBack.YES,
1324 fsts.assert_called_once()
1325 # __BLACK_STDIN_FILENAME__ should have been stripped
1326 report.done.assert_called_with(expected, black.Changed.YES)
1328 def test_reformat_one_with_stdin_empty(self) -> None:
1335 (" \t\r\n\t ", "\r\n"),
1339 output: io.StringIO, io_TextIOWrapper: Type[io.TextIOWrapper]
1340 ) -> Callable[[Any, Any], io.TextIOWrapper]:
1341 def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper:
1342 if args == (sys.stdout.buffer,):
1343 # It's `format_stdin_to_stdout()` calling `io.TextIOWrapper()`,
1344 # return our mock object.
1346 # It's something else (i.e. `decode_bytes()`) calling
1347 # `io.TextIOWrapper()`, pass through to the original implementation.
1348 # See discussion in https://github.com/psf/black/pull/2489
1349 return io_TextIOWrapper(*args, **kwargs)
1353 mode = black.Mode(preview=True)
1354 for content, expected in cases:
1355 output = io.StringIO()
1356 io_TextIOWrapper = io.TextIOWrapper
1358 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1360 black.format_stdin_to_stdout(
1363 write_back=black.WriteBack.YES,
1366 except io.UnsupportedOperation:
1367 pass # StringIO does not support detach
1368 assert output.getvalue() == expected
1370 # An empty string is the only test case for `preview=False`
1371 output = io.StringIO()
1372 io_TextIOWrapper = io.TextIOWrapper
1373 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1375 black.format_stdin_to_stdout(
1378 write_back=black.WriteBack.YES,
1381 except io.UnsupportedOperation:
1382 pass # StringIO does not support detach
1383 assert output.getvalue() == ""
1385 def test_invalid_cli_regex(self) -> None:
1386 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1387 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1389 def test_required_version_matches_version(self) -> None:
1391 ["--required-version", black.__version__, "-c", "0"],
1396 def test_required_version_matches_partial_version(self) -> None:
1398 ["--required-version", black.__version__.split(".")[0], "-c", "0"],
1403 def test_required_version_does_not_match_on_minor_version(self) -> None:
1405 ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
1410 def test_required_version_does_not_match_version(self) -> None:
1411 result = BlackRunner().invoke(
1413 ["--required-version", "20.99b", "-c", "0"],
1415 self.assertEqual(result.exit_code, 1)
1416 self.assertIn("required version", result.stderr)
1418 def test_preserves_line_endings(self) -> None:
1419 with TemporaryDirectory() as workspace:
1420 test_file = Path(workspace) / "test.py"
1421 for nl in ["\n", "\r\n"]:
1422 contents = nl.join(["def f( ):", " pass"])
1423 test_file.write_bytes(contents.encode())
1424 ff(test_file, write_back=black.WriteBack.YES)
1425 updated_contents: bytes = test_file.read_bytes()
1426 self.assertIn(nl.encode(), updated_contents)
1428 self.assertNotIn(b"\r\n", updated_contents)
1430 def test_preserves_line_endings_via_stdin(self) -> None:
1431 for nl in ["\n", "\r\n"]:
1432 contents = nl.join(["def f( ):", " pass"])
1433 runner = BlackRunner()
1434 result = runner.invoke(
1435 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf-8"))
1437 self.assertEqual(result.exit_code, 0)
1438 output = result.stdout_bytes
1439 self.assertIn(nl.encode("utf-8"), output)
1441 self.assertNotIn(b"\r\n", output)
1443 def test_normalize_line_endings(self) -> None:
1444 with TemporaryDirectory() as workspace:
1445 test_file = Path(workspace) / "test.py"
1446 for data, expected in (
1447 (b"c\r\nc\n ", b"c\r\nc\r\n"),
1448 (b"l\nl\r\n ", b"l\nl\n"),
1450 test_file.write_bytes(data)
1451 ff(test_file, write_back=black.WriteBack.YES)
1452 self.assertEqual(test_file.read_bytes(), expected)
1454 def test_assert_equivalent_different_asts(self) -> None:
1455 with self.assertRaises(AssertionError):
1456 black.assert_equivalent("{}", "None")
1458 def test_root_logger_not_used_directly(self) -> None:
1459 def fail(*args: Any, **kwargs: Any) -> None:
1460 self.fail("Record created with root logger")
1462 with patch.multiple(
1471 ff(THIS_DIR / "util.py")
1473 def test_invalid_config_return_code(self) -> None:
1474 tmp_file = Path(black.dump_to_file())
1476 tmp_config = Path(black.dump_to_file())
1478 args = ["--config", str(tmp_config), str(tmp_file)]
1479 self.invokeBlack(args, exit_code=2, ignore_config=False)
1483 def test_parse_pyproject_toml(self) -> None:
1484 test_toml_file = THIS_DIR / "test.toml"
1485 config = black.parse_pyproject_toml(str(test_toml_file))
1486 self.assertEqual(config["verbose"], 1)
1487 self.assertEqual(config["check"], "no")
1488 self.assertEqual(config["diff"], "y")
1489 self.assertEqual(config["color"], True)
1490 self.assertEqual(config["line_length"], 79)
1491 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1492 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1493 self.assertEqual(config["exclude"], r"\.pyi?$")
1494 self.assertEqual(config["include"], r"\.py?$")
1496 def test_parse_pyproject_toml_project_metadata(self) -> None:
1497 for test_toml, expected in [
1498 ("only_black_pyproject.toml", ["py310"]),
1499 ("only_metadata_pyproject.toml", ["py37", "py38", "py39", "py310"]),
1500 ("neither_pyproject.toml", None),
1501 ("both_pyproject.toml", ["py310"]),
1503 test_toml_file = THIS_DIR / "data" / "project_metadata" / test_toml
1504 config = black.parse_pyproject_toml(str(test_toml_file))
1505 self.assertEqual(config.get("target_version"), expected)
1507 def test_infer_target_version(self) -> None:
1508 for version, expected in [
1509 ("3.6", [TargetVersion.PY36]),
1510 ("3.11.0rc1", [TargetVersion.PY311]),
1511 (">=3.10", [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312]),
1514 [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
1516 ("<3.6", [TargetVersion.PY33, TargetVersion.PY34, TargetVersion.PY35]),
1517 (">3.7,<3.10", [TargetVersion.PY38, TargetVersion.PY39]),
1520 [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
1523 "> 3.9.4, != 3.10.3",
1526 TargetVersion.PY310,
1527 TargetVersion.PY311,
1528 TargetVersion.PY312,
1539 TargetVersion.PY310,
1540 TargetVersion.PY311,
1541 TargetVersion.PY312,
1554 TargetVersion.PY310,
1555 TargetVersion.PY311,
1556 TargetVersion.PY312,
1559 ("==3.8.*", [TargetVersion.PY38]),
1563 ("==invalid", None),
1564 (">3.9,!=invalid", None),
1569 (">3.10,<3.11", None),
1571 test_toml = {"project": {"requires-python": version}}
1572 result = black.files.infer_target_version(test_toml)
1573 self.assertEqual(result, expected)
1575 def test_read_pyproject_toml(self) -> None:
1576 test_toml_file = THIS_DIR / "test.toml"
1577 fake_ctx = FakeContext()
1578 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1579 config = fake_ctx.default_map
1580 self.assertEqual(config["verbose"], "1")
1581 self.assertEqual(config["check"], "no")
1582 self.assertEqual(config["diff"], "y")
1583 self.assertEqual(config["color"], "True")
1584 self.assertEqual(config["line_length"], "79")
1585 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1586 self.assertEqual(config["exclude"], r"\.pyi?$")
1587 self.assertEqual(config["include"], r"\.py?$")
1589 def test_read_pyproject_toml_from_stdin(self) -> None:
1590 with TemporaryDirectory() as workspace:
1591 root = Path(workspace)
1593 src_dir = root / "src"
1596 src_pyproject = src_dir / "pyproject.toml"
1597 src_pyproject.touch()
1599 test_toml_content = (THIS_DIR / "test.toml").read_text(encoding="utf-8")
1600 src_pyproject.write_text(test_toml_content, encoding="utf-8")
1602 src_python = src_dir / "foo.py"
1605 fake_ctx = FakeContext()
1606 fake_ctx.params["src"] = ("-",)
1607 fake_ctx.params["stdin_filename"] = str(src_python)
1609 with change_directory(root):
1610 black.read_pyproject_toml(fake_ctx, FakeParameter(), None)
1612 config = fake_ctx.default_map
1613 self.assertEqual(config["verbose"], "1")
1614 self.assertEqual(config["check"], "no")
1615 self.assertEqual(config["diff"], "y")
1616 self.assertEqual(config["color"], "True")
1617 self.assertEqual(config["line_length"], "79")
1618 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1619 self.assertEqual(config["exclude"], r"\.pyi?$")
1620 self.assertEqual(config["include"], r"\.py?$")
1622 @pytest.mark.incompatible_with_mypyc
1623 def test_find_project_root(self) -> None:
1624 with TemporaryDirectory() as workspace:
1625 root = Path(workspace)
1626 test_dir = root / "test"
1629 src_dir = root / "src"
1632 root_pyproject = root / "pyproject.toml"
1633 root_pyproject.touch()
1634 src_pyproject = src_dir / "pyproject.toml"
1635 src_pyproject.touch()
1636 src_python = src_dir / "foo.py"
1640 black.find_project_root((src_dir, test_dir)),
1641 (root.resolve(), "pyproject.toml"),
1644 black.find_project_root((src_dir,)),
1645 (src_dir.resolve(), "pyproject.toml"),
1648 black.find_project_root((src_python,)),
1649 (src_dir.resolve(), "pyproject.toml"),
1652 with change_directory(test_dir):
1654 black.find_project_root(("-",), stdin_filename="../src/a.py"),
1655 (src_dir.resolve(), "pyproject.toml"),
1659 "black.files.find_user_pyproject_toml",
1661 def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
1662 find_user_pyproject_toml.side_effect = RuntimeError()
1664 with redirect_stderr(io.StringIO()) as stderr:
1665 result = black.files.find_pyproject_toml(
1666 path_search_start=(str(Path.cwd().root),)
1669 assert result is None
1670 err = stderr.getvalue()
1671 assert "Ignoring user configuration" in err
1674 "black.files.find_user_pyproject_toml",
1675 black.files.find_user_pyproject_toml.__wrapped__,
1677 def test_find_user_pyproject_toml_linux(self) -> None:
1678 if system() == "Windows":
1681 # Test if XDG_CONFIG_HOME is checked
1682 with TemporaryDirectory() as workspace:
1683 tmp_user_config = Path(workspace) / "black"
1684 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1686 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1689 # Test fallback for XDG_CONFIG_HOME
1690 with patch.dict("os.environ"):
1691 os.environ.pop("XDG_CONFIG_HOME", None)
1692 fallback_user_config = Path("~/.config").expanduser() / "black"
1694 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1697 def test_find_user_pyproject_toml_windows(self) -> None:
1698 if system() != "Windows":
1701 user_config_path = Path.home() / ".black"
1703 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1706 def test_bpo_33660_workaround(self) -> None:
1707 if system() == "Windows":
1710 # https://bugs.python.org/issue33660
1712 with change_directory(root):
1713 path = Path("workspace") / "project"
1714 report = black.Report(verbose=True)
1715 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1716 self.assertEqual(normalized_path, "workspace/project")
1718 def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
1719 if system() != "Windows":
1722 with TemporaryDirectory() as workspace:
1723 root = Path(workspace)
1724 junction_dir = root / "junction"
1725 junction_target_outside_of_root = root / ".."
1726 os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
1728 report = black.Report(verbose=True)
1729 normalized_path = black.normalize_path_maybe_ignore(
1730 junction_dir, root, report
1732 # Manually delete for Python < 3.8
1733 os.system(f"rmdir {junction_dir}")
1735 self.assertEqual(normalized_path, None)
1737 def test_newline_comment_interaction(self) -> None:
1738 source = "class A:\\\r\n# type: ignore\n pass\n"
1739 output = black.format_str(source, mode=DEFAULT_MODE)
1740 black.assert_stable(source, output, mode=DEFAULT_MODE)
1742 def test_bpo_2142_workaround(self) -> None:
1743 # https://bugs.python.org/issue2142
1745 source, _ = read_data("miscellaneous", "missing_final_newline")
1746 # read_data adds a trailing newline
1747 source = source.rstrip()
1748 expected, _ = read_data("miscellaneous", "missing_final_newline.diff")
1749 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1750 diff_header = re.compile(
1751 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1752 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
1755 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1756 self.assertEqual(result.exit_code, 0)
1759 actual = result.output
1760 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1761 self.assertEqual(actual, expected)
1764 def compare_results(
1765 result: click.testing.Result, expected_value: str, expected_exit_code: int
1767 """Helper method to test the value and exit code of a click Result."""
1769 result.output == expected_value
1770 ), "The output did not match the expected value."
1771 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1773 def test_code_option(self) -> None:
1774 """Test the code option with no changes."""
1775 code = 'print("Hello world")\n'
1776 args = ["--code", code]
1777 result = CliRunner().invoke(black.main, args)
1779 self.compare_results(result, code, 0)
1781 def test_code_option_changed(self) -> None:
1782 """Test the code option when changes are required."""
1783 code = "print('hello world')"
1784 formatted = black.format_str(code, mode=DEFAULT_MODE)
1786 args = ["--code", code]
1787 result = CliRunner().invoke(black.main, args)
1789 self.compare_results(result, formatted, 0)
1791 def test_code_option_check(self) -> None:
1792 """Test the code option when check is passed."""
1793 args = ["--check", "--code", 'print("Hello world")\n']
1794 result = CliRunner().invoke(black.main, args)
1795 self.compare_results(result, "", 0)
1797 def test_code_option_check_changed(self) -> None:
1798 """Test the code option when changes are required, and check is passed."""
1799 args = ["--check", "--code", "print('hello world')"]
1800 result = CliRunner().invoke(black.main, args)
1801 self.compare_results(result, "", 1)
1803 def test_code_option_diff(self) -> None:
1804 """Test the code option when diff is passed."""
1805 code = "print('hello world')"
1806 formatted = black.format_str(code, mode=DEFAULT_MODE)
1807 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1809 args = ["--diff", "--code", code]
1810 result = CliRunner().invoke(black.main, args)
1812 # Remove time from diff
1813 output = DIFF_TIME.sub("", result.output)
1815 assert output == result_diff, "The output did not match the expected value."
1816 assert result.exit_code == 0, "The exit code is incorrect."
1818 def test_code_option_color_diff(self) -> None:
1819 """Test the code option when color and diff are passed."""
1820 code = "print('hello world')"
1821 formatted = black.format_str(code, mode=DEFAULT_MODE)
1823 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1824 result_diff = color_diff(result_diff)
1826 args = ["--diff", "--color", "--code", code]
1827 result = CliRunner().invoke(black.main, args)
1829 # Remove time from diff
1830 output = DIFF_TIME.sub("", result.output)
1832 assert output == result_diff, "The output did not match the expected value."
1833 assert result.exit_code == 0, "The exit code is incorrect."
1835 @pytest.mark.incompatible_with_mypyc
1836 def test_code_option_safe(self) -> None:
1837 """Test that the code option throws an error when the sanity checks fail."""
1838 # Patch black.assert_equivalent to ensure the sanity checks fail
1839 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1840 code = 'print("Hello world")'
1841 error_msg = f"{code}\nerror: cannot format <string>: \n"
1843 args = ["--safe", "--code", code]
1844 result = CliRunner().invoke(black.main, args)
1846 self.compare_results(result, error_msg, 123)
1848 def test_code_option_fast(self) -> None:
1849 """Test that the code option ignores errors when the sanity checks fail."""
1850 # Patch black.assert_equivalent to ensure the sanity checks fail
1851 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1852 code = 'print("Hello world")'
1853 formatted = black.format_str(code, mode=DEFAULT_MODE)
1855 args = ["--fast", "--code", code]
1856 result = CliRunner().invoke(black.main, args)
1858 self.compare_results(result, formatted, 0)
1860 @pytest.mark.incompatible_with_mypyc
1861 def test_code_option_config(self) -> None:
1863 Test that the code option finds the pyproject.toml in the current directory.
1865 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1866 args = ["--code", "print"]
1867 # This is the only directory known to contain a pyproject.toml
1868 with change_directory(PROJECT_ROOT):
1869 CliRunner().invoke(black.main, args)
1870 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1873 len(parse.mock_calls) >= 1
1874 ), "Expected config parse to be called with the current directory."
1876 _, call_args, _ = parse.mock_calls[0]
1878 call_args[0].lower() == str(pyproject_path).lower()
1879 ), "Incorrect config loaded."
1881 @pytest.mark.incompatible_with_mypyc
1882 def test_code_option_parent_config(self) -> None:
1884 Test that the code option finds the pyproject.toml in the parent directory.
1886 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1887 with change_directory(THIS_DIR):
1888 args = ["--code", "print"]
1889 CliRunner().invoke(black.main, args)
1891 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1893 len(parse.mock_calls) >= 1
1894 ), "Expected config parse to be called with the current directory."
1896 _, call_args, _ = parse.mock_calls[0]
1898 call_args[0].lower() == str(pyproject_path).lower()
1899 ), "Incorrect config loaded."
1901 def test_for_handled_unexpected_eof_error(self) -> None:
1903 Test that an unexpected EOF SyntaxError is nicely presented.
1905 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1906 black.lib2to3_parse("print(", {})
1908 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1910 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1911 with pytest.raises(AssertionError) as err:
1912 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1915 # Unfortunately the SyntaxError message has changed in newer versions so we
1916 # can't match it directly.
1917 err.match("invalid character")
1918 err.match(r"\(<unknown>, line 1\)")
1922 def test_get_cache_dir(
1925 monkeypatch: pytest.MonkeyPatch,
1927 # Create multiple cache directories
1928 workspace1 = tmp_path / "ws1"
1930 workspace2 = tmp_path / "ws2"
1933 # Force user_cache_dir to use the temporary directory for easier assertions
1934 patch_user_cache_dir = patch(
1935 target="black.cache.user_cache_dir",
1937 return_value=str(workspace1),
1940 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1941 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1942 with patch_user_cache_dir:
1943 assert get_cache_dir() == workspace1
1945 # If it is set, use the path provided in the env var.
1946 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1947 assert get_cache_dir() == workspace2
1949 def test_cache_broken_file(self) -> None:
1951 with cache_dir() as workspace:
1952 cache_file = get_cache_file(mode)
1953 cache_file.write_text("this is not a pickle", encoding="utf-8")
1954 assert black.Cache.read(mode).file_data == {}
1955 src = (workspace / "test.py").resolve()
1956 src.write_text("print('hello')", encoding="utf-8")
1957 invokeBlack([str(src)])
1958 cache = black.Cache.read(mode)
1959 assert not cache.is_changed(src)
1961 def test_cache_single_file_already_cached(self) -> None:
1963 with cache_dir() as workspace:
1964 src = (workspace / "test.py").resolve()
1965 src.write_text("print('hello')", encoding="utf-8")
1966 cache = black.Cache.read(mode)
1968 invokeBlack([str(src)])
1969 assert src.read_text(encoding="utf-8") == "print('hello')"
1972 def test_cache_multiple_files(self) -> None:
1974 with cache_dir() as workspace, patch(
1975 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
1977 one = (workspace / "one.py").resolve()
1978 one.write_text("print('hello')", encoding="utf-8")
1979 two = (workspace / "two.py").resolve()
1980 two.write_text("print('hello')", encoding="utf-8")
1981 cache = black.Cache.read(mode)
1983 invokeBlack([str(workspace)])
1984 assert one.read_text(encoding="utf-8") == "print('hello')"
1985 assert two.read_text(encoding="utf-8") == 'print("hello")\n'
1986 cache = black.Cache.read(mode)
1987 assert not cache.is_changed(one)
1988 assert not cache.is_changed(two)
1990 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1991 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1993 with cache_dir() as workspace:
1994 src = (workspace / "test.py").resolve()
1995 src.write_text("print('hello')", encoding="utf-8")
1996 with patch.object(black.Cache, "read") as read_cache, patch.object(
1997 black.Cache, "write"
1999 cmd = [str(src), "--diff"]
2001 cmd.append("--color")
2003 cache_file = get_cache_file(mode)
2004 assert cache_file.exists() is False
2005 read_cache.assert_called_once()
2006 write_cache.assert_not_called()
2008 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
2010 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
2011 with cache_dir() as workspace:
2012 for tag in range(0, 4):
2013 src = (workspace / f"test{tag}.py").resolve()
2014 src.write_text("print('hello')", encoding="utf-8")
2016 "black.concurrency.Manager", wraps=multiprocessing.Manager
2018 cmd = ["--diff", str(workspace)]
2020 cmd.append("--color")
2021 invokeBlack(cmd, exit_code=0)
2022 # this isn't quite doing what we want, but if it _isn't_
2023 # called then we cannot be using the lock it provides
2026 def test_no_cache_when_stdin(self) -> None:
2029 result = CliRunner().invoke(
2030 black.main, ["-"], input=BytesIO(b"print('hello')")
2032 assert not result.exit_code
2033 cache_file = get_cache_file(mode)
2034 assert not cache_file.exists()
2036 def test_read_cache_no_cachefile(self) -> None:
2039 assert black.Cache.read(mode).file_data == {}
2041 def test_write_cache_read_cache(self) -> None:
2043 with cache_dir() as workspace:
2044 src = (workspace / "test.py").resolve()
2046 write_cache = black.Cache.read(mode)
2047 write_cache.write([src])
2048 read_cache = black.Cache.read(mode)
2049 assert not read_cache.is_changed(src)
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_symlink_out_of_root_directory(self) -> None:
2380 root = THIS_DIR.resolve()
2382 include = re.compile(black.DEFAULT_INCLUDES)
2383 exclude = re.compile(black.DEFAULT_EXCLUDES)
2384 report = black.Report()
2385 gitignore = PathSpec.from_lines("gitwildmatch", [])
2386 # `child` should behave like a symlink which resolved path is clearly
2387 # outside of the `root` directory.
2388 path.iterdir.return_value = [child]
2389 child.resolve.return_value = Path("/a/b/c")
2390 child.as_posix.return_value = "/a/b/c"
2393 black.gen_python_files(
2406 except ValueError as ve:
2407 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
2408 path.iterdir.assert_called_once()
2409 child.resolve.assert_called_once()
2411 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2412 def test_get_sources_with_stdin(self) -> None:
2415 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2417 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2418 def test_get_sources_with_stdin_filename(self) -> None:
2420 stdin_filename = str(THIS_DIR / "data/collections.py")
2421 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2422 assert_collected_sources(
2425 exclude=r"/exclude/a\.py",
2426 stdin_filename=stdin_filename,
2429 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2430 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2431 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2432 # file being passed directly. This is the same as
2433 # test_exclude_for_issue_1572
2434 path = DATA_DIR / "include_exclude_tests"
2436 stdin_filename = str(path / "b/exclude/a.py")
2437 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2438 assert_collected_sources(
2441 exclude=r"/exclude/|a\.py",
2442 stdin_filename=stdin_filename,
2445 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2446 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2447 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2448 # file being passed directly. This is the same as
2449 # test_exclude_for_issue_1572
2451 path = THIS_DIR / "data" / "include_exclude_tests"
2452 stdin_filename = str(path / "b/exclude/a.py")
2453 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2454 assert_collected_sources(
2457 extend_exclude=r"/exclude/|a\.py",
2458 stdin_filename=stdin_filename,
2461 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2462 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2463 # Force exclude should exclude the file when passing it through
2465 path = THIS_DIR / "data" / "include_exclude_tests"
2466 stdin_filename = str(path / "b/exclude/a.py")
2467 assert_collected_sources(
2470 force_exclude=r"/exclude/|a\.py",
2471 stdin_filename=stdin_filename,
2476 with open(black.__file__, "r", encoding="utf-8") as _bf:
2477 black_source_lines = _bf.readlines()
2478 except UnicodeDecodeError:
2479 if not black.COMPILED:
2484 frame: types.FrameType, event: str, arg: Any
2485 ) -> Callable[[types.FrameType, str, Any], Any]:
2486 """Show function calls `from black/__init__.py` as they happen.
2488 Register this with `sys.settrace()` in a test you're debugging.
2493 stack = len(inspect.stack()) - 19
2495 filename = frame.f_code.co_filename
2496 lineno = frame.f_lineno
2497 func_sig_lineno = lineno - 1
2498 funcname = black_source_lines[func_sig_lineno].strip()
2499 while funcname.startswith("@"):
2500 func_sig_lineno += 1
2501 funcname = black_source_lines[func_sig_lineno].strip()
2502 if "black/__init__.py" in filename:
2503 print(f"{' ' * stack}{lineno}:{funcname}")