All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
13 from concurrent.futures import ThreadPoolExecutor
14 from contextlib import contextmanager, redirect_stderr
15 from dataclasses import replace
16 from io import BytesIO
17 from pathlib import Path
18 from platform import system
19 from tempfile import TemporaryDirectory
32 from unittest.mock import MagicMock, patch
36 from click import unstyle
37 from click.testing import CliRunner
38 from pathspec import PathSpec
42 from black import Feature, TargetVersion
43 from black import re_compile_maybe_verbose as compile_pattern
44 from black.cache import get_cache_dir, get_cache_file
45 from black.debug import DebugVisitor
46 from black.output import color_diff, diff
47 from black.report import Report
49 # Import other test classes
50 from tests.util import (
68 THIS_FILE = Path(__file__)
69 EMPTY_CONFIG = THIS_DIR / "data" / "empty_pyproject.toml"
70 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
71 DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
72 DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
76 # Match the time output in a diff, but nothing else
77 DIFF_TIME = re.compile(r"\t[\d\-:+\. ]+")
81 def cache_dir(exists: bool = True) -> Iterator[Path]:
82 with TemporaryDirectory() as workspace:
83 cache_dir = Path(workspace)
85 cache_dir = cache_dir / "new"
86 with patch("black.cache.CACHE_DIR", cache_dir):
91 def event_loop() -> Iterator[None]:
92 policy = asyncio.get_event_loop_policy()
93 loop = policy.new_event_loop()
94 asyncio.set_event_loop(loop)
102 class FakeContext(click.Context):
103 """A fake click Context for when calling functions that need it."""
105 def __init__(self) -> None:
106 self.default_map: Dict[str, Any] = {}
107 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.read_cache(pyi_mode)
1125 self.assertIn(str(path), pyi_cache)
1126 normal_cache = black.read_cache(DEFAULT_MODE)
1127 self.assertNotIn(str(path), normal_cache)
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.read_cache(pyi_mode)
1150 normal_cache = black.read_cache(reg_mode)
1152 self.assertIn(str(path), pyi_cache)
1153 self.assertNotIn(str(path), normal_cache)
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.read_cache(py36_mode)
1175 self.assertIn(str(path), py36_cache)
1176 normal_cache = black.read_cache(reg_mode)
1177 self.assertNotIn(str(path), normal_cache)
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.read_cache(py36_mode)
1198 normal_cache = black.read_cache(reg_mode)
1200 self.assertIn(str(path), pyi_cache)
1201 self.assertNotIn(str(path), normal_cache)
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.read_cache(mode) == {}
1957 src = (workspace / "test.py").resolve()
1958 src.write_text("print('hello')", encoding="utf-8")
1959 invokeBlack([str(src)])
1960 cache = black.read_cache(mode)
1961 assert str(src) in cache
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 black.write_cache({}, [src], mode)
1969 invokeBlack([str(src)])
1970 assert src.read_text(encoding="utf-8") == "print('hello')"
1973 def test_cache_multiple_files(self) -> None:
1975 with cache_dir() as workspace, patch(
1976 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
1978 one = (workspace / "one.py").resolve()
1979 one.write_text("print('hello')", encoding="utf-8")
1980 two = (workspace / "two.py").resolve()
1981 two.write_text("print('hello')", encoding="utf-8")
1982 black.write_cache({}, [one], 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.read_cache(mode)
1987 assert str(one) in cache
1988 assert str(two) in cache
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("black.read_cache") as read_cache, patch(
1999 cmd = [str(src), "--diff"]
2001 cmd.append("--color")
2003 cache_file = get_cache_file(mode)
2004 assert cache_file.exists() is False
2005 write_cache.assert_not_called()
2006 read_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.read_cache(mode) == {}
2041 def test_write_cache_read_cache(self) -> None:
2043 with cache_dir() as workspace:
2044 src = (workspace / "test.py").resolve()
2046 black.write_cache({}, [src], mode)
2047 cache = black.read_cache(mode)
2048 assert str(src) in cache
2049 assert cache[str(src)] == black.get_cache_info(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()
2061 str(cached): black.get_cache_info(cached),
2062 str(cached_but_changed): (0.0, 0),
2064 todo, done = black.cache.filter_cached(
2065 cache, {uncached, cached, cached_but_changed}
2067 assert todo == {uncached, cached_but_changed}
2068 assert done == {cached}
2070 def test_write_cache_creates_directory_if_needed(self) -> None:
2072 with cache_dir(exists=False) as workspace:
2073 assert not workspace.exists()
2074 black.write_cache({}, [], mode)
2075 assert workspace.exists()
2078 def test_failed_formatting_does_not_get_cached(self) -> None:
2080 with cache_dir() as workspace, patch(
2081 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
2083 failing = (workspace / "failing.py").resolve()
2084 failing.write_text("not actually python", encoding="utf-8")
2085 clean = (workspace / "clean.py").resolve()
2086 clean.write_text('print("hello")\n', encoding="utf-8")
2087 invokeBlack([str(workspace)], exit_code=123)
2088 cache = black.read_cache(mode)
2089 assert str(failing) not in cache
2090 assert str(clean) in cache
2092 def test_write_cache_write_fail(self) -> None:
2094 with cache_dir(), patch.object(Path, "open") as mock:
2095 mock.side_effect = OSError
2096 black.write_cache({}, [], mode)
2098 def test_read_cache_line_lengths(self) -> None:
2100 short_mode = replace(DEFAULT_MODE, line_length=1)
2101 with cache_dir() as workspace:
2102 path = (workspace / "file.py").resolve()
2104 black.write_cache({}, [path], mode)
2105 one = black.read_cache(mode)
2106 assert str(path) in one
2107 two = black.read_cache(short_mode)
2108 assert str(path) not in two
2111 def assert_collected_sources(
2112 src: Sequence[Union[str, Path]],
2113 expected: Sequence[Union[str, Path]],
2115 ctx: Optional[FakeContext] = None,
2116 exclude: Optional[str] = None,
2117 include: Optional[str] = None,
2118 extend_exclude: Optional[str] = None,
2119 force_exclude: Optional[str] = None,
2120 stdin_filename: Optional[str] = None,
2122 gs_src = tuple(str(Path(s)) for s in src)
2123 gs_expected = [Path(s) for s in expected]
2124 gs_exclude = None if exclude is None else compile_pattern(exclude)
2125 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
2126 gs_extend_exclude = (
2127 None if extend_exclude is None else compile_pattern(extend_exclude)
2129 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
2130 collected = black.get_sources(
2131 ctx=ctx or FakeContext(),
2137 extend_exclude=gs_extend_exclude,
2138 force_exclude=gs_force_exclude,
2139 report=black.Report(),
2140 stdin_filename=stdin_filename,
2142 assert sorted(collected) == sorted(gs_expected)
2145 class TestFileCollection:
2146 def test_include_exclude(self) -> None:
2147 path = THIS_DIR / "data" / "include_exclude_tests"
2150 Path(path / "b/dont_exclude/a.py"),
2151 Path(path / "b/dont_exclude/a.pyi"),
2153 assert_collected_sources(
2157 exclude=r"/exclude/|/\.definitely_exclude/",
2160 def test_gitignore_used_as_default(self) -> None:
2161 base = Path(DATA_DIR / "include_exclude_tests")
2163 base / "b/.definitely_exclude/a.py",
2164 base / "b/.definitely_exclude/a.pyi",
2168 ctx.obj["root"] = base
2169 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
2171 def test_gitignore_used_on_multiple_sources(self) -> None:
2172 root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
2174 root / "dir1" / "b.py",
2175 root / "dir2" / "b.py",
2178 ctx.obj["root"] = root
2179 src = [root / "dir1", root / "dir2"]
2180 assert_collected_sources(src, expected, ctx=ctx)
2182 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2183 def test_exclude_for_issue_1572(self) -> None:
2184 # Exclude shouldn't touch files that were explicitly given to Black through the
2185 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
2186 # https://github.com/psf/black/issues/1572
2187 path = DATA_DIR / "include_exclude_tests"
2188 src = [path / "b/exclude/a.py"]
2189 expected = [path / "b/exclude/a.py"]
2190 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2192 def test_gitignore_exclude(self) -> None:
2193 path = THIS_DIR / "data" / "include_exclude_tests"
2194 include = re.compile(r"\.pyi?$")
2195 exclude = re.compile(r"")
2196 report = black.Report()
2197 gitignore = PathSpec.from_lines(
2198 "gitwildmatch", ["exclude/", ".definitely_exclude"]
2200 sources: List[Path] = []
2202 Path(path / "b/dont_exclude/a.py"),
2203 Path(path / "b/dont_exclude/a.pyi"),
2205 this_abs = THIS_DIR.resolve()
2207 black.gen_python_files(
2220 assert sorted(expected) == sorted(sources)
2222 def test_nested_gitignore(self) -> None:
2223 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
2224 include = re.compile(r"\.pyi?$")
2225 exclude = re.compile(r"")
2226 root_gitignore = black.files.get_gitignore(path)
2227 report = black.Report()
2228 expected: List[Path] = [
2229 Path(path / "x.py"),
2230 Path(path / "root/b.py"),
2231 Path(path / "root/c.py"),
2232 Path(path / "root/child/c.py"),
2234 this_abs = THIS_DIR.resolve()
2236 black.gen_python_files(
2244 {path: root_gitignore},
2249 assert sorted(expected) == sorted(sources)
2251 def test_nested_gitignore_directly_in_source_directory(self) -> None:
2252 # https://github.com/psf/black/issues/2598
2253 path = Path(DATA_DIR / "nested_gitignore_tests")
2254 src = Path(path / "root" / "child")
2255 expected = [src / "a.py", src / "c.py"]
2256 assert_collected_sources([src], expected)
2258 def test_invalid_gitignore(self) -> None:
2259 path = THIS_DIR / "data" / "invalid_gitignore_tests"
2260 empty_config = path / "pyproject.toml"
2261 result = BlackRunner().invoke(
2262 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2264 assert result.exit_code == 1
2265 assert result.stderr_bytes is not None
2267 gitignore = path / ".gitignore"
2268 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2270 def test_invalid_nested_gitignore(self) -> None:
2271 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
2272 empty_config = path / "pyproject.toml"
2273 result = BlackRunner().invoke(
2274 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2276 assert result.exit_code == 1
2277 assert result.stderr_bytes is not None
2279 gitignore = path / "a" / ".gitignore"
2280 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2282 def test_gitignore_that_ignores_subfolders(self) -> None:
2283 # If gitignore with */* is in root
2284 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir")
2285 expected = [root / "b.py"]
2287 ctx.obj["root"] = root
2288 assert_collected_sources([root], expected, ctx=ctx)
2290 # If .gitignore with */* is nested
2291 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2294 root / "subdir" / "b.py",
2297 ctx.obj["root"] = root
2298 assert_collected_sources([root], expected, ctx=ctx)
2300 # If command is executed from outer dir
2301 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2302 target = root / "subdir"
2303 expected = [target / "b.py"]
2305 ctx.obj["root"] = root
2306 assert_collected_sources([target], expected, ctx=ctx)
2308 def test_empty_include(self) -> None:
2309 path = DATA_DIR / "include_exclude_tests"
2312 Path(path / "b/exclude/a.pie"),
2313 Path(path / "b/exclude/a.py"),
2314 Path(path / "b/exclude/a.pyi"),
2315 Path(path / "b/dont_exclude/a.pie"),
2316 Path(path / "b/dont_exclude/a.py"),
2317 Path(path / "b/dont_exclude/a.pyi"),
2318 Path(path / "b/.definitely_exclude/a.pie"),
2319 Path(path / "b/.definitely_exclude/a.py"),
2320 Path(path / "b/.definitely_exclude/a.pyi"),
2321 Path(path / ".gitignore"),
2322 Path(path / "pyproject.toml"),
2324 # Setting exclude explicitly to an empty string to block .gitignore usage.
2325 assert_collected_sources(src, expected, include="", exclude="")
2327 def test_extend_exclude(self) -> None:
2328 path = DATA_DIR / "include_exclude_tests"
2331 Path(path / "b/exclude/a.py"),
2332 Path(path / "b/dont_exclude/a.py"),
2334 assert_collected_sources(
2335 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
2338 @pytest.mark.incompatible_with_mypyc
2339 def test_symlink_out_of_root_directory(self) -> None:
2341 root = THIS_DIR.resolve()
2343 include = re.compile(black.DEFAULT_INCLUDES)
2344 exclude = re.compile(black.DEFAULT_EXCLUDES)
2345 report = black.Report()
2346 gitignore = PathSpec.from_lines("gitwildmatch", [])
2347 # `child` should behave like a symlink which resolved path is clearly
2348 # outside of the `root` directory.
2349 path.iterdir.return_value = [child]
2350 child.resolve.return_value = Path("/a/b/c")
2351 child.as_posix.return_value = "/a/b/c"
2354 black.gen_python_files(
2367 except ValueError as ve:
2368 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
2369 path.iterdir.assert_called_once()
2370 child.resolve.assert_called_once()
2372 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2373 def test_get_sources_with_stdin(self) -> None:
2376 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2378 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2379 def test_get_sources_with_stdin_filename(self) -> None:
2381 stdin_filename = str(THIS_DIR / "data/collections.py")
2382 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2383 assert_collected_sources(
2386 exclude=r"/exclude/a\.py",
2387 stdin_filename=stdin_filename,
2390 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2391 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2392 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2393 # file being passed directly. This is the same as
2394 # test_exclude_for_issue_1572
2395 path = DATA_DIR / "include_exclude_tests"
2397 stdin_filename = str(path / "b/exclude/a.py")
2398 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2399 assert_collected_sources(
2402 exclude=r"/exclude/|a\.py",
2403 stdin_filename=stdin_filename,
2406 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2407 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2408 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2409 # file being passed directly. This is the same as
2410 # test_exclude_for_issue_1572
2412 path = THIS_DIR / "data" / "include_exclude_tests"
2413 stdin_filename = str(path / "b/exclude/a.py")
2414 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2415 assert_collected_sources(
2418 extend_exclude=r"/exclude/|a\.py",
2419 stdin_filename=stdin_filename,
2422 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2423 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2424 # Force exclude should exclude the file when passing it through
2426 path = THIS_DIR / "data" / "include_exclude_tests"
2427 stdin_filename = str(path / "b/exclude/a.py")
2428 assert_collected_sources(
2431 force_exclude=r"/exclude/|a\.py",
2432 stdin_filename=stdin_filename,
2437 with open(black.__file__, "r", encoding="utf-8") as _bf:
2438 black_source_lines = _bf.readlines()
2439 except UnicodeDecodeError:
2440 if not black.COMPILED:
2445 frame: types.FrameType, event: str, arg: Any
2446 ) -> Callable[[types.FrameType, str, Any], Any]:
2447 """Show function calls `from black/__init__.py` as they happen.
2449 Register this with `sys.settrace()` in a test you're debugging.
2454 stack = len(inspect.stack()) - 19
2456 filename = frame.f_code.co_filename
2457 lineno = frame.f_lineno
2458 func_sig_lineno = lineno - 1
2459 funcname = black_source_lines[func_sig_lineno].strip()
2460 while funcname.startswith("@"):
2461 func_sig_lineno += 1
2462 funcname = black_source_lines[func_sig_lineno].strip()
2463 if "black/__init__.py" in filename:
2464 print(f"{' ' * stack}{lineno}:{funcname}")