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])):
511 # Note that the root folder (project_root) isn't the folder
512 # named "root" (aka working_directory)
513 ctx.obj["root"] = project_root
514 report = MagicMock(verbose=True)
520 include=DEFAULT_INCLUDE,
528 mock_args[1].startswith("is a symbolic link that points outside")
529 for _, mock_args, _ in report.path_ignored.mock_calls
530 ), "A symbolic link was reported."
531 report.path_ignored.assert_called_once_with(
532 Path("root", "child", "b.py"), "matches a .gitignore file content"
535 def test_report_verbose(self) -> None:
536 report = Report(verbose=True)
540 def out(msg: str, **kwargs: Any) -> None:
541 out_lines.append(msg)
543 def err(msg: str, **kwargs: Any) -> None:
544 err_lines.append(msg)
546 with patch("black.output._out", out), patch("black.output._err", err):
547 report.done(Path("f1"), black.Changed.NO)
548 self.assertEqual(len(out_lines), 1)
549 self.assertEqual(len(err_lines), 0)
550 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
551 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
552 self.assertEqual(report.return_code, 0)
553 report.done(Path("f2"), black.Changed.YES)
554 self.assertEqual(len(out_lines), 2)
555 self.assertEqual(len(err_lines), 0)
556 self.assertEqual(out_lines[-1], "reformatted f2")
558 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
560 report.done(Path("f3"), black.Changed.CACHED)
561 self.assertEqual(len(out_lines), 3)
562 self.assertEqual(len(err_lines), 0)
564 out_lines[-1], "f3 wasn't modified on disk since last run."
567 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
569 self.assertEqual(report.return_code, 0)
571 self.assertEqual(report.return_code, 1)
573 report.failed(Path("e1"), "boom")
574 self.assertEqual(len(out_lines), 3)
575 self.assertEqual(len(err_lines), 1)
576 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
578 unstyle(str(report)),
579 "1 file reformatted, 2 files left unchanged, 1 file failed to"
582 self.assertEqual(report.return_code, 123)
583 report.done(Path("f3"), black.Changed.YES)
584 self.assertEqual(len(out_lines), 4)
585 self.assertEqual(len(err_lines), 1)
586 self.assertEqual(out_lines[-1], "reformatted f3")
588 unstyle(str(report)),
589 "2 files reformatted, 2 files left unchanged, 1 file failed to"
592 self.assertEqual(report.return_code, 123)
593 report.failed(Path("e2"), "boom")
594 self.assertEqual(len(out_lines), 4)
595 self.assertEqual(len(err_lines), 2)
596 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
598 unstyle(str(report)),
599 "2 files reformatted, 2 files left unchanged, 2 files failed to"
602 self.assertEqual(report.return_code, 123)
603 report.path_ignored(Path("wat"), "no match")
604 self.assertEqual(len(out_lines), 5)
605 self.assertEqual(len(err_lines), 2)
606 self.assertEqual(out_lines[-1], "wat ignored: no match")
608 unstyle(str(report)),
609 "2 files reformatted, 2 files left unchanged, 2 files failed to"
612 self.assertEqual(report.return_code, 123)
613 report.done(Path("f4"), black.Changed.NO)
614 self.assertEqual(len(out_lines), 6)
615 self.assertEqual(len(err_lines), 2)
616 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
618 unstyle(str(report)),
619 "2 files reformatted, 3 files left unchanged, 2 files failed to"
622 self.assertEqual(report.return_code, 123)
625 unstyle(str(report)),
626 "2 files would be reformatted, 3 files would be left unchanged, 2"
627 " files would fail to reformat.",
632 unstyle(str(report)),
633 "2 files would be reformatted, 3 files would be left unchanged, 2"
634 " files would fail to reformat.",
637 def test_report_quiet(self) -> None:
638 report = Report(quiet=True)
642 def out(msg: str, **kwargs: Any) -> None:
643 out_lines.append(msg)
645 def err(msg: str, **kwargs: Any) -> None:
646 err_lines.append(msg)
648 with patch("black.output._out", out), patch("black.output._err", err):
649 report.done(Path("f1"), black.Changed.NO)
650 self.assertEqual(len(out_lines), 0)
651 self.assertEqual(len(err_lines), 0)
652 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
653 self.assertEqual(report.return_code, 0)
654 report.done(Path("f2"), black.Changed.YES)
655 self.assertEqual(len(out_lines), 0)
656 self.assertEqual(len(err_lines), 0)
658 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
660 report.done(Path("f3"), black.Changed.CACHED)
661 self.assertEqual(len(out_lines), 0)
662 self.assertEqual(len(err_lines), 0)
664 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
666 self.assertEqual(report.return_code, 0)
668 self.assertEqual(report.return_code, 1)
670 report.failed(Path("e1"), "boom")
671 self.assertEqual(len(out_lines), 0)
672 self.assertEqual(len(err_lines), 1)
673 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
675 unstyle(str(report)),
676 "1 file reformatted, 2 files left unchanged, 1 file failed to"
679 self.assertEqual(report.return_code, 123)
680 report.done(Path("f3"), black.Changed.YES)
681 self.assertEqual(len(out_lines), 0)
682 self.assertEqual(len(err_lines), 1)
684 unstyle(str(report)),
685 "2 files reformatted, 2 files left unchanged, 1 file failed to"
688 self.assertEqual(report.return_code, 123)
689 report.failed(Path("e2"), "boom")
690 self.assertEqual(len(out_lines), 0)
691 self.assertEqual(len(err_lines), 2)
692 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
694 unstyle(str(report)),
695 "2 files reformatted, 2 files left unchanged, 2 files failed to"
698 self.assertEqual(report.return_code, 123)
699 report.path_ignored(Path("wat"), "no match")
700 self.assertEqual(len(out_lines), 0)
701 self.assertEqual(len(err_lines), 2)
703 unstyle(str(report)),
704 "2 files reformatted, 2 files left unchanged, 2 files failed to"
707 self.assertEqual(report.return_code, 123)
708 report.done(Path("f4"), black.Changed.NO)
709 self.assertEqual(len(out_lines), 0)
710 self.assertEqual(len(err_lines), 2)
712 unstyle(str(report)),
713 "2 files reformatted, 3 files left unchanged, 2 files failed to"
716 self.assertEqual(report.return_code, 123)
719 unstyle(str(report)),
720 "2 files would be reformatted, 3 files would be left unchanged, 2"
721 " files would fail to reformat.",
726 unstyle(str(report)),
727 "2 files would be reformatted, 3 files would be left unchanged, 2"
728 " files would fail to reformat.",
731 def test_report_normal(self) -> None:
732 report = black.Report()
736 def out(msg: str, **kwargs: Any) -> None:
737 out_lines.append(msg)
739 def err(msg: str, **kwargs: Any) -> None:
740 err_lines.append(msg)
742 with patch("black.output._out", out), patch("black.output._err", err):
743 report.done(Path("f1"), black.Changed.NO)
744 self.assertEqual(len(out_lines), 0)
745 self.assertEqual(len(err_lines), 0)
746 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
747 self.assertEqual(report.return_code, 0)
748 report.done(Path("f2"), black.Changed.YES)
749 self.assertEqual(len(out_lines), 1)
750 self.assertEqual(len(err_lines), 0)
751 self.assertEqual(out_lines[-1], "reformatted f2")
753 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
755 report.done(Path("f3"), black.Changed.CACHED)
756 self.assertEqual(len(out_lines), 1)
757 self.assertEqual(len(err_lines), 0)
758 self.assertEqual(out_lines[-1], "reformatted f2")
760 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
762 self.assertEqual(report.return_code, 0)
764 self.assertEqual(report.return_code, 1)
766 report.failed(Path("e1"), "boom")
767 self.assertEqual(len(out_lines), 1)
768 self.assertEqual(len(err_lines), 1)
769 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
771 unstyle(str(report)),
772 "1 file reformatted, 2 files left unchanged, 1 file failed to"
775 self.assertEqual(report.return_code, 123)
776 report.done(Path("f3"), black.Changed.YES)
777 self.assertEqual(len(out_lines), 2)
778 self.assertEqual(len(err_lines), 1)
779 self.assertEqual(out_lines[-1], "reformatted f3")
781 unstyle(str(report)),
782 "2 files reformatted, 2 files left unchanged, 1 file failed to"
785 self.assertEqual(report.return_code, 123)
786 report.failed(Path("e2"), "boom")
787 self.assertEqual(len(out_lines), 2)
788 self.assertEqual(len(err_lines), 2)
789 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
791 unstyle(str(report)),
792 "2 files reformatted, 2 files left unchanged, 2 files failed to"
795 self.assertEqual(report.return_code, 123)
796 report.path_ignored(Path("wat"), "no match")
797 self.assertEqual(len(out_lines), 2)
798 self.assertEqual(len(err_lines), 2)
800 unstyle(str(report)),
801 "2 files reformatted, 2 files left unchanged, 2 files failed to"
804 self.assertEqual(report.return_code, 123)
805 report.done(Path("f4"), black.Changed.NO)
806 self.assertEqual(len(out_lines), 2)
807 self.assertEqual(len(err_lines), 2)
809 unstyle(str(report)),
810 "2 files reformatted, 3 files left unchanged, 2 files failed to"
813 self.assertEqual(report.return_code, 123)
816 unstyle(str(report)),
817 "2 files would be reformatted, 3 files would be left unchanged, 2"
818 " files would fail to reformat.",
823 unstyle(str(report)),
824 "2 files would be reformatted, 3 files would be left unchanged, 2"
825 " files would fail to reformat.",
828 def test_lib2to3_parse(self) -> None:
829 with self.assertRaises(black.InvalidInput):
830 black.lib2to3_parse("invalid syntax")
833 black.lib2to3_parse(straddling)
834 black.lib2to3_parse(straddling, {TargetVersion.PY36})
837 with self.assertRaises(black.InvalidInput):
838 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
840 py3_only = "exec(x, end=y)"
841 black.lib2to3_parse(py3_only)
842 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
844 def test_get_features_used_decorator(self) -> None:
845 # Test the feature detection of new decorator syntax
846 # since this makes some test cases of test_get_features_used()
847 # fails if it fails, this is tested first so that a useful case
849 simples, relaxed = read_data("miscellaneous", "decorators")
850 # skip explanation comments at the top of the file
851 for simple_test in simples.split("##")[1:]:
852 node = black.lib2to3_parse(simple_test)
853 decorator = str(node.children[0].children[0]).strip()
855 Feature.RELAXED_DECORATORS,
856 black.get_features_used(node),
858 f"decorator '{decorator}' follows python<=3.8 syntax"
859 "but is detected as 3.9+"
860 # f"The full node is\n{node!r}"
863 # skip the '# output' comment at the top of the output part
864 for relaxed_test in relaxed.split("##")[1:]:
865 node = black.lib2to3_parse(relaxed_test)
866 decorator = str(node.children[0].children[0]).strip()
868 Feature.RELAXED_DECORATORS,
869 black.get_features_used(node),
871 f"decorator '{decorator}' uses python3.9+ syntax"
872 "but is detected as python<=3.8"
873 # f"The full node is\n{node!r}"
877 def test_get_features_used(self) -> None:
878 node = black.lib2to3_parse("def f(*, arg): ...\n")
879 self.assertEqual(black.get_features_used(node), set())
880 node = black.lib2to3_parse("def f(*, arg,): ...\n")
881 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
882 node = black.lib2to3_parse("f(*arg,)\n")
884 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
886 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
887 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
888 node = black.lib2to3_parse("123_456\n")
889 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
890 node = black.lib2to3_parse("123456\n")
891 self.assertEqual(black.get_features_used(node), set())
892 source, expected = read_data("simple_cases", "function")
893 node = black.lib2to3_parse(source)
894 expected_features = {
895 Feature.TRAILING_COMMA_IN_CALL,
896 Feature.TRAILING_COMMA_IN_DEF,
899 self.assertEqual(black.get_features_used(node), expected_features)
900 node = black.lib2to3_parse(expected)
901 self.assertEqual(black.get_features_used(node), expected_features)
902 source, expected = read_data("simple_cases", "expression")
903 node = black.lib2to3_parse(source)
904 self.assertEqual(black.get_features_used(node), set())
905 node = black.lib2to3_parse(expected)
906 self.assertEqual(black.get_features_used(node), set())
907 node = black.lib2to3_parse("lambda a, /, b: ...")
908 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
909 node = black.lib2to3_parse("def fn(a, /, b): ...")
910 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
911 node = black.lib2to3_parse("def fn(): yield a, b")
912 self.assertEqual(black.get_features_used(node), set())
913 node = black.lib2to3_parse("def fn(): return a, b")
914 self.assertEqual(black.get_features_used(node), set())
915 node = black.lib2to3_parse("def fn(): yield *b, c")
916 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
917 node = black.lib2to3_parse("def fn(): return a, *b, c")
918 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
919 node = black.lib2to3_parse("x = a, *b, c")
920 self.assertEqual(black.get_features_used(node), set())
921 node = black.lib2to3_parse("x: Any = regular")
922 self.assertEqual(black.get_features_used(node), set())
923 node = black.lib2to3_parse("x: Any = (regular, regular)")
924 self.assertEqual(black.get_features_used(node), set())
925 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
926 self.assertEqual(black.get_features_used(node), set())
927 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
929 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
931 node = black.lib2to3_parse("try: pass\nexcept Something: pass")
932 self.assertEqual(black.get_features_used(node), set())
933 node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass")
934 self.assertEqual(black.get_features_used(node), set())
935 node = black.lib2to3_parse("try: pass\nexcept *Group: pass")
936 self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR})
937 node = black.lib2to3_parse("a[*b]")
938 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
939 node = black.lib2to3_parse("a[x, *y(), z] = t")
940 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
941 node = black.lib2to3_parse("def fn(*args: *T): pass")
942 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
944 def test_get_features_used_for_future_flags(self) -> None:
945 for src, features in [
946 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
948 "from __future__ import (other, annotations)",
949 {Feature.FUTURE_ANNOTATIONS},
951 ("a = 1 + 2\nfrom something import annotations", set()),
952 ("from __future__ import x, y", set()),
954 with self.subTest(src=src, features=features):
955 node = black.lib2to3_parse(src)
956 future_imports = black.get_future_imports(node)
958 black.get_features_used(node, future_imports=future_imports),
962 def test_get_future_imports(self) -> None:
963 node = black.lib2to3_parse("\n")
964 self.assertEqual(set(), black.get_future_imports(node))
965 node = black.lib2to3_parse("from __future__ import black\n")
966 self.assertEqual({"black"}, black.get_future_imports(node))
967 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
968 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
969 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
970 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
971 node = black.lib2to3_parse(
972 "from __future__ import multiple\nfrom __future__ import imports\n"
974 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
975 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
976 self.assertEqual({"black"}, black.get_future_imports(node))
977 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
978 self.assertEqual({"black"}, black.get_future_imports(node))
979 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
980 self.assertEqual(set(), black.get_future_imports(node))
981 node = black.lib2to3_parse("from some.module import black\n")
982 self.assertEqual(set(), black.get_future_imports(node))
983 node = black.lib2to3_parse(
984 "from __future__ import unicode_literals as _unicode_literals"
986 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
987 node = black.lib2to3_parse(
988 "from __future__ import unicode_literals as _lol, print"
990 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
992 @pytest.mark.incompatible_with_mypyc
993 def test_debug_visitor(self) -> None:
994 source, _ = read_data("miscellaneous", "debug_visitor")
995 expected, _ = read_data("miscellaneous", "debug_visitor.out")
999 def out(msg: str, **kwargs: Any) -> None:
1000 out_lines.append(msg)
1002 def err(msg: str, **kwargs: Any) -> None:
1003 err_lines.append(msg)
1005 with patch("black.debug.out", out):
1006 DebugVisitor.show(source)
1007 actual = "\n".join(out_lines) + "\n"
1009 if expected != actual:
1010 log_name = black.dump_to_file(*out_lines)
1014 f"AST print out is different. Actual version dumped to {log_name}",
1017 def test_format_file_contents(self) -> None:
1020 with self.assertRaises(black.NothingChanged):
1021 black.format_file_contents(empty, mode=mode, fast=False)
1023 with self.assertRaises(black.NothingChanged):
1024 black.format_file_contents(just_nl, mode=mode, fast=False)
1025 same = "j = [1, 2, 3]\n"
1026 with self.assertRaises(black.NothingChanged):
1027 black.format_file_contents(same, mode=mode, fast=False)
1028 different = "j = [1,2,3]"
1030 actual = black.format_file_contents(different, mode=mode, fast=False)
1031 self.assertEqual(expected, actual)
1032 invalid = "return if you can"
1033 with self.assertRaises(black.InvalidInput) as e:
1034 black.format_file_contents(invalid, mode=mode, fast=False)
1035 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
1037 mode = black.Mode(preview=True)
1039 with self.assertRaises(black.NothingChanged):
1040 black.format_file_contents(just_crlf, mode=mode, fast=False)
1041 just_whitespace_nl = "\n\t\n \n\t \n \t\n\n"
1042 actual = black.format_file_contents(just_whitespace_nl, mode=mode, fast=False)
1043 self.assertEqual("\n", actual)
1044 just_whitespace_crlf = "\r\n\t\r\n \r\n\t \r\n \t\r\n\r\n"
1045 actual = black.format_file_contents(just_whitespace_crlf, mode=mode, fast=False)
1046 self.assertEqual("\r\n", actual)
1048 def test_endmarker(self) -> None:
1049 n = black.lib2to3_parse("\n")
1050 self.assertEqual(n.type, black.syms.file_input)
1051 self.assertEqual(len(n.children), 1)
1052 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
1054 @pytest.mark.incompatible_with_mypyc
1055 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
1056 def test_assertFormatEqual(self) -> None:
1060 def out(msg: str, **kwargs: Any) -> None:
1061 out_lines.append(msg)
1063 def err(msg: str, **kwargs: Any) -> None:
1064 err_lines.append(msg)
1066 with patch("black.output._out", out), patch("black.output._err", err):
1067 with self.assertRaises(AssertionError):
1068 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
1070 out_str = "".join(out_lines)
1071 self.assertIn("Expected tree:", out_str)
1072 self.assertIn("Actual tree:", out_str)
1073 self.assertEqual("".join(err_lines), "")
1076 @patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1077 def test_works_in_mono_process_only_environment(self) -> None:
1078 with cache_dir() as workspace:
1080 (workspace / "one.py").resolve(),
1081 (workspace / "two.py").resolve(),
1083 f.write_text('print("hello")\n', encoding="utf-8")
1084 self.invokeBlack([str(workspace)])
1087 def test_check_diff_use_together(self) -> None:
1089 # Files which will be reformatted.
1090 src1 = get_case_path("miscellaneous", "string_quotes")
1091 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1092 # Files which will not be reformatted.
1093 src2 = get_case_path("simple_cases", "composition")
1094 self.invokeBlack([str(src2), "--diff", "--check"])
1095 # Multi file command.
1096 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1098 def test_no_src_fails(self) -> None:
1100 self.invokeBlack([], exit_code=1)
1102 def test_src_and_code_fails(self) -> None:
1104 self.invokeBlack([".", "-c", "0"], exit_code=1)
1106 def test_broken_symlink(self) -> None:
1107 with cache_dir() as workspace:
1108 symlink = workspace / "broken_link.py"
1110 symlink.symlink_to("nonexistent.py")
1111 except (OSError, NotImplementedError) as e:
1112 self.skipTest(f"Can't create symlinks: {e}")
1113 self.invokeBlack([str(workspace.resolve())])
1115 def test_single_file_force_pyi(self) -> None:
1116 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1117 contents, expected = read_data("miscellaneous", "force_pyi")
1118 with cache_dir() as workspace:
1119 path = (workspace / "file.py").resolve()
1120 path.write_text(contents, encoding="utf-8")
1121 self.invokeBlack([str(path), "--pyi"])
1122 actual = path.read_text(encoding="utf-8")
1123 # verify cache with --pyi is separate
1124 pyi_cache = black.Cache.read(pyi_mode)
1125 assert not pyi_cache.is_changed(path)
1126 normal_cache = black.Cache.read(DEFAULT_MODE)
1127 assert normal_cache.is_changed(path)
1128 self.assertFormatEqual(expected, actual)
1129 black.assert_equivalent(contents, actual)
1130 black.assert_stable(contents, actual, pyi_mode)
1133 def test_multi_file_force_pyi(self) -> None:
1134 reg_mode = DEFAULT_MODE
1135 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1136 contents, expected = read_data("miscellaneous", "force_pyi")
1137 with cache_dir() as workspace:
1139 (workspace / "file1.py").resolve(),
1140 (workspace / "file2.py").resolve(),
1143 path.write_text(contents, encoding="utf-8")
1144 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1146 actual = path.read_text(encoding="utf-8")
1147 self.assertEqual(actual, expected)
1148 # verify cache with --pyi is separate
1149 pyi_cache = black.Cache.read(pyi_mode)
1150 normal_cache = black.Cache.read(reg_mode)
1152 assert not pyi_cache.is_changed(path)
1153 assert normal_cache.is_changed(path)
1155 def test_pipe_force_pyi(self) -> None:
1156 source, expected = read_data("miscellaneous", "force_pyi")
1157 result = CliRunner().invoke(
1158 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf-8"))
1160 self.assertEqual(result.exit_code, 0)
1161 actual = result.output
1162 self.assertFormatEqual(actual, expected)
1164 def test_single_file_force_py36(self) -> None:
1165 reg_mode = DEFAULT_MODE
1166 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1167 source, expected = read_data("miscellaneous", "force_py36")
1168 with cache_dir() as workspace:
1169 path = (workspace / "file.py").resolve()
1170 path.write_text(source, encoding="utf-8")
1171 self.invokeBlack([str(path), *PY36_ARGS])
1172 actual = path.read_text(encoding="utf-8")
1173 # verify cache with --target-version is separate
1174 py36_cache = black.Cache.read(py36_mode)
1175 assert not py36_cache.is_changed(path)
1176 normal_cache = black.Cache.read(reg_mode)
1177 assert normal_cache.is_changed(path)
1178 self.assertEqual(actual, expected)
1181 def test_multi_file_force_py36(self) -> None:
1182 reg_mode = DEFAULT_MODE
1183 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1184 source, expected = read_data("miscellaneous", "force_py36")
1185 with cache_dir() as workspace:
1187 (workspace / "file1.py").resolve(),
1188 (workspace / "file2.py").resolve(),
1191 path.write_text(source, encoding="utf-8")
1192 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1194 actual = path.read_text(encoding="utf-8")
1195 self.assertEqual(actual, expected)
1196 # verify cache with --target-version is separate
1197 pyi_cache = black.Cache.read(py36_mode)
1198 normal_cache = black.Cache.read(reg_mode)
1200 assert not pyi_cache.is_changed(path)
1201 assert normal_cache.is_changed(path)
1203 def test_pipe_force_py36(self) -> None:
1204 source, expected = read_data("miscellaneous", "force_py36")
1205 result = CliRunner().invoke(
1207 ["-", "-q", "--target-version=py36"],
1208 input=BytesIO(source.encode("utf-8")),
1210 self.assertEqual(result.exit_code, 0)
1211 actual = result.output
1212 self.assertFormatEqual(actual, expected)
1214 @pytest.mark.incompatible_with_mypyc
1215 def test_reformat_one_with_stdin(self) -> None:
1217 "black.format_stdin_to_stdout",
1218 return_value=lambda *args, **kwargs: black.Changed.YES,
1220 report = MagicMock()
1225 write_back=black.WriteBack.YES,
1229 fsts.assert_called_once()
1230 report.done.assert_called_with(path, black.Changed.YES)
1232 @pytest.mark.incompatible_with_mypyc
1233 def test_reformat_one_with_stdin_filename(self) -> None:
1235 "black.format_stdin_to_stdout",
1236 return_value=lambda *args, **kwargs: black.Changed.YES,
1238 report = MagicMock()
1240 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1245 write_back=black.WriteBack.YES,
1249 fsts.assert_called_once_with(
1250 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1252 # __BLACK_STDIN_FILENAME__ should have been stripped
1253 report.done.assert_called_with(expected, black.Changed.YES)
1255 @pytest.mark.incompatible_with_mypyc
1256 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1258 "black.format_stdin_to_stdout",
1259 return_value=lambda *args, **kwargs: black.Changed.YES,
1261 report = MagicMock()
1263 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1268 write_back=black.WriteBack.YES,
1272 fsts.assert_called_once_with(
1274 write_back=black.WriteBack.YES,
1275 mode=replace(DEFAULT_MODE, is_pyi=True),
1277 # __BLACK_STDIN_FILENAME__ should have been stripped
1278 report.done.assert_called_with(expected, black.Changed.YES)
1280 @pytest.mark.incompatible_with_mypyc
1281 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1283 "black.format_stdin_to_stdout",
1284 return_value=lambda *args, **kwargs: black.Changed.YES,
1286 report = MagicMock()
1288 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1293 write_back=black.WriteBack.YES,
1297 fsts.assert_called_once_with(
1299 write_back=black.WriteBack.YES,
1300 mode=replace(DEFAULT_MODE, is_ipynb=True),
1302 # __BLACK_STDIN_FILENAME__ should have been stripped
1303 report.done.assert_called_with(expected, black.Changed.YES)
1305 @pytest.mark.incompatible_with_mypyc
1306 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1308 "black.format_stdin_to_stdout",
1309 return_value=lambda *args, **kwargs: black.Changed.YES,
1311 report = MagicMock()
1312 # Even with an existing file, since we are forcing stdin, black
1313 # should output to stdout and not modify the file inplace
1314 p = THIS_DIR / "data" / "simple_cases" / "collections.py"
1315 # Make sure is_file actually returns True
1316 self.assertTrue(p.is_file())
1317 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1322 write_back=black.WriteBack.YES,
1326 fsts.assert_called_once()
1327 # __BLACK_STDIN_FILENAME__ should have been stripped
1328 report.done.assert_called_with(expected, black.Changed.YES)
1330 def test_reformat_one_with_stdin_empty(self) -> None:
1337 (" \t\r\n\t ", "\r\n"),
1341 output: io.StringIO, io_TextIOWrapper: Type[io.TextIOWrapper]
1342 ) -> Callable[[Any, Any], io.TextIOWrapper]:
1343 def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper:
1344 if args == (sys.stdout.buffer,):
1345 # It's `format_stdin_to_stdout()` calling `io.TextIOWrapper()`,
1346 # return our mock object.
1348 # It's something else (i.e. `decode_bytes()`) calling
1349 # `io.TextIOWrapper()`, pass through to the original implementation.
1350 # See discussion in https://github.com/psf/black/pull/2489
1351 return io_TextIOWrapper(*args, **kwargs)
1355 mode = black.Mode(preview=True)
1356 for content, expected in cases:
1357 output = io.StringIO()
1358 io_TextIOWrapper = io.TextIOWrapper
1360 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1362 black.format_stdin_to_stdout(
1365 write_back=black.WriteBack.YES,
1368 except io.UnsupportedOperation:
1369 pass # StringIO does not support detach
1370 assert output.getvalue() == expected
1372 # An empty string is the only test case for `preview=False`
1373 output = io.StringIO()
1374 io_TextIOWrapper = io.TextIOWrapper
1375 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1377 black.format_stdin_to_stdout(
1380 write_back=black.WriteBack.YES,
1383 except io.UnsupportedOperation:
1384 pass # StringIO does not support detach
1385 assert output.getvalue() == ""
1387 def test_invalid_cli_regex(self) -> None:
1388 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1389 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1391 def test_required_version_matches_version(self) -> None:
1393 ["--required-version", black.__version__, "-c", "0"],
1398 def test_required_version_matches_partial_version(self) -> None:
1400 ["--required-version", black.__version__.split(".")[0], "-c", "0"],
1405 def test_required_version_does_not_match_on_minor_version(self) -> None:
1407 ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
1412 def test_required_version_does_not_match_version(self) -> None:
1413 result = BlackRunner().invoke(
1415 ["--required-version", "20.99b", "-c", "0"],
1417 self.assertEqual(result.exit_code, 1)
1418 self.assertIn("required version", result.stderr)
1420 def test_preserves_line_endings(self) -> None:
1421 with TemporaryDirectory() as workspace:
1422 test_file = Path(workspace) / "test.py"
1423 for nl in ["\n", "\r\n"]:
1424 contents = nl.join(["def f( ):", " pass"])
1425 test_file.write_bytes(contents.encode())
1426 ff(test_file, write_back=black.WriteBack.YES)
1427 updated_contents: bytes = test_file.read_bytes()
1428 self.assertIn(nl.encode(), updated_contents)
1430 self.assertNotIn(b"\r\n", updated_contents)
1432 def test_preserves_line_endings_via_stdin(self) -> None:
1433 for nl in ["\n", "\r\n"]:
1434 contents = nl.join(["def f( ):", " pass"])
1435 runner = BlackRunner()
1436 result = runner.invoke(
1437 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf-8"))
1439 self.assertEqual(result.exit_code, 0)
1440 output = result.stdout_bytes
1441 self.assertIn(nl.encode("utf-8"), output)
1443 self.assertNotIn(b"\r\n", output)
1445 def test_normalize_line_endings(self) -> None:
1446 with TemporaryDirectory() as workspace:
1447 test_file = Path(workspace) / "test.py"
1448 for data, expected in (
1449 (b"c\r\nc\n ", b"c\r\nc\r\n"),
1450 (b"l\nl\r\n ", b"l\nl\n"),
1452 test_file.write_bytes(data)
1453 ff(test_file, write_back=black.WriteBack.YES)
1454 self.assertEqual(test_file.read_bytes(), expected)
1456 def test_assert_equivalent_different_asts(self) -> None:
1457 with self.assertRaises(AssertionError):
1458 black.assert_equivalent("{}", "None")
1460 def test_root_logger_not_used_directly(self) -> None:
1461 def fail(*args: Any, **kwargs: Any) -> None:
1462 self.fail("Record created with root logger")
1464 with patch.multiple(
1473 ff(THIS_DIR / "util.py")
1475 def test_invalid_config_return_code(self) -> None:
1476 tmp_file = Path(black.dump_to_file())
1478 tmp_config = Path(black.dump_to_file())
1480 args = ["--config", str(tmp_config), str(tmp_file)]
1481 self.invokeBlack(args, exit_code=2, ignore_config=False)
1485 def test_parse_pyproject_toml(self) -> None:
1486 test_toml_file = THIS_DIR / "test.toml"
1487 config = black.parse_pyproject_toml(str(test_toml_file))
1488 self.assertEqual(config["verbose"], 1)
1489 self.assertEqual(config["check"], "no")
1490 self.assertEqual(config["diff"], "y")
1491 self.assertEqual(config["color"], True)
1492 self.assertEqual(config["line_length"], 79)
1493 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1494 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1495 self.assertEqual(config["exclude"], r"\.pyi?$")
1496 self.assertEqual(config["include"], r"\.py?$")
1498 def test_parse_pyproject_toml_project_metadata(self) -> None:
1499 for test_toml, expected in [
1500 ("only_black_pyproject.toml", ["py310"]),
1501 ("only_metadata_pyproject.toml", ["py37", "py38", "py39", "py310"]),
1502 ("neither_pyproject.toml", None),
1503 ("both_pyproject.toml", ["py310"]),
1505 test_toml_file = THIS_DIR / "data" / "project_metadata" / test_toml
1506 config = black.parse_pyproject_toml(str(test_toml_file))
1507 self.assertEqual(config.get("target_version"), expected)
1509 def test_infer_target_version(self) -> None:
1510 for version, expected in [
1511 ("3.6", [TargetVersion.PY36]),
1512 ("3.11.0rc1", [TargetVersion.PY311]),
1513 (">=3.10", [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312]),
1516 [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
1518 ("<3.6", [TargetVersion.PY33, TargetVersion.PY34, TargetVersion.PY35]),
1519 (">3.7,<3.10", [TargetVersion.PY38, TargetVersion.PY39]),
1522 [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
1525 "> 3.9.4, != 3.10.3",
1528 TargetVersion.PY310,
1529 TargetVersion.PY311,
1530 TargetVersion.PY312,
1541 TargetVersion.PY310,
1542 TargetVersion.PY311,
1543 TargetVersion.PY312,
1556 TargetVersion.PY310,
1557 TargetVersion.PY311,
1558 TargetVersion.PY312,
1561 ("==3.8.*", [TargetVersion.PY38]),
1565 ("==invalid", None),
1566 (">3.9,!=invalid", None),
1571 (">3.10,<3.11", None),
1573 test_toml = {"project": {"requires-python": version}}
1574 result = black.files.infer_target_version(test_toml)
1575 self.assertEqual(result, expected)
1577 def test_read_pyproject_toml(self) -> None:
1578 test_toml_file = THIS_DIR / "test.toml"
1579 fake_ctx = FakeContext()
1580 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1581 config = fake_ctx.default_map
1582 self.assertEqual(config["verbose"], "1")
1583 self.assertEqual(config["check"], "no")
1584 self.assertEqual(config["diff"], "y")
1585 self.assertEqual(config["color"], "True")
1586 self.assertEqual(config["line_length"], "79")
1587 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1588 self.assertEqual(config["exclude"], r"\.pyi?$")
1589 self.assertEqual(config["include"], r"\.py?$")
1591 def test_read_pyproject_toml_from_stdin(self) -> None:
1592 with TemporaryDirectory() as workspace:
1593 root = Path(workspace)
1595 src_dir = root / "src"
1598 src_pyproject = src_dir / "pyproject.toml"
1599 src_pyproject.touch()
1601 test_toml_content = (THIS_DIR / "test.toml").read_text(encoding="utf-8")
1602 src_pyproject.write_text(test_toml_content, encoding="utf-8")
1604 src_python = src_dir / "foo.py"
1607 fake_ctx = FakeContext()
1608 fake_ctx.params["src"] = ("-",)
1609 fake_ctx.params["stdin_filename"] = str(src_python)
1611 with change_directory(root):
1612 black.read_pyproject_toml(fake_ctx, FakeParameter(), None)
1614 config = fake_ctx.default_map
1615 self.assertEqual(config["verbose"], "1")
1616 self.assertEqual(config["check"], "no")
1617 self.assertEqual(config["diff"], "y")
1618 self.assertEqual(config["color"], "True")
1619 self.assertEqual(config["line_length"], "79")
1620 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1621 self.assertEqual(config["exclude"], r"\.pyi?$")
1622 self.assertEqual(config["include"], r"\.py?$")
1624 @pytest.mark.incompatible_with_mypyc
1625 def test_find_project_root(self) -> None:
1626 with TemporaryDirectory() as workspace:
1627 root = Path(workspace)
1628 test_dir = root / "test"
1631 src_dir = root / "src"
1634 root_pyproject = root / "pyproject.toml"
1635 root_pyproject.touch()
1636 src_pyproject = src_dir / "pyproject.toml"
1637 src_pyproject.touch()
1638 src_python = src_dir / "foo.py"
1642 black.find_project_root((src_dir, test_dir)),
1643 (root.resolve(), "pyproject.toml"),
1646 black.find_project_root((src_dir,)),
1647 (src_dir.resolve(), "pyproject.toml"),
1650 black.find_project_root((src_python,)),
1651 (src_dir.resolve(), "pyproject.toml"),
1654 with change_directory(test_dir):
1656 black.find_project_root(("-",), stdin_filename="../src/a.py"),
1657 (src_dir.resolve(), "pyproject.toml"),
1661 "black.files.find_user_pyproject_toml",
1663 def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
1664 find_user_pyproject_toml.side_effect = RuntimeError()
1666 with redirect_stderr(io.StringIO()) as stderr:
1667 result = black.files.find_pyproject_toml(
1668 path_search_start=(str(Path.cwd().root),)
1671 assert result is None
1672 err = stderr.getvalue()
1673 assert "Ignoring user configuration" in err
1676 "black.files.find_user_pyproject_toml",
1677 black.files.find_user_pyproject_toml.__wrapped__,
1679 def test_find_user_pyproject_toml_linux(self) -> None:
1680 if system() == "Windows":
1683 # Test if XDG_CONFIG_HOME is checked
1684 with TemporaryDirectory() as workspace:
1685 tmp_user_config = Path(workspace) / "black"
1686 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1688 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1691 # Test fallback for XDG_CONFIG_HOME
1692 with patch.dict("os.environ"):
1693 os.environ.pop("XDG_CONFIG_HOME", None)
1694 fallback_user_config = Path("~/.config").expanduser() / "black"
1696 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1699 def test_find_user_pyproject_toml_windows(self) -> None:
1700 if system() != "Windows":
1703 user_config_path = Path.home() / ".black"
1705 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1708 def test_bpo_33660_workaround(self) -> None:
1709 if system() == "Windows":
1712 # https://bugs.python.org/issue33660
1714 with change_directory(root):
1715 path = Path("workspace") / "project"
1716 report = black.Report(verbose=True)
1717 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1718 self.assertEqual(normalized_path, "workspace/project")
1720 def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
1721 if system() != "Windows":
1724 with TemporaryDirectory() as workspace:
1725 root = Path(workspace)
1726 junction_dir = root / "junction"
1727 junction_target_outside_of_root = root / ".."
1728 os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
1730 report = black.Report(verbose=True)
1731 normalized_path = black.normalize_path_maybe_ignore(
1732 junction_dir, root, report
1734 # Manually delete for Python < 3.8
1735 os.system(f"rmdir {junction_dir}")
1737 self.assertEqual(normalized_path, None)
1739 def test_newline_comment_interaction(self) -> None:
1740 source = "class A:\\\r\n# type: ignore\n pass\n"
1741 output = black.format_str(source, mode=DEFAULT_MODE)
1742 black.assert_stable(source, output, mode=DEFAULT_MODE)
1744 def test_bpo_2142_workaround(self) -> None:
1745 # https://bugs.python.org/issue2142
1747 source, _ = read_data("miscellaneous", "missing_final_newline")
1748 # read_data adds a trailing newline
1749 source = source.rstrip()
1750 expected, _ = read_data("miscellaneous", "missing_final_newline.diff")
1751 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1752 diff_header = re.compile(
1753 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1754 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
1757 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1758 self.assertEqual(result.exit_code, 0)
1761 actual = result.output
1762 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1763 self.assertEqual(actual, expected)
1766 def compare_results(
1767 result: click.testing.Result, expected_value: str, expected_exit_code: int
1769 """Helper method to test the value and exit code of a click Result."""
1771 result.output == expected_value
1772 ), "The output did not match the expected value."
1773 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1775 def test_code_option(self) -> None:
1776 """Test the code option with no changes."""
1777 code = 'print("Hello world")\n'
1778 args = ["--code", code]
1779 result = CliRunner().invoke(black.main, args)
1781 self.compare_results(result, code, 0)
1783 def test_code_option_changed(self) -> None:
1784 """Test the code option when changes are required."""
1785 code = "print('hello world')"
1786 formatted = black.format_str(code, mode=DEFAULT_MODE)
1788 args = ["--code", code]
1789 result = CliRunner().invoke(black.main, args)
1791 self.compare_results(result, formatted, 0)
1793 def test_code_option_check(self) -> None:
1794 """Test the code option when check is passed."""
1795 args = ["--check", "--code", 'print("Hello world")\n']
1796 result = CliRunner().invoke(black.main, args)
1797 self.compare_results(result, "", 0)
1799 def test_code_option_check_changed(self) -> None:
1800 """Test the code option when changes are required, and check is passed."""
1801 args = ["--check", "--code", "print('hello world')"]
1802 result = CliRunner().invoke(black.main, args)
1803 self.compare_results(result, "", 1)
1805 def test_code_option_diff(self) -> None:
1806 """Test the code option when diff is passed."""
1807 code = "print('hello world')"
1808 formatted = black.format_str(code, mode=DEFAULT_MODE)
1809 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1811 args = ["--diff", "--code", code]
1812 result = CliRunner().invoke(black.main, args)
1814 # Remove time from diff
1815 output = DIFF_TIME.sub("", result.output)
1817 assert output == result_diff, "The output did not match the expected value."
1818 assert result.exit_code == 0, "The exit code is incorrect."
1820 def test_code_option_color_diff(self) -> None:
1821 """Test the code option when color and diff are passed."""
1822 code = "print('hello world')"
1823 formatted = black.format_str(code, mode=DEFAULT_MODE)
1825 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1826 result_diff = color_diff(result_diff)
1828 args = ["--diff", "--color", "--code", code]
1829 result = CliRunner().invoke(black.main, args)
1831 # Remove time from diff
1832 output = DIFF_TIME.sub("", result.output)
1834 assert output == result_diff, "The output did not match the expected value."
1835 assert result.exit_code == 0, "The exit code is incorrect."
1837 @pytest.mark.incompatible_with_mypyc
1838 def test_code_option_safe(self) -> None:
1839 """Test that the code option throws an error when the sanity checks fail."""
1840 # Patch black.assert_equivalent to ensure the sanity checks fail
1841 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1842 code = 'print("Hello world")'
1843 error_msg = f"{code}\nerror: cannot format <string>: \n"
1845 args = ["--safe", "--code", code]
1846 result = CliRunner().invoke(black.main, args)
1848 self.compare_results(result, error_msg, 123)
1850 def test_code_option_fast(self) -> None:
1851 """Test that the code option ignores errors when the sanity checks fail."""
1852 # Patch black.assert_equivalent to ensure the sanity checks fail
1853 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1854 code = 'print("Hello world")'
1855 formatted = black.format_str(code, mode=DEFAULT_MODE)
1857 args = ["--fast", "--code", code]
1858 result = CliRunner().invoke(black.main, args)
1860 self.compare_results(result, formatted, 0)
1862 @pytest.mark.incompatible_with_mypyc
1863 def test_code_option_config(self) -> None:
1865 Test that the code option finds the pyproject.toml in the current directory.
1867 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1868 args = ["--code", "print"]
1869 # This is the only directory known to contain a pyproject.toml
1870 with change_directory(PROJECT_ROOT):
1871 CliRunner().invoke(black.main, args)
1872 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1875 len(parse.mock_calls) >= 1
1876 ), "Expected config parse to be called with the current directory."
1878 _, call_args, _ = parse.mock_calls[0]
1880 call_args[0].lower() == str(pyproject_path).lower()
1881 ), "Incorrect config loaded."
1883 @pytest.mark.incompatible_with_mypyc
1884 def test_code_option_parent_config(self) -> None:
1886 Test that the code option finds the pyproject.toml in the parent directory.
1888 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1889 with change_directory(THIS_DIR):
1890 args = ["--code", "print"]
1891 CliRunner().invoke(black.main, args)
1893 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1895 len(parse.mock_calls) >= 1
1896 ), "Expected config parse to be called with the current directory."
1898 _, call_args, _ = parse.mock_calls[0]
1900 call_args[0].lower() == str(pyproject_path).lower()
1901 ), "Incorrect config loaded."
1903 def test_for_handled_unexpected_eof_error(self) -> None:
1905 Test that an unexpected EOF SyntaxError is nicely presented.
1907 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1908 black.lib2to3_parse("print(", {})
1910 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1912 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1913 with pytest.raises(AssertionError) as err:
1914 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1917 # Unfortunately the SyntaxError message has changed in newer versions so we
1918 # can't match it directly.
1919 err.match("invalid character")
1920 err.match(r"\(<unknown>, line 1\)")
1924 def test_get_cache_dir(
1927 monkeypatch: pytest.MonkeyPatch,
1929 # Create multiple cache directories
1930 workspace1 = tmp_path / "ws1"
1932 workspace2 = tmp_path / "ws2"
1935 # Force user_cache_dir to use the temporary directory for easier assertions
1936 patch_user_cache_dir = patch(
1937 target="black.cache.user_cache_dir",
1939 return_value=str(workspace1),
1942 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1943 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1944 with patch_user_cache_dir:
1945 assert get_cache_dir() == workspace1
1947 # If it is set, use the path provided in the env var.
1948 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1949 assert get_cache_dir() == workspace2
1951 def test_cache_broken_file(self) -> None:
1953 with cache_dir() as workspace:
1954 cache_file = get_cache_file(mode)
1955 cache_file.write_text("this is not a pickle", encoding="utf-8")
1956 assert black.Cache.read(mode).file_data == {}
1957 src = (workspace / "test.py").resolve()
1958 src.write_text("print('hello')", encoding="utf-8")
1959 invokeBlack([str(src)])
1960 cache = black.Cache.read(mode)
1961 assert not cache.is_changed(src)
1963 def test_cache_single_file_already_cached(self) -> None:
1965 with cache_dir() as workspace:
1966 src = (workspace / "test.py").resolve()
1967 src.write_text("print('hello')", encoding="utf-8")
1968 cache = black.Cache.read(mode)
1970 invokeBlack([str(src)])
1971 assert src.read_text(encoding="utf-8") == "print('hello')"
1974 def test_cache_multiple_files(self) -> None:
1976 with cache_dir() as workspace, patch(
1977 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
1979 one = (workspace / "one.py").resolve()
1980 one.write_text("print('hello')", encoding="utf-8")
1981 two = (workspace / "two.py").resolve()
1982 two.write_text("print('hello')", encoding="utf-8")
1983 cache = black.Cache.read(mode)
1985 invokeBlack([str(workspace)])
1986 assert one.read_text(encoding="utf-8") == "print('hello')"
1987 assert two.read_text(encoding="utf-8") == 'print("hello")\n'
1988 cache = black.Cache.read(mode)
1989 assert not cache.is_changed(one)
1990 assert not cache.is_changed(two)
1992 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1993 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1995 with cache_dir() as workspace:
1996 src = (workspace / "test.py").resolve()
1997 src.write_text("print('hello')", encoding="utf-8")
1998 with patch.object(black.Cache, "read") as read_cache, patch.object(
1999 black.Cache, "write"
2001 cmd = [str(src), "--diff"]
2003 cmd.append("--color")
2005 cache_file = get_cache_file(mode)
2006 assert cache_file.exists() is False
2007 read_cache.assert_called_once()
2008 write_cache.assert_not_called()
2010 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
2012 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
2013 with cache_dir() as workspace:
2014 for tag in range(0, 4):
2015 src = (workspace / f"test{tag}.py").resolve()
2016 src.write_text("print('hello')", encoding="utf-8")
2018 "black.concurrency.Manager", wraps=multiprocessing.Manager
2020 cmd = ["--diff", str(workspace)]
2022 cmd.append("--color")
2023 invokeBlack(cmd, exit_code=0)
2024 # this isn't quite doing what we want, but if it _isn't_
2025 # called then we cannot be using the lock it provides
2028 def test_no_cache_when_stdin(self) -> None:
2031 result = CliRunner().invoke(
2032 black.main, ["-"], input=BytesIO(b"print('hello')")
2034 assert not result.exit_code
2035 cache_file = get_cache_file(mode)
2036 assert not cache_file.exists()
2038 def test_read_cache_no_cachefile(self) -> None:
2041 assert black.Cache.read(mode).file_data == {}
2043 def test_write_cache_read_cache(self) -> None:
2045 with cache_dir() as workspace:
2046 src = (workspace / "test.py").resolve()
2048 write_cache = black.Cache.read(mode)
2049 write_cache.write([src])
2050 read_cache = black.Cache.read(mode)
2051 assert not read_cache.is_changed(src)
2053 def test_filter_cached(self) -> None:
2054 with TemporaryDirectory() as workspace:
2055 path = Path(workspace)
2056 uncached = (path / "uncached").resolve()
2057 cached = (path / "cached").resolve()
2058 cached_but_changed = (path / "changed").resolve()
2061 cached_but_changed.touch()
2062 cache = black.Cache.read(DEFAULT_MODE)
2064 orig_func = black.Cache.get_file_data
2066 def wrapped_func(path: Path) -> FileData:
2068 return orig_func(path)
2069 if path == cached_but_changed:
2070 return FileData(0.0, 0, "")
2071 raise AssertionError
2073 with patch.object(black.Cache, "get_file_data", side_effect=wrapped_func):
2074 cache.write([cached, cached_but_changed])
2075 todo, done = cache.filtered_cached({uncached, cached, cached_but_changed})
2076 assert todo == {uncached, cached_but_changed}
2077 assert done == {cached}
2079 def test_filter_cached_hash(self) -> None:
2080 with TemporaryDirectory() as workspace:
2081 path = Path(workspace)
2082 src = (path / "test.py").resolve()
2083 src.write_text("print('hello')", encoding="utf-8")
2085 cache = black.Cache.read(DEFAULT_MODE)
2087 cached_file_data = cache.file_data[str(src)]
2089 todo, done = cache.filtered_cached([src])
2090 assert todo == set()
2091 assert done == {src}
2092 assert cached_file_data.st_mtime == st.st_mtime
2095 cached_file_data = cache.file_data[str(src)] = FileData(
2096 cached_file_data.st_mtime - 1,
2097 cached_file_data.st_size,
2098 cached_file_data.hash,
2100 todo, done = cache.filtered_cached([src])
2101 assert todo == set()
2102 assert done == {src}
2103 assert cached_file_data.st_mtime < st.st_mtime
2104 assert cached_file_data.st_size == st.st_size
2105 assert cached_file_data.hash == black.Cache.hash_digest(src)
2108 src.write_text("print('hello world')", encoding="utf-8")
2110 todo, done = cache.filtered_cached([src])
2111 assert todo == {src}
2112 assert done == set()
2113 assert cached_file_data.st_mtime < new_st.st_mtime
2114 assert cached_file_data.st_size != new_st.st_size
2115 assert cached_file_data.hash != black.Cache.hash_digest(src)
2117 def test_write_cache_creates_directory_if_needed(self) -> None:
2119 with cache_dir(exists=False) as workspace:
2120 assert not workspace.exists()
2121 cache = black.Cache.read(mode)
2123 assert workspace.exists()
2126 def test_failed_formatting_does_not_get_cached(self) -> None:
2128 with cache_dir() as workspace, patch(
2129 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
2131 failing = (workspace / "failing.py").resolve()
2132 failing.write_text("not actually python", encoding="utf-8")
2133 clean = (workspace / "clean.py").resolve()
2134 clean.write_text('print("hello")\n', encoding="utf-8")
2135 invokeBlack([str(workspace)], exit_code=123)
2136 cache = black.Cache.read(mode)
2137 assert cache.is_changed(failing)
2138 assert not cache.is_changed(clean)
2140 def test_write_cache_write_fail(self) -> None:
2143 cache = black.Cache.read(mode)
2144 with patch.object(Path, "open") as mock:
2145 mock.side_effect = OSError
2148 def test_read_cache_line_lengths(self) -> None:
2150 short_mode = replace(DEFAULT_MODE, line_length=1)
2151 with cache_dir() as workspace:
2152 path = (workspace / "file.py").resolve()
2154 cache = black.Cache.read(mode)
2156 one = black.Cache.read(mode)
2157 assert not one.is_changed(path)
2158 two = black.Cache.read(short_mode)
2159 assert two.is_changed(path)
2162 def assert_collected_sources(
2163 src: Sequence[Union[str, Path]],
2164 expected: Sequence[Union[str, Path]],
2166 ctx: Optional[FakeContext] = None,
2167 exclude: Optional[str] = None,
2168 include: Optional[str] = None,
2169 extend_exclude: Optional[str] = None,
2170 force_exclude: Optional[str] = None,
2171 stdin_filename: Optional[str] = None,
2173 gs_src = tuple(str(Path(s)) for s in src)
2174 gs_expected = [Path(s) for s in expected]
2175 gs_exclude = None if exclude is None else compile_pattern(exclude)
2176 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
2177 gs_extend_exclude = (
2178 None if extend_exclude is None else compile_pattern(extend_exclude)
2180 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
2181 collected = black.get_sources(
2182 ctx=ctx or FakeContext(),
2188 extend_exclude=gs_extend_exclude,
2189 force_exclude=gs_force_exclude,
2190 report=black.Report(),
2191 stdin_filename=stdin_filename,
2193 assert sorted(collected) == sorted(gs_expected)
2196 class TestFileCollection:
2197 def test_include_exclude(self) -> None:
2198 path = THIS_DIR / "data" / "include_exclude_tests"
2201 Path(path / "b/dont_exclude/a.py"),
2202 Path(path / "b/dont_exclude/a.pyi"),
2204 assert_collected_sources(
2208 exclude=r"/exclude/|/\.definitely_exclude/",
2211 def test_gitignore_used_as_default(self) -> None:
2212 base = Path(DATA_DIR / "include_exclude_tests")
2214 base / "b/.definitely_exclude/a.py",
2215 base / "b/.definitely_exclude/a.pyi",
2219 ctx.obj["root"] = base
2220 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
2222 def test_gitignore_used_on_multiple_sources(self) -> None:
2223 root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
2225 root / "dir1" / "b.py",
2226 root / "dir2" / "b.py",
2229 ctx.obj["root"] = root
2230 src = [root / "dir1", root / "dir2"]
2231 assert_collected_sources(src, expected, ctx=ctx)
2233 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2234 def test_exclude_for_issue_1572(self) -> None:
2235 # Exclude shouldn't touch files that were explicitly given to Black through the
2236 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
2237 # https://github.com/psf/black/issues/1572
2238 path = DATA_DIR / "include_exclude_tests"
2239 src = [path / "b/exclude/a.py"]
2240 expected = [path / "b/exclude/a.py"]
2241 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2243 def test_gitignore_exclude(self) -> None:
2244 path = THIS_DIR / "data" / "include_exclude_tests"
2245 include = re.compile(r"\.pyi?$")
2246 exclude = re.compile(r"")
2247 report = black.Report()
2248 gitignore = PathSpec.from_lines(
2249 "gitwildmatch", ["exclude/", ".definitely_exclude"]
2251 sources: List[Path] = []
2253 Path(path / "b/dont_exclude/a.py"),
2254 Path(path / "b/dont_exclude/a.pyi"),
2256 this_abs = THIS_DIR.resolve()
2258 black.gen_python_files(
2271 assert sorted(expected) == sorted(sources)
2273 def test_nested_gitignore(self) -> None:
2274 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
2275 include = re.compile(r"\.pyi?$")
2276 exclude = re.compile(r"")
2277 root_gitignore = black.files.get_gitignore(path)
2278 report = black.Report()
2279 expected: List[Path] = [
2280 Path(path / "x.py"),
2281 Path(path / "root/b.py"),
2282 Path(path / "root/c.py"),
2283 Path(path / "root/child/c.py"),
2285 this_abs = THIS_DIR.resolve()
2287 black.gen_python_files(
2295 {path: root_gitignore},
2300 assert sorted(expected) == sorted(sources)
2302 def test_nested_gitignore_directly_in_source_directory(self) -> None:
2303 # https://github.com/psf/black/issues/2598
2304 path = Path(DATA_DIR / "nested_gitignore_tests")
2305 src = Path(path / "root" / "child")
2306 expected = [src / "a.py", src / "c.py"]
2307 assert_collected_sources([src], expected)
2309 def test_invalid_gitignore(self) -> None:
2310 path = THIS_DIR / "data" / "invalid_gitignore_tests"
2311 empty_config = path / "pyproject.toml"
2312 result = BlackRunner().invoke(
2313 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2315 assert result.exit_code == 1
2316 assert result.stderr_bytes is not None
2318 gitignore = path / ".gitignore"
2319 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2321 def test_invalid_nested_gitignore(self) -> None:
2322 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
2323 empty_config = path / "pyproject.toml"
2324 result = BlackRunner().invoke(
2325 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2327 assert result.exit_code == 1
2328 assert result.stderr_bytes is not None
2330 gitignore = path / "a" / ".gitignore"
2331 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2333 def test_gitignore_that_ignores_subfolders(self) -> None:
2334 # If gitignore with */* is in root
2335 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir")
2336 expected = [root / "b.py"]
2338 ctx.obj["root"] = root
2339 assert_collected_sources([root], expected, ctx=ctx)
2341 # If .gitignore with */* is nested
2342 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2345 root / "subdir" / "b.py",
2348 ctx.obj["root"] = root
2349 assert_collected_sources([root], expected, ctx=ctx)
2351 # If command is executed from outer dir
2352 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2353 target = root / "subdir"
2354 expected = [target / "b.py"]
2356 ctx.obj["root"] = root
2357 assert_collected_sources([target], expected, ctx=ctx)
2359 def test_empty_include(self) -> None:
2360 path = DATA_DIR / "include_exclude_tests"
2363 Path(path / "b/exclude/a.pie"),
2364 Path(path / "b/exclude/a.py"),
2365 Path(path / "b/exclude/a.pyi"),
2366 Path(path / "b/dont_exclude/a.pie"),
2367 Path(path / "b/dont_exclude/a.py"),
2368 Path(path / "b/dont_exclude/a.pyi"),
2369 Path(path / "b/.definitely_exclude/a.pie"),
2370 Path(path / "b/.definitely_exclude/a.py"),
2371 Path(path / "b/.definitely_exclude/a.pyi"),
2372 Path(path / ".gitignore"),
2373 Path(path / "pyproject.toml"),
2375 # Setting exclude explicitly to an empty string to block .gitignore usage.
2376 assert_collected_sources(src, expected, include="", exclude="")
2378 def test_extend_exclude(self) -> None:
2379 path = DATA_DIR / "include_exclude_tests"
2382 Path(path / "b/exclude/a.py"),
2383 Path(path / "b/dont_exclude/a.py"),
2385 assert_collected_sources(
2386 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
2389 @pytest.mark.incompatible_with_mypyc
2390 def test_symlink_out_of_root_directory(self) -> None:
2392 root = THIS_DIR.resolve()
2394 include = re.compile(black.DEFAULT_INCLUDES)
2395 exclude = re.compile(black.DEFAULT_EXCLUDES)
2396 report = black.Report()
2397 gitignore = PathSpec.from_lines("gitwildmatch", [])
2398 # `child` should behave like a symlink which resolved path is clearly
2399 # outside of the `root` directory.
2400 path.iterdir.return_value = [child]
2401 child.resolve.return_value = Path("/a/b/c")
2402 child.as_posix.return_value = "/a/b/c"
2405 black.gen_python_files(
2418 except ValueError as ve:
2419 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
2420 path.iterdir.assert_called_once()
2421 child.resolve.assert_called_once()
2423 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2424 def test_get_sources_with_stdin(self) -> None:
2427 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2429 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2430 def test_get_sources_with_stdin_filename(self) -> None:
2432 stdin_filename = str(THIS_DIR / "data/collections.py")
2433 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2434 assert_collected_sources(
2437 exclude=r"/exclude/a\.py",
2438 stdin_filename=stdin_filename,
2441 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2442 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2443 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2444 # file being passed directly. This is the same as
2445 # test_exclude_for_issue_1572
2446 path = DATA_DIR / "include_exclude_tests"
2448 stdin_filename = str(path / "b/exclude/a.py")
2449 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2450 assert_collected_sources(
2453 exclude=r"/exclude/|a\.py",
2454 stdin_filename=stdin_filename,
2457 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2458 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2459 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2460 # file being passed directly. This is the same as
2461 # test_exclude_for_issue_1572
2463 path = THIS_DIR / "data" / "include_exclude_tests"
2464 stdin_filename = str(path / "b/exclude/a.py")
2465 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2466 assert_collected_sources(
2469 extend_exclude=r"/exclude/|a\.py",
2470 stdin_filename=stdin_filename,
2473 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2474 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2475 # Force exclude should exclude the file when passing it through
2477 path = THIS_DIR / "data" / "include_exclude_tests"
2478 stdin_filename = str(path / "b/exclude/a.py")
2479 assert_collected_sources(
2482 force_exclude=r"/exclude/|a\.py",
2483 stdin_filename=stdin_filename,
2488 with open(black.__file__, "r", encoding="utf-8") as _bf:
2489 black_source_lines = _bf.readlines()
2490 except UnicodeDecodeError:
2491 if not black.COMPILED:
2496 frame: types.FrameType, event: str, arg: Any
2497 ) -> Callable[[types.FrameType, str, Any], Any]:
2498 """Show function calls `from black/__init__.py` as they happen.
2500 Register this with `sys.settrace()` in a test you're debugging.
2505 stack = len(inspect.stack()) - 19
2507 filename = frame.f_code.co_filename
2508 lineno = frame.f_lineno
2509 func_sig_lineno = lineno - 1
2510 funcname = black_source_lines[func_sig_lineno].strip()
2511 while funcname.startswith("@"):
2512 func_sig_lineno += 1
2513 funcname = black_source_lines[func_sig_lineno].strip()
2514 if "black/__init__.py" in filename:
2515 print(f"{' ' * stack}{lineno}:{funcname}")