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.
12 from concurrent.futures import ThreadPoolExecutor
13 from contextlib import contextmanager, redirect_stderr
14 from dataclasses import replace
15 from io import BytesIO
16 from pathlib import Path
17 from platform import system
18 from tempfile import TemporaryDirectory
31 from unittest.mock import MagicMock, patch
35 from click import unstyle
36 from click.testing import CliRunner
37 from pathspec import PathSpec
41 from black import Feature, TargetVersion
42 from black import re_compile_maybe_verbose as compile_pattern
43 from black.cache import FileData, get_cache_dir, get_cache_file
44 from black.debug import DebugVisitor
45 from black.output import color_diff, diff
46 from black.report import Report
48 # Import other test classes
49 from tests.util import (
67 THIS_FILE = Path(__file__)
68 EMPTY_CONFIG = THIS_DIR / "data" / "empty_pyproject.toml"
69 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
70 DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
71 DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
75 # Match the time output in a diff, but nothing else
76 DIFF_TIME = re.compile(r"\t[\d\-:+\. ]+")
80 def cache_dir(exists: bool = True) -> Iterator[Path]:
81 with TemporaryDirectory() as workspace:
82 cache_dir = Path(workspace)
84 cache_dir = cache_dir / "new"
85 with patch("black.cache.CACHE_DIR", cache_dir):
90 def event_loop() -> Iterator[None]:
91 policy = asyncio.get_event_loop_policy()
92 loop = policy.new_event_loop()
93 asyncio.set_event_loop(loop)
101 class FakeContext(click.Context):
102 """A fake click Context for when calling functions that need it."""
104 def __init__(self) -> None:
105 self.default_map: Dict[str, Any] = {}
106 self.params: Dict[str, Any] = {}
107 # Dummy root, since most of the tests don't care about it
108 self.obj: Dict[str, Any] = {"root": PROJECT_ROOT}
111 class FakeParameter(click.Parameter):
112 """A fake click Parameter for when calling functions that need it."""
114 def __init__(self) -> None:
118 class BlackRunner(CliRunner):
119 """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
121 def __init__(self) -> None:
122 super().__init__(mix_stderr=False)
126 args: List[str], exit_code: int = 0, ignore_config: bool = True
128 runner = BlackRunner()
130 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
131 result = runner.invoke(black.main, args, catch_exceptions=False)
132 assert result.stdout_bytes is not None
133 assert result.stderr_bytes is not None
135 f"Failed with args: {args}\n"
136 f"stdout: {result.stdout_bytes.decode()!r}\n"
137 f"stderr: {result.stderr_bytes.decode()!r}\n"
138 f"exception: {result.exception}"
140 assert result.exit_code == exit_code, msg
143 class BlackTestCase(BlackBaseTestCase):
144 invokeBlack = staticmethod(invokeBlack)
146 def test_empty_ff(self) -> None:
148 tmp_file = Path(black.dump_to_file())
150 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
151 actual = tmp_file.read_text(encoding="utf-8")
154 self.assertFormatEqual(expected, actual)
156 @patch("black.dump_to_file", dump_to_stderr)
157 def test_one_empty_line(self) -> None:
158 mode = black.Mode(preview=True)
159 for nl in ["\n", "\r\n"]:
160 source = expected = nl
161 assert_format(source, expected, mode=mode)
163 def test_one_empty_line_ff(self) -> None:
164 mode = black.Mode(preview=True)
165 for nl in ["\n", "\r\n"]:
167 tmp_file = Path(black.dump_to_file(nl))
168 if system() == "Windows":
169 # Writing files in text mode automatically uses the system newline,
170 # but in this case we don't want this for testing reasons. See:
171 # https://github.com/psf/black/pull/3348
172 with open(tmp_file, "wb") as f:
173 f.write(nl.encode("utf-8"))
176 ff(tmp_file, mode=mode, write_back=black.WriteBack.YES)
178 with open(tmp_file, "rb") as f:
179 actual = f.read().decode("utf-8")
182 self.assertFormatEqual(expected, actual)
184 def test_experimental_string_processing_warns(self) -> None:
186 black.mode.Deprecated, black.Mode, experimental_string_processing=True
189 def test_piping(self) -> None:
190 _, source, expected = read_data_from_file(
191 PROJECT_ROOT / "src/black/__init__.py"
193 result = BlackRunner().invoke(
198 f"--line-length={black.DEFAULT_LINE_LENGTH}",
199 f"--config={EMPTY_CONFIG}",
201 input=BytesIO(source.encode("utf-8")),
203 self.assertEqual(result.exit_code, 0)
204 self.assertFormatEqual(expected, result.output)
205 if source != result.output:
206 black.assert_equivalent(source, result.output)
207 black.assert_stable(source, result.output, DEFAULT_MODE)
209 def test_piping_diff(self) -> None:
210 diff_header = re.compile(
211 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d"
214 source, _ = read_data("cases", "expression.py")
215 expected, _ = read_data("cases", "expression.diff")
219 f"--line-length={black.DEFAULT_LINE_LENGTH}",
221 f"--config={EMPTY_CONFIG}",
223 result = BlackRunner().invoke(
224 black.main, args, input=BytesIO(source.encode("utf-8"))
226 self.assertEqual(result.exit_code, 0)
227 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
228 actual = actual.rstrip() + "\n" # the diff output has a trailing space
229 self.assertEqual(expected, actual)
231 def test_piping_diff_with_color(self) -> None:
232 source, _ = read_data("cases", "expression.py")
236 f"--line-length={black.DEFAULT_LINE_LENGTH}",
239 f"--config={EMPTY_CONFIG}",
241 result = BlackRunner().invoke(
242 black.main, args, input=BytesIO(source.encode("utf-8"))
244 actual = result.output
245 # Again, the contents are checked in a different test, so only look for colors.
246 self.assertIn("\033[1m", actual)
247 self.assertIn("\033[36m", actual)
248 self.assertIn("\033[32m", actual)
249 self.assertIn("\033[31m", actual)
250 self.assertIn("\033[0m", actual)
252 @patch("black.dump_to_file", dump_to_stderr)
253 def _test_wip(self) -> None:
254 source, expected = read_data("miscellaneous", "wip")
255 sys.settrace(tracefunc)
258 experimental_string_processing=False,
259 target_versions={black.TargetVersion.PY38},
261 actual = fs(source, mode=mode)
263 self.assertFormatEqual(expected, actual)
264 black.assert_equivalent(source, actual)
265 black.assert_stable(source, actual, black.FileMode())
267 def test_pep_572_version_detection(self) -> None:
268 source, _ = read_data("cases", "pep_572")
269 root = black.lib2to3_parse(source)
270 features = black.get_features_used(root)
271 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
272 versions = black.detect_target_versions(root)
273 self.assertIn(black.TargetVersion.PY38, versions)
275 def test_pep_695_version_detection(self) -> None:
276 for file in ("type_aliases", "type_params"):
277 source, _ = read_data("cases", file)
278 root = black.lib2to3_parse(source)
279 features = black.get_features_used(root)
280 self.assertIn(black.Feature.TYPE_PARAMS, features)
281 versions = black.detect_target_versions(root)
282 self.assertIn(black.TargetVersion.PY312, versions)
284 def test_expression_ff(self) -> None:
285 source, expected = read_data("cases", "expression.py")
286 tmp_file = Path(black.dump_to_file(source))
288 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
289 actual = tmp_file.read_text(encoding="utf-8")
292 self.assertFormatEqual(expected, actual)
293 with patch("black.dump_to_file", dump_to_stderr):
294 black.assert_equivalent(source, actual)
295 black.assert_stable(source, actual, DEFAULT_MODE)
297 def test_expression_diff(self) -> None:
298 source, _ = read_data("cases", "expression.py")
299 expected, _ = read_data("cases", "expression.diff")
300 tmp_file = Path(black.dump_to_file(source))
301 diff_header = re.compile(
302 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
303 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
306 result = BlackRunner().invoke(
307 black.main, ["--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
309 self.assertEqual(result.exit_code, 0)
312 actual = result.output
313 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
314 if expected != actual:
315 dump = black.dump_to_file(actual)
317 "Expected diff isn't equal to the actual. If you made changes to"
318 " expression.py and this is an anticipated difference, overwrite"
319 f" tests/data/expression.diff with {dump}"
321 self.assertEqual(expected, actual, msg)
323 def test_expression_diff_with_color(self) -> None:
324 source, _ = read_data("cases", "expression.py")
325 expected, _ = read_data("cases", "expression.diff")
326 tmp_file = Path(black.dump_to_file(source))
328 result = BlackRunner().invoke(
330 ["--diff", "--color", str(tmp_file), f"--config={EMPTY_CONFIG}"],
334 actual = result.output
335 # We check the contents of the diff in `test_expression_diff`. All
336 # we need to check here is that color codes exist in the result.
337 self.assertIn("\033[1m", actual)
338 self.assertIn("\033[36m", actual)
339 self.assertIn("\033[32m", actual)
340 self.assertIn("\033[31m", actual)
341 self.assertIn("\033[0m", actual)
343 def test_detect_pos_only_arguments(self) -> None:
344 source, _ = read_data("cases", "pep_570")
345 root = black.lib2to3_parse(source)
346 features = black.get_features_used(root)
347 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
348 versions = black.detect_target_versions(root)
349 self.assertIn(black.TargetVersion.PY38, versions)
351 def test_detect_debug_f_strings(self) -> None:
352 root = black.lib2to3_parse("""f"{x=}" """)
353 features = black.get_features_used(root)
354 self.assertIn(black.Feature.DEBUG_F_STRINGS, features)
355 versions = black.detect_target_versions(root)
356 self.assertIn(black.TargetVersion.PY38, versions)
358 root = black.lib2to3_parse(
359 """f"{x}"\nf'{"="}'\nf'{(x:=5)}'\nf'{f(a="3=")}'\nf'{x:=10}'\n"""
361 features = black.get_features_used(root)
362 self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
364 # We don't yet support feature version detection in nested f-strings
365 root = black.lib2to3_parse(
366 """f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """
368 features = black.get_features_used(root)
369 self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
371 @patch("black.dump_to_file", dump_to_stderr)
372 def test_string_quotes(self) -> None:
373 source, expected = read_data("miscellaneous", "string_quotes")
374 mode = black.Mode(preview=True)
375 assert_format(source, expected, mode)
376 mode = replace(mode, string_normalization=False)
377 not_normalized = fs(source, mode=mode)
378 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
379 black.assert_equivalent(source, not_normalized)
380 black.assert_stable(source, not_normalized, mode=mode)
382 def test_skip_source_first_line(self) -> None:
383 source, _ = read_data("miscellaneous", "invalid_header")
384 tmp_file = Path(black.dump_to_file(source))
385 # Full source should fail (invalid syntax at header)
386 self.invokeBlack([str(tmp_file), "--diff", "--check"], exit_code=123)
387 # So, skipping the first line should work
388 result = BlackRunner().invoke(
389 black.main, [str(tmp_file), "-x", f"--config={EMPTY_CONFIG}"]
391 self.assertEqual(result.exit_code, 0)
392 actual = tmp_file.read_text(encoding="utf-8")
393 self.assertFormatEqual(source, actual)
395 def test_skip_source_first_line_when_mixing_newlines(self) -> None:
396 code_mixing_newlines = b"Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n"
397 expected = b"Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n"
398 with TemporaryDirectory() as workspace:
399 test_file = Path(workspace) / "skip_header.py"
400 test_file.write_bytes(code_mixing_newlines)
401 mode = replace(DEFAULT_MODE, skip_source_first_line=True)
402 ff(test_file, mode=mode, write_back=black.WriteBack.YES)
403 self.assertEqual(test_file.read_bytes(), expected)
405 def test_skip_magic_trailing_comma(self) -> None:
406 source, _ = read_data("cases", "expression")
407 expected, _ = read_data(
408 "miscellaneous", "expression_skip_magic_trailing_comma.diff"
410 tmp_file = Path(black.dump_to_file(source))
411 diff_header = re.compile(
412 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
413 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
416 result = BlackRunner().invoke(
417 black.main, ["-C", "--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
419 self.assertEqual(result.exit_code, 0)
422 actual = result.output
423 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
424 actual = actual.rstrip() + "\n" # the diff output has a trailing space
425 if expected != actual:
426 dump = black.dump_to_file(actual)
428 "Expected diff isn't equal to the actual. If you made changes to"
429 " expression.py and this is an anticipated difference, overwrite"
430 " tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff"
433 self.assertEqual(expected, actual, msg)
435 @patch("black.dump_to_file", dump_to_stderr)
436 def test_async_as_identifier(self) -> None:
437 source_path = get_case_path("miscellaneous", "async_as_identifier")
438 _, source, expected = read_data_from_file(source_path)
440 self.assertFormatEqual(expected, actual)
441 major, minor = sys.version_info[:2]
442 if major < 3 or (major <= 3 and minor < 7):
443 black.assert_equivalent(source, actual)
444 black.assert_stable(source, actual, DEFAULT_MODE)
445 # ensure black can parse this when the target is 3.6
446 self.invokeBlack([str(source_path), "--target-version", "py36"])
447 # but not on 3.7, because async/await is no longer an identifier
448 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
450 @patch("black.dump_to_file", dump_to_stderr)
451 def test_python37(self) -> None:
452 source_path = get_case_path("cases", "python37")
453 _, source, expected = read_data_from_file(source_path)
455 self.assertFormatEqual(expected, actual)
456 major, minor = sys.version_info[:2]
457 if major > 3 or (major == 3 and minor >= 7):
458 black.assert_equivalent(source, actual)
459 black.assert_stable(source, actual, DEFAULT_MODE)
460 # ensure black can parse this when the target is 3.7
461 self.invokeBlack([str(source_path), "--target-version", "py37"])
462 # but not on 3.6, because we use async as a reserved keyword
463 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
465 def test_tab_comment_indentation(self) -> None:
466 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
467 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
468 self.assertFormatEqual(contents_spc, fs(contents_spc))
469 self.assertFormatEqual(contents_spc, fs(contents_tab))
471 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
472 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
473 self.assertFormatEqual(contents_spc, fs(contents_spc))
474 self.assertFormatEqual(contents_spc, fs(contents_tab))
476 # mixed tabs and spaces (valid Python 2 code)
477 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
478 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
479 self.assertFormatEqual(contents_spc, fs(contents_spc))
480 self.assertFormatEqual(contents_spc, fs(contents_tab))
482 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
483 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
484 self.assertFormatEqual(contents_spc, fs(contents_spc))
485 self.assertFormatEqual(contents_spc, fs(contents_tab))
487 def test_false_positive_symlink_output_issue_3384(self) -> None:
488 # Emulate the behavior when using the CLI (`black ./child --verbose`), which
489 # involves patching some `pathlib.Path` methods. In particular, `is_dir` is
490 # patched only on its first call: when checking if "./child" is a directory it
491 # should return True. The "./child" folder exists relative to the cwd when
492 # running from CLI, but fails when running the tests because cwd is different
493 project_root = Path(THIS_DIR / "data" / "nested_gitignore_tests")
494 working_directory = project_root / "root"
495 target_abspath = working_directory / "child"
496 target_contents = list(target_abspath.iterdir())
498 def mock_n_calls(responses: List[bool]) -> Callable[[], bool]:
499 def _mocked_calls() -> bool:
501 return responses.pop(0)
506 with patch("pathlib.Path.iterdir", return_value=target_contents), patch(
507 "pathlib.Path.cwd", return_value=working_directory
508 ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])):
509 # Note that the root folder (project_root) isn't the folder
510 # named "root" (aka working_directory)
511 report = MagicMock(verbose=True)
517 include=DEFAULT_INCLUDE,
525 mock_args[1].startswith("is a symbolic link that points outside")
526 for _, mock_args, _ in report.path_ignored.mock_calls
527 ), "A symbolic link was reported."
528 report.path_ignored.assert_called_once_with(
529 Path("root", "child", "b.py"), "matches a .gitignore file content"
532 def test_report_verbose(self) -> None:
533 report = Report(verbose=True)
537 def out(msg: str, **kwargs: Any) -> None:
538 out_lines.append(msg)
540 def err(msg: str, **kwargs: Any) -> None:
541 err_lines.append(msg)
543 with patch("black.output._out", out), patch("black.output._err", err):
544 report.done(Path("f1"), black.Changed.NO)
545 self.assertEqual(len(out_lines), 1)
546 self.assertEqual(len(err_lines), 0)
547 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
548 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
549 self.assertEqual(report.return_code, 0)
550 report.done(Path("f2"), black.Changed.YES)
551 self.assertEqual(len(out_lines), 2)
552 self.assertEqual(len(err_lines), 0)
553 self.assertEqual(out_lines[-1], "reformatted f2")
555 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
557 report.done(Path("f3"), black.Changed.CACHED)
558 self.assertEqual(len(out_lines), 3)
559 self.assertEqual(len(err_lines), 0)
561 out_lines[-1], "f3 wasn't modified on disk since last run."
564 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
566 self.assertEqual(report.return_code, 0)
568 self.assertEqual(report.return_code, 1)
570 report.failed(Path("e1"), "boom")
571 self.assertEqual(len(out_lines), 3)
572 self.assertEqual(len(err_lines), 1)
573 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
575 unstyle(str(report)),
576 "1 file reformatted, 2 files left unchanged, 1 file failed to"
579 self.assertEqual(report.return_code, 123)
580 report.done(Path("f3"), black.Changed.YES)
581 self.assertEqual(len(out_lines), 4)
582 self.assertEqual(len(err_lines), 1)
583 self.assertEqual(out_lines[-1], "reformatted f3")
585 unstyle(str(report)),
586 "2 files reformatted, 2 files left unchanged, 1 file failed to"
589 self.assertEqual(report.return_code, 123)
590 report.failed(Path("e2"), "boom")
591 self.assertEqual(len(out_lines), 4)
592 self.assertEqual(len(err_lines), 2)
593 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
595 unstyle(str(report)),
596 "2 files reformatted, 2 files left unchanged, 2 files failed to"
599 self.assertEqual(report.return_code, 123)
600 report.path_ignored(Path("wat"), "no match")
601 self.assertEqual(len(out_lines), 5)
602 self.assertEqual(len(err_lines), 2)
603 self.assertEqual(out_lines[-1], "wat ignored: no match")
605 unstyle(str(report)),
606 "2 files reformatted, 2 files left unchanged, 2 files failed to"
609 self.assertEqual(report.return_code, 123)
610 report.done(Path("f4"), black.Changed.NO)
611 self.assertEqual(len(out_lines), 6)
612 self.assertEqual(len(err_lines), 2)
613 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
615 unstyle(str(report)),
616 "2 files reformatted, 3 files left unchanged, 2 files failed to"
619 self.assertEqual(report.return_code, 123)
622 unstyle(str(report)),
623 "2 files would be reformatted, 3 files would be left unchanged, 2"
624 " files would fail to reformat.",
629 unstyle(str(report)),
630 "2 files would be reformatted, 3 files would be left unchanged, 2"
631 " files would fail to reformat.",
634 def test_report_quiet(self) -> None:
635 report = Report(quiet=True)
639 def out(msg: str, **kwargs: Any) -> None:
640 out_lines.append(msg)
642 def err(msg: str, **kwargs: Any) -> None:
643 err_lines.append(msg)
645 with patch("black.output._out", out), patch("black.output._err", err):
646 report.done(Path("f1"), black.Changed.NO)
647 self.assertEqual(len(out_lines), 0)
648 self.assertEqual(len(err_lines), 0)
649 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
650 self.assertEqual(report.return_code, 0)
651 report.done(Path("f2"), black.Changed.YES)
652 self.assertEqual(len(out_lines), 0)
653 self.assertEqual(len(err_lines), 0)
655 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
657 report.done(Path("f3"), black.Changed.CACHED)
658 self.assertEqual(len(out_lines), 0)
659 self.assertEqual(len(err_lines), 0)
661 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
663 self.assertEqual(report.return_code, 0)
665 self.assertEqual(report.return_code, 1)
667 report.failed(Path("e1"), "boom")
668 self.assertEqual(len(out_lines), 0)
669 self.assertEqual(len(err_lines), 1)
670 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
672 unstyle(str(report)),
673 "1 file reformatted, 2 files left unchanged, 1 file failed to"
676 self.assertEqual(report.return_code, 123)
677 report.done(Path("f3"), black.Changed.YES)
678 self.assertEqual(len(out_lines), 0)
679 self.assertEqual(len(err_lines), 1)
681 unstyle(str(report)),
682 "2 files reformatted, 2 files left unchanged, 1 file failed to"
685 self.assertEqual(report.return_code, 123)
686 report.failed(Path("e2"), "boom")
687 self.assertEqual(len(out_lines), 0)
688 self.assertEqual(len(err_lines), 2)
689 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
691 unstyle(str(report)),
692 "2 files reformatted, 2 files left unchanged, 2 files failed to"
695 self.assertEqual(report.return_code, 123)
696 report.path_ignored(Path("wat"), "no match")
697 self.assertEqual(len(out_lines), 0)
698 self.assertEqual(len(err_lines), 2)
700 unstyle(str(report)),
701 "2 files reformatted, 2 files left unchanged, 2 files failed to"
704 self.assertEqual(report.return_code, 123)
705 report.done(Path("f4"), black.Changed.NO)
706 self.assertEqual(len(out_lines), 0)
707 self.assertEqual(len(err_lines), 2)
709 unstyle(str(report)),
710 "2 files reformatted, 3 files left unchanged, 2 files failed to"
713 self.assertEqual(report.return_code, 123)
716 unstyle(str(report)),
717 "2 files would be reformatted, 3 files would be left unchanged, 2"
718 " files would fail to reformat.",
723 unstyle(str(report)),
724 "2 files would be reformatted, 3 files would be left unchanged, 2"
725 " files would fail to reformat.",
728 def test_report_normal(self) -> None:
729 report = black.Report()
733 def out(msg: str, **kwargs: Any) -> None:
734 out_lines.append(msg)
736 def err(msg: str, **kwargs: Any) -> None:
737 err_lines.append(msg)
739 with patch("black.output._out", out), patch("black.output._err", err):
740 report.done(Path("f1"), black.Changed.NO)
741 self.assertEqual(len(out_lines), 0)
742 self.assertEqual(len(err_lines), 0)
743 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
744 self.assertEqual(report.return_code, 0)
745 report.done(Path("f2"), black.Changed.YES)
746 self.assertEqual(len(out_lines), 1)
747 self.assertEqual(len(err_lines), 0)
748 self.assertEqual(out_lines[-1], "reformatted f2")
750 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
752 report.done(Path("f3"), black.Changed.CACHED)
753 self.assertEqual(len(out_lines), 1)
754 self.assertEqual(len(err_lines), 0)
755 self.assertEqual(out_lines[-1], "reformatted f2")
757 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
759 self.assertEqual(report.return_code, 0)
761 self.assertEqual(report.return_code, 1)
763 report.failed(Path("e1"), "boom")
764 self.assertEqual(len(out_lines), 1)
765 self.assertEqual(len(err_lines), 1)
766 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
768 unstyle(str(report)),
769 "1 file reformatted, 2 files left unchanged, 1 file failed to"
772 self.assertEqual(report.return_code, 123)
773 report.done(Path("f3"), black.Changed.YES)
774 self.assertEqual(len(out_lines), 2)
775 self.assertEqual(len(err_lines), 1)
776 self.assertEqual(out_lines[-1], "reformatted f3")
778 unstyle(str(report)),
779 "2 files reformatted, 2 files left unchanged, 1 file failed to"
782 self.assertEqual(report.return_code, 123)
783 report.failed(Path("e2"), "boom")
784 self.assertEqual(len(out_lines), 2)
785 self.assertEqual(len(err_lines), 2)
786 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
788 unstyle(str(report)),
789 "2 files reformatted, 2 files left unchanged, 2 files failed to"
792 self.assertEqual(report.return_code, 123)
793 report.path_ignored(Path("wat"), "no match")
794 self.assertEqual(len(out_lines), 2)
795 self.assertEqual(len(err_lines), 2)
797 unstyle(str(report)),
798 "2 files reformatted, 2 files left unchanged, 2 files failed to"
801 self.assertEqual(report.return_code, 123)
802 report.done(Path("f4"), black.Changed.NO)
803 self.assertEqual(len(out_lines), 2)
804 self.assertEqual(len(err_lines), 2)
806 unstyle(str(report)),
807 "2 files reformatted, 3 files left unchanged, 2 files failed to"
810 self.assertEqual(report.return_code, 123)
813 unstyle(str(report)),
814 "2 files would be reformatted, 3 files would be left unchanged, 2"
815 " files would fail to reformat.",
820 unstyle(str(report)),
821 "2 files would be reformatted, 3 files would be left unchanged, 2"
822 " files would fail to reformat.",
825 def test_lib2to3_parse(self) -> None:
826 with self.assertRaises(black.InvalidInput):
827 black.lib2to3_parse("invalid syntax")
830 black.lib2to3_parse(straddling)
831 black.lib2to3_parse(straddling, {TargetVersion.PY36})
834 with self.assertRaises(black.InvalidInput):
835 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
837 py3_only = "exec(x, end=y)"
838 black.lib2to3_parse(py3_only)
839 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
841 def test_get_features_used_decorator(self) -> None:
842 # Test the feature detection of new decorator syntax
843 # since this makes some test cases of test_get_features_used()
844 # fails if it fails, this is tested first so that a useful case
846 simples, relaxed = read_data("miscellaneous", "decorators")
847 # skip explanation comments at the top of the file
848 for simple_test in simples.split("##")[1:]:
849 node = black.lib2to3_parse(simple_test)
850 decorator = str(node.children[0].children[0]).strip()
852 Feature.RELAXED_DECORATORS,
853 black.get_features_used(node),
855 f"decorator '{decorator}' follows python<=3.8 syntax"
856 "but is detected as 3.9+"
857 # f"The full node is\n{node!r}"
860 # skip the '# output' comment at the top of the output part
861 for relaxed_test in relaxed.split("##")[1:]:
862 node = black.lib2to3_parse(relaxed_test)
863 decorator = str(node.children[0].children[0]).strip()
865 Feature.RELAXED_DECORATORS,
866 black.get_features_used(node),
868 f"decorator '{decorator}' uses python3.9+ syntax"
869 "but is detected as python<=3.8"
870 # f"The full node is\n{node!r}"
874 def test_get_features_used(self) -> None:
875 node = black.lib2to3_parse("def f(*, arg): ...\n")
876 self.assertEqual(black.get_features_used(node), set())
877 node = black.lib2to3_parse("def f(*, arg,): ...\n")
878 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
879 node = black.lib2to3_parse("f(*arg,)\n")
881 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
883 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
884 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
885 node = black.lib2to3_parse("123_456\n")
886 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
887 node = black.lib2to3_parse("123456\n")
888 self.assertEqual(black.get_features_used(node), set())
889 source, expected = read_data("cases", "function")
890 node = black.lib2to3_parse(source)
891 expected_features = {
892 Feature.TRAILING_COMMA_IN_CALL,
893 Feature.TRAILING_COMMA_IN_DEF,
896 self.assertEqual(black.get_features_used(node), expected_features)
897 node = black.lib2to3_parse(expected)
898 self.assertEqual(black.get_features_used(node), expected_features)
899 source, expected = read_data("cases", "expression")
900 node = black.lib2to3_parse(source)
901 self.assertEqual(black.get_features_used(node), set())
902 node = black.lib2to3_parse(expected)
903 self.assertEqual(black.get_features_used(node), set())
904 node = black.lib2to3_parse("lambda a, /, b: ...")
905 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
906 node = black.lib2to3_parse("def fn(a, /, b): ...")
907 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
908 node = black.lib2to3_parse("def fn(): yield a, b")
909 self.assertEqual(black.get_features_used(node), set())
910 node = black.lib2to3_parse("def fn(): return a, b")
911 self.assertEqual(black.get_features_used(node), set())
912 node = black.lib2to3_parse("def fn(): yield *b, c")
913 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
914 node = black.lib2to3_parse("def fn(): return a, *b, c")
915 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
916 node = black.lib2to3_parse("x = a, *b, c")
917 self.assertEqual(black.get_features_used(node), set())
918 node = black.lib2to3_parse("x: Any = regular")
919 self.assertEqual(black.get_features_used(node), set())
920 node = black.lib2to3_parse("x: Any = (regular, regular)")
921 self.assertEqual(black.get_features_used(node), set())
922 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
923 self.assertEqual(black.get_features_used(node), set())
924 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
926 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
928 node = black.lib2to3_parse("try: pass\nexcept Something: pass")
929 self.assertEqual(black.get_features_used(node), set())
930 node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass")
931 self.assertEqual(black.get_features_used(node), set())
932 node = black.lib2to3_parse("try: pass\nexcept *Group: pass")
933 self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR})
934 node = black.lib2to3_parse("a[*b]")
935 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
936 node = black.lib2to3_parse("a[x, *y(), z] = t")
937 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
938 node = black.lib2to3_parse("def fn(*args: *T): pass")
939 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
941 def test_get_features_used_for_future_flags(self) -> None:
942 for src, features in [
943 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
945 "from __future__ import (other, annotations)",
946 {Feature.FUTURE_ANNOTATIONS},
948 ("a = 1 + 2\nfrom something import annotations", set()),
949 ("from __future__ import x, y", set()),
951 with self.subTest(src=src, features=features):
952 node = black.lib2to3_parse(src)
953 future_imports = black.get_future_imports(node)
955 black.get_features_used(node, future_imports=future_imports),
959 def test_get_future_imports(self) -> None:
960 node = black.lib2to3_parse("\n")
961 self.assertEqual(set(), black.get_future_imports(node))
962 node = black.lib2to3_parse("from __future__ import black\n")
963 self.assertEqual({"black"}, black.get_future_imports(node))
964 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
965 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
966 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
967 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
968 node = black.lib2to3_parse(
969 "from __future__ import multiple\nfrom __future__ import imports\n"
971 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
972 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
973 self.assertEqual({"black"}, black.get_future_imports(node))
974 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
975 self.assertEqual({"black"}, black.get_future_imports(node))
976 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
977 self.assertEqual(set(), black.get_future_imports(node))
978 node = black.lib2to3_parse("from some.module import black\n")
979 self.assertEqual(set(), black.get_future_imports(node))
980 node = black.lib2to3_parse(
981 "from __future__ import unicode_literals as _unicode_literals"
983 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
984 node = black.lib2to3_parse(
985 "from __future__ import unicode_literals as _lol, print"
987 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
989 @pytest.mark.incompatible_with_mypyc
990 def test_debug_visitor(self) -> None:
991 source, _ = read_data("miscellaneous", "debug_visitor")
992 expected, _ = read_data("miscellaneous", "debug_visitor.out")
996 def out(msg: str, **kwargs: Any) -> None:
997 out_lines.append(msg)
999 def err(msg: str, **kwargs: Any) -> None:
1000 err_lines.append(msg)
1002 with patch("black.debug.out", out):
1003 DebugVisitor.show(source)
1004 actual = "\n".join(out_lines) + "\n"
1006 if expected != actual:
1007 log_name = black.dump_to_file(*out_lines)
1011 f"AST print out is different. Actual version dumped to {log_name}",
1014 def test_format_file_contents(self) -> None:
1017 with self.assertRaises(black.NothingChanged):
1018 black.format_file_contents(empty, mode=mode, fast=False)
1020 with self.assertRaises(black.NothingChanged):
1021 black.format_file_contents(just_nl, mode=mode, fast=False)
1022 same = "j = [1, 2, 3]\n"
1023 with self.assertRaises(black.NothingChanged):
1024 black.format_file_contents(same, mode=mode, fast=False)
1025 different = "j = [1,2,3]"
1027 actual = black.format_file_contents(different, mode=mode, fast=False)
1028 self.assertEqual(expected, actual)
1029 invalid = "return if you can"
1030 with self.assertRaises(black.InvalidInput) as e:
1031 black.format_file_contents(invalid, mode=mode, fast=False)
1032 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
1034 mode = black.Mode(preview=True)
1036 with self.assertRaises(black.NothingChanged):
1037 black.format_file_contents(just_crlf, mode=mode, fast=False)
1038 just_whitespace_nl = "\n\t\n \n\t \n \t\n\n"
1039 actual = black.format_file_contents(just_whitespace_nl, mode=mode, fast=False)
1040 self.assertEqual("\n", actual)
1041 just_whitespace_crlf = "\r\n\t\r\n \r\n\t \r\n \t\r\n\r\n"
1042 actual = black.format_file_contents(just_whitespace_crlf, mode=mode, fast=False)
1043 self.assertEqual("\r\n", actual)
1045 def test_endmarker(self) -> None:
1046 n = black.lib2to3_parse("\n")
1047 self.assertEqual(n.type, black.syms.file_input)
1048 self.assertEqual(len(n.children), 1)
1049 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
1051 @patch("tests.conftest.PRINT_FULL_TREE", True)
1052 @patch("tests.conftest.PRINT_TREE_DIFF", False)
1053 @pytest.mark.incompatible_with_mypyc
1054 def test_assertFormatEqual_print_full_tree(self) -> None:
1058 def out(msg: str, **kwargs: Any) -> None:
1059 out_lines.append(msg)
1061 def err(msg: str, **kwargs: Any) -> None:
1062 err_lines.append(msg)
1064 with patch("black.output._out", out), patch("black.output._err", err):
1065 with self.assertRaises(AssertionError):
1066 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
1068 out_str = "".join(out_lines)
1069 self.assertIn("Expected tree:", out_str)
1070 self.assertIn("Actual tree:", out_str)
1071 self.assertEqual("".join(err_lines), "")
1073 @patch("tests.conftest.PRINT_FULL_TREE", False)
1074 @patch("tests.conftest.PRINT_TREE_DIFF", True)
1075 @pytest.mark.incompatible_with_mypyc
1076 def test_assertFormatEqual_print_tree_diff(self) -> None:
1080 def out(msg: str, **kwargs: Any) -> None:
1081 out_lines.append(msg)
1083 def err(msg: str, **kwargs: Any) -> None:
1084 err_lines.append(msg)
1086 with patch("black.output._out", out), patch("black.output._err", err):
1087 with self.assertRaises(AssertionError):
1088 self.assertFormatEqual("j = [1, 2, 3]\n", "j = [1, 2, 3,]\n")
1090 out_str = "".join(out_lines)
1091 self.assertIn("Tree Diff:", out_str)
1092 self.assertIn("+ COMMA", out_str)
1093 self.assertIn("+ ','", out_str)
1094 self.assertEqual("".join(err_lines), "")
1097 @patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1098 def test_works_in_mono_process_only_environment(self) -> None:
1099 with cache_dir() as workspace:
1101 (workspace / "one.py").resolve(),
1102 (workspace / "two.py").resolve(),
1104 f.write_text('print("hello")\n', encoding="utf-8")
1105 self.invokeBlack([str(workspace)])
1108 def test_check_diff_use_together(self) -> None:
1110 # Files which will be reformatted.
1111 src1 = get_case_path("miscellaneous", "string_quotes")
1112 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1113 # Files which will not be reformatted.
1114 src2 = get_case_path("cases", "composition")
1115 self.invokeBlack([str(src2), "--diff", "--check"])
1116 # Multi file command.
1117 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1119 def test_no_src_fails(self) -> None:
1121 self.invokeBlack([], exit_code=1)
1123 def test_src_and_code_fails(self) -> None:
1125 self.invokeBlack([".", "-c", "0"], exit_code=1)
1127 def test_broken_symlink(self) -> None:
1128 with cache_dir() as workspace:
1129 symlink = workspace / "broken_link.py"
1131 symlink.symlink_to("nonexistent.py")
1132 except (OSError, NotImplementedError) as e:
1133 self.skipTest(f"Can't create symlinks: {e}")
1134 self.invokeBlack([str(workspace.resolve())])
1136 def test_single_file_force_pyi(self) -> None:
1137 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1138 contents, expected = read_data("miscellaneous", "force_pyi")
1139 with cache_dir() as workspace:
1140 path = (workspace / "file.py").resolve()
1141 path.write_text(contents, encoding="utf-8")
1142 self.invokeBlack([str(path), "--pyi"])
1143 actual = path.read_text(encoding="utf-8")
1144 # verify cache with --pyi is separate
1145 pyi_cache = black.Cache.read(pyi_mode)
1146 assert not pyi_cache.is_changed(path)
1147 normal_cache = black.Cache.read(DEFAULT_MODE)
1148 assert normal_cache.is_changed(path)
1149 self.assertFormatEqual(expected, actual)
1150 black.assert_equivalent(contents, actual)
1151 black.assert_stable(contents, actual, pyi_mode)
1154 def test_multi_file_force_pyi(self) -> None:
1155 reg_mode = DEFAULT_MODE
1156 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1157 contents, expected = read_data("miscellaneous", "force_pyi")
1158 with cache_dir() as workspace:
1160 (workspace / "file1.py").resolve(),
1161 (workspace / "file2.py").resolve(),
1164 path.write_text(contents, encoding="utf-8")
1165 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1167 actual = path.read_text(encoding="utf-8")
1168 self.assertEqual(actual, expected)
1169 # verify cache with --pyi is separate
1170 pyi_cache = black.Cache.read(pyi_mode)
1171 normal_cache = black.Cache.read(reg_mode)
1173 assert not pyi_cache.is_changed(path)
1174 assert normal_cache.is_changed(path)
1176 def test_pipe_force_pyi(self) -> None:
1177 source, expected = read_data("miscellaneous", "force_pyi")
1178 result = CliRunner().invoke(
1179 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf-8"))
1181 self.assertEqual(result.exit_code, 0)
1182 actual = result.output
1183 self.assertFormatEqual(actual, expected)
1185 def test_single_file_force_py36(self) -> None:
1186 reg_mode = DEFAULT_MODE
1187 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1188 source, expected = read_data("miscellaneous", "force_py36")
1189 with cache_dir() as workspace:
1190 path = (workspace / "file.py").resolve()
1191 path.write_text(source, encoding="utf-8")
1192 self.invokeBlack([str(path), *PY36_ARGS])
1193 actual = path.read_text(encoding="utf-8")
1194 # verify cache with --target-version is separate
1195 py36_cache = black.Cache.read(py36_mode)
1196 assert not py36_cache.is_changed(path)
1197 normal_cache = black.Cache.read(reg_mode)
1198 assert normal_cache.is_changed(path)
1199 self.assertEqual(actual, expected)
1202 def test_multi_file_force_py36(self) -> None:
1203 reg_mode = DEFAULT_MODE
1204 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1205 source, expected = read_data("miscellaneous", "force_py36")
1206 with cache_dir() as workspace:
1208 (workspace / "file1.py").resolve(),
1209 (workspace / "file2.py").resolve(),
1212 path.write_text(source, encoding="utf-8")
1213 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1215 actual = path.read_text(encoding="utf-8")
1216 self.assertEqual(actual, expected)
1217 # verify cache with --target-version is separate
1218 pyi_cache = black.Cache.read(py36_mode)
1219 normal_cache = black.Cache.read(reg_mode)
1221 assert not pyi_cache.is_changed(path)
1222 assert normal_cache.is_changed(path)
1224 def test_pipe_force_py36(self) -> None:
1225 source, expected = read_data("miscellaneous", "force_py36")
1226 result = CliRunner().invoke(
1228 ["-", "-q", "--target-version=py36"],
1229 input=BytesIO(source.encode("utf-8")),
1231 self.assertEqual(result.exit_code, 0)
1232 actual = result.output
1233 self.assertFormatEqual(actual, expected)
1235 @pytest.mark.incompatible_with_mypyc
1236 def test_reformat_one_with_stdin(self) -> None:
1238 "black.format_stdin_to_stdout",
1239 return_value=lambda *args, **kwargs: black.Changed.YES,
1241 report = MagicMock()
1246 write_back=black.WriteBack.YES,
1250 fsts.assert_called_once()
1251 report.done.assert_called_with(path, black.Changed.YES)
1253 @pytest.mark.incompatible_with_mypyc
1254 def test_reformat_one_with_stdin_filename(self) -> None:
1256 "black.format_stdin_to_stdout",
1257 return_value=lambda *args, **kwargs: black.Changed.YES,
1259 report = MagicMock()
1261 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1266 write_back=black.WriteBack.YES,
1270 fsts.assert_called_once_with(
1271 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1273 # __BLACK_STDIN_FILENAME__ should have been stripped
1274 report.done.assert_called_with(expected, black.Changed.YES)
1276 @pytest.mark.incompatible_with_mypyc
1277 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1279 "black.format_stdin_to_stdout",
1280 return_value=lambda *args, **kwargs: black.Changed.YES,
1282 report = MagicMock()
1284 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1289 write_back=black.WriteBack.YES,
1293 fsts.assert_called_once_with(
1295 write_back=black.WriteBack.YES,
1296 mode=replace(DEFAULT_MODE, is_pyi=True),
1298 # __BLACK_STDIN_FILENAME__ should have been stripped
1299 report.done.assert_called_with(expected, black.Changed.YES)
1301 @pytest.mark.incompatible_with_mypyc
1302 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1304 "black.format_stdin_to_stdout",
1305 return_value=lambda *args, **kwargs: black.Changed.YES,
1307 report = MagicMock()
1309 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1314 write_back=black.WriteBack.YES,
1318 fsts.assert_called_once_with(
1320 write_back=black.WriteBack.YES,
1321 mode=replace(DEFAULT_MODE, is_ipynb=True),
1323 # __BLACK_STDIN_FILENAME__ should have been stripped
1324 report.done.assert_called_with(expected, black.Changed.YES)
1326 @pytest.mark.incompatible_with_mypyc
1327 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1329 "black.format_stdin_to_stdout",
1330 return_value=lambda *args, **kwargs: black.Changed.YES,
1332 report = MagicMock()
1333 # Even with an existing file, since we are forcing stdin, black
1334 # should output to stdout and not modify the file inplace
1335 p = THIS_DIR / "data" / "cases" / "collections.py"
1336 # Make sure is_file actually returns True
1337 self.assertTrue(p.is_file())
1338 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1343 write_back=black.WriteBack.YES,
1347 fsts.assert_called_once()
1348 # __BLACK_STDIN_FILENAME__ should have been stripped
1349 report.done.assert_called_with(expected, black.Changed.YES)
1351 def test_reformat_one_with_stdin_empty(self) -> None:
1358 (" \t\r\n\t ", "\r\n"),
1362 output: io.StringIO, io_TextIOWrapper: Type[io.TextIOWrapper]
1363 ) -> Callable[[Any, Any], io.TextIOWrapper]:
1364 def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper:
1365 if args == (sys.stdout.buffer,):
1366 # It's `format_stdin_to_stdout()` calling `io.TextIOWrapper()`,
1367 # return our mock object.
1369 # It's something else (i.e. `decode_bytes()`) calling
1370 # `io.TextIOWrapper()`, pass through to the original implementation.
1371 # See discussion in https://github.com/psf/black/pull/2489
1372 return io_TextIOWrapper(*args, **kwargs)
1376 mode = black.Mode(preview=True)
1377 for content, expected in cases:
1378 output = io.StringIO()
1379 io_TextIOWrapper = io.TextIOWrapper
1381 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1383 black.format_stdin_to_stdout(
1386 write_back=black.WriteBack.YES,
1389 except io.UnsupportedOperation:
1390 pass # StringIO does not support detach
1391 assert output.getvalue() == expected
1393 # An empty string is the only test case for `preview=False`
1394 output = io.StringIO()
1395 io_TextIOWrapper = io.TextIOWrapper
1396 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1398 black.format_stdin_to_stdout(
1401 write_back=black.WriteBack.YES,
1404 except io.UnsupportedOperation:
1405 pass # StringIO does not support detach
1406 assert output.getvalue() == ""
1408 def test_invalid_cli_regex(self) -> None:
1409 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1410 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1412 def test_required_version_matches_version(self) -> None:
1414 ["--required-version", black.__version__, "-c", "0"],
1419 def test_required_version_matches_partial_version(self) -> None:
1421 ["--required-version", black.__version__.split(".")[0], "-c", "0"],
1426 def test_required_version_does_not_match_on_minor_version(self) -> None:
1428 ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
1433 def test_required_version_does_not_match_version(self) -> None:
1434 result = BlackRunner().invoke(
1436 ["--required-version", "20.99b", "-c", "0"],
1438 self.assertEqual(result.exit_code, 1)
1439 self.assertIn("required version", result.stderr)
1441 def test_preserves_line_endings(self) -> None:
1442 with TemporaryDirectory() as workspace:
1443 test_file = Path(workspace) / "test.py"
1444 for nl in ["\n", "\r\n"]:
1445 contents = nl.join(["def f( ):", " pass"])
1446 test_file.write_bytes(contents.encode())
1447 ff(test_file, write_back=black.WriteBack.YES)
1448 updated_contents: bytes = test_file.read_bytes()
1449 self.assertIn(nl.encode(), updated_contents)
1451 self.assertNotIn(b"\r\n", updated_contents)
1453 def test_preserves_line_endings_via_stdin(self) -> None:
1454 for nl in ["\n", "\r\n"]:
1455 contents = nl.join(["def f( ):", " pass"])
1456 runner = BlackRunner()
1457 result = runner.invoke(
1458 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf-8"))
1460 self.assertEqual(result.exit_code, 0)
1461 output = result.stdout_bytes
1462 self.assertIn(nl.encode("utf-8"), output)
1464 self.assertNotIn(b"\r\n", output)
1466 def test_normalize_line_endings(self) -> None:
1467 with TemporaryDirectory() as workspace:
1468 test_file = Path(workspace) / "test.py"
1469 for data, expected in (
1470 (b"c\r\nc\n ", b"c\r\nc\r\n"),
1471 (b"l\nl\r\n ", b"l\nl\n"),
1473 test_file.write_bytes(data)
1474 ff(test_file, write_back=black.WriteBack.YES)
1475 self.assertEqual(test_file.read_bytes(), expected)
1477 def test_assert_equivalent_different_asts(self) -> None:
1478 with self.assertRaises(AssertionError):
1479 black.assert_equivalent("{}", "None")
1481 def test_root_logger_not_used_directly(self) -> None:
1482 def fail(*args: Any, **kwargs: Any) -> None:
1483 self.fail("Record created with root logger")
1485 with patch.multiple(
1494 ff(THIS_DIR / "util.py")
1496 def test_invalid_config_return_code(self) -> None:
1497 tmp_file = Path(black.dump_to_file())
1499 tmp_config = Path(black.dump_to_file())
1501 args = ["--config", str(tmp_config), str(tmp_file)]
1502 self.invokeBlack(args, exit_code=2, ignore_config=False)
1506 def test_parse_pyproject_toml(self) -> None:
1507 test_toml_file = THIS_DIR / "test.toml"
1508 config = black.parse_pyproject_toml(str(test_toml_file))
1509 self.assertEqual(config["verbose"], 1)
1510 self.assertEqual(config["check"], "no")
1511 self.assertEqual(config["diff"], "y")
1512 self.assertEqual(config["color"], True)
1513 self.assertEqual(config["line_length"], 79)
1514 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1515 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1516 self.assertEqual(config["exclude"], r"\.pyi?$")
1517 self.assertEqual(config["include"], r"\.py?$")
1519 def test_parse_pyproject_toml_project_metadata(self) -> None:
1520 for test_toml, expected in [
1521 ("only_black_pyproject.toml", ["py310"]),
1522 ("only_metadata_pyproject.toml", ["py37", "py38", "py39", "py310"]),
1523 ("neither_pyproject.toml", None),
1524 ("both_pyproject.toml", ["py310"]),
1526 test_toml_file = THIS_DIR / "data" / "project_metadata" / test_toml
1527 config = black.parse_pyproject_toml(str(test_toml_file))
1528 self.assertEqual(config.get("target_version"), expected)
1530 def test_infer_target_version(self) -> None:
1531 for version, expected in [
1532 ("3.6", [TargetVersion.PY36]),
1533 ("3.11.0rc1", [TargetVersion.PY311]),
1534 (">=3.10", [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312]),
1537 [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
1539 ("<3.6", [TargetVersion.PY33, TargetVersion.PY34, TargetVersion.PY35]),
1540 (">3.7,<3.10", [TargetVersion.PY38, TargetVersion.PY39]),
1543 [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
1546 "> 3.9.4, != 3.10.3",
1549 TargetVersion.PY310,
1550 TargetVersion.PY311,
1551 TargetVersion.PY312,
1562 TargetVersion.PY310,
1563 TargetVersion.PY311,
1564 TargetVersion.PY312,
1577 TargetVersion.PY310,
1578 TargetVersion.PY311,
1579 TargetVersion.PY312,
1582 ("==3.8.*", [TargetVersion.PY38]),
1586 ("==invalid", None),
1587 (">3.9,!=invalid", None),
1592 (">3.10,<3.11", None),
1594 test_toml = {"project": {"requires-python": version}}
1595 result = black.files.infer_target_version(test_toml)
1596 self.assertEqual(result, expected)
1598 def test_read_pyproject_toml(self) -> None:
1599 test_toml_file = THIS_DIR / "test.toml"
1600 fake_ctx = FakeContext()
1601 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1602 config = fake_ctx.default_map
1603 self.assertEqual(config["verbose"], "1")
1604 self.assertEqual(config["check"], "no")
1605 self.assertEqual(config["diff"], "y")
1606 self.assertEqual(config["color"], "True")
1607 self.assertEqual(config["line_length"], "79")
1608 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1609 self.assertEqual(config["exclude"], r"\.pyi?$")
1610 self.assertEqual(config["include"], r"\.py?$")
1612 def test_read_pyproject_toml_from_stdin(self) -> None:
1613 with TemporaryDirectory() as workspace:
1614 root = Path(workspace)
1616 src_dir = root / "src"
1619 src_pyproject = src_dir / "pyproject.toml"
1620 src_pyproject.touch()
1622 test_toml_content = (THIS_DIR / "test.toml").read_text(encoding="utf-8")
1623 src_pyproject.write_text(test_toml_content, encoding="utf-8")
1625 src_python = src_dir / "foo.py"
1628 fake_ctx = FakeContext()
1629 fake_ctx.params["src"] = ("-",)
1630 fake_ctx.params["stdin_filename"] = str(src_python)
1632 with change_directory(root):
1633 black.read_pyproject_toml(fake_ctx, FakeParameter(), None)
1635 config = fake_ctx.default_map
1636 self.assertEqual(config["verbose"], "1")
1637 self.assertEqual(config["check"], "no")
1638 self.assertEqual(config["diff"], "y")
1639 self.assertEqual(config["color"], "True")
1640 self.assertEqual(config["line_length"], "79")
1641 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1642 self.assertEqual(config["exclude"], r"\.pyi?$")
1643 self.assertEqual(config["include"], r"\.py?$")
1645 @pytest.mark.incompatible_with_mypyc
1646 def test_find_project_root(self) -> None:
1647 with TemporaryDirectory() as workspace:
1648 root = Path(workspace)
1649 test_dir = root / "test"
1652 src_dir = root / "src"
1655 root_pyproject = root / "pyproject.toml"
1656 root_pyproject.touch()
1657 src_pyproject = src_dir / "pyproject.toml"
1658 src_pyproject.touch()
1659 src_python = src_dir / "foo.py"
1663 black.find_project_root((src_dir, test_dir)),
1664 (root.resolve(), "pyproject.toml"),
1667 black.find_project_root((src_dir,)),
1668 (src_dir.resolve(), "pyproject.toml"),
1671 black.find_project_root((src_python,)),
1672 (src_dir.resolve(), "pyproject.toml"),
1675 with change_directory(test_dir):
1677 black.find_project_root(("-",), stdin_filename="../src/a.py"),
1678 (src_dir.resolve(), "pyproject.toml"),
1682 "black.files.find_user_pyproject_toml",
1684 def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
1685 find_user_pyproject_toml.side_effect = RuntimeError()
1687 with redirect_stderr(io.StringIO()) as stderr:
1688 result = black.files.find_pyproject_toml(
1689 path_search_start=(str(Path.cwd().root),)
1692 assert result is None
1693 err = stderr.getvalue()
1694 assert "Ignoring user configuration" in err
1697 "black.files.find_user_pyproject_toml",
1698 black.files.find_user_pyproject_toml.__wrapped__,
1700 def test_find_user_pyproject_toml_linux(self) -> None:
1701 if system() == "Windows":
1704 # Test if XDG_CONFIG_HOME is checked
1705 with TemporaryDirectory() as workspace:
1706 tmp_user_config = Path(workspace) / "black"
1707 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1709 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1712 # Test fallback for XDG_CONFIG_HOME
1713 with patch.dict("os.environ"):
1714 os.environ.pop("XDG_CONFIG_HOME", None)
1715 fallback_user_config = Path("~/.config").expanduser() / "black"
1717 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1720 def test_find_user_pyproject_toml_windows(self) -> None:
1721 if system() != "Windows":
1724 user_config_path = Path.home() / ".black"
1726 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1729 def test_bpo_33660_workaround(self) -> None:
1730 if system() == "Windows":
1733 # https://bugs.python.org/issue33660
1735 with change_directory(root):
1736 path = Path("workspace") / "project"
1737 report = black.Report(verbose=True)
1738 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1739 self.assertEqual(normalized_path, "workspace/project")
1741 def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
1742 if system() != "Windows":
1745 with TemporaryDirectory() as workspace:
1746 root = Path(workspace)
1747 junction_dir = root / "junction"
1748 junction_target_outside_of_root = root / ".."
1749 os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
1751 report = black.Report(verbose=True)
1752 normalized_path = black.normalize_path_maybe_ignore(
1753 junction_dir, root, report
1755 # Manually delete for Python < 3.8
1756 os.system(f"rmdir {junction_dir}")
1758 self.assertEqual(normalized_path, None)
1760 def test_newline_comment_interaction(self) -> None:
1761 source = "class A:\\\r\n# type: ignore\n pass\n"
1762 output = black.format_str(source, mode=DEFAULT_MODE)
1763 black.assert_stable(source, output, mode=DEFAULT_MODE)
1765 def test_bpo_2142_workaround(self) -> None:
1766 # https://bugs.python.org/issue2142
1768 source, _ = read_data("miscellaneous", "missing_final_newline")
1769 # read_data adds a trailing newline
1770 source = source.rstrip()
1771 expected, _ = read_data("miscellaneous", "missing_final_newline.diff")
1772 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1773 diff_header = re.compile(
1774 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1775 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
1778 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1779 self.assertEqual(result.exit_code, 0)
1782 actual = result.output
1783 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1784 self.assertEqual(actual, expected)
1787 def compare_results(
1788 result: click.testing.Result, expected_value: str, expected_exit_code: int
1790 """Helper method to test the value and exit code of a click Result."""
1792 result.output == expected_value
1793 ), "The output did not match the expected value."
1794 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1796 def test_code_option(self) -> None:
1797 """Test the code option with no changes."""
1798 code = 'print("Hello world")\n'
1799 args = ["--code", code]
1800 result = CliRunner().invoke(black.main, args)
1802 self.compare_results(result, code, 0)
1804 def test_code_option_changed(self) -> None:
1805 """Test the code option when changes are required."""
1806 code = "print('hello world')"
1807 formatted = black.format_str(code, mode=DEFAULT_MODE)
1809 args = ["--code", code]
1810 result = CliRunner().invoke(black.main, args)
1812 self.compare_results(result, formatted, 0)
1814 def test_code_option_check(self) -> None:
1815 """Test the code option when check is passed."""
1816 args = ["--check", "--code", 'print("Hello world")\n']
1817 result = CliRunner().invoke(black.main, args)
1818 self.compare_results(result, "", 0)
1820 def test_code_option_check_changed(self) -> None:
1821 """Test the code option when changes are required, and check is passed."""
1822 args = ["--check", "--code", "print('hello world')"]
1823 result = CliRunner().invoke(black.main, args)
1824 self.compare_results(result, "", 1)
1826 def test_code_option_diff(self) -> None:
1827 """Test the code option when diff is passed."""
1828 code = "print('hello world')"
1829 formatted = black.format_str(code, mode=DEFAULT_MODE)
1830 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1832 args = ["--diff", "--code", code]
1833 result = CliRunner().invoke(black.main, args)
1835 # Remove time from diff
1836 output = DIFF_TIME.sub("", result.output)
1838 assert output == result_diff, "The output did not match the expected value."
1839 assert result.exit_code == 0, "The exit code is incorrect."
1841 def test_code_option_color_diff(self) -> None:
1842 """Test the code option when color and diff are passed."""
1843 code = "print('hello world')"
1844 formatted = black.format_str(code, mode=DEFAULT_MODE)
1846 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1847 result_diff = color_diff(result_diff)
1849 args = ["--diff", "--color", "--code", code]
1850 result = CliRunner().invoke(black.main, args)
1852 # Remove time from diff
1853 output = DIFF_TIME.sub("", result.output)
1855 assert output == result_diff, "The output did not match the expected value."
1856 assert result.exit_code == 0, "The exit code is incorrect."
1858 @pytest.mark.incompatible_with_mypyc
1859 def test_code_option_safe(self) -> None:
1860 """Test that the code option throws an error when the sanity checks fail."""
1861 # Patch black.assert_equivalent to ensure the sanity checks fail
1862 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1863 code = 'print("Hello world")'
1864 error_msg = f"{code}\nerror: cannot format <string>: \n"
1866 args = ["--safe", "--code", code]
1867 result = CliRunner().invoke(black.main, args)
1869 self.compare_results(result, error_msg, 123)
1871 def test_code_option_fast(self) -> None:
1872 """Test that the code option ignores errors when the sanity checks fail."""
1873 # Patch black.assert_equivalent to ensure the sanity checks fail
1874 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1875 code = 'print("Hello world")'
1876 formatted = black.format_str(code, mode=DEFAULT_MODE)
1878 args = ["--fast", "--code", code]
1879 result = CliRunner().invoke(black.main, args)
1881 self.compare_results(result, formatted, 0)
1883 @pytest.mark.incompatible_with_mypyc
1884 def test_code_option_config(self) -> None:
1886 Test that the code option finds the pyproject.toml in the current directory.
1888 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1889 args = ["--code", "print"]
1890 # This is the only directory known to contain a pyproject.toml
1891 with change_directory(PROJECT_ROOT):
1892 CliRunner().invoke(black.main, args)
1893 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1896 len(parse.mock_calls) >= 1
1897 ), "Expected config parse to be called with the current directory."
1899 _, call_args, _ = parse.mock_calls[0]
1901 call_args[0].lower() == str(pyproject_path).lower()
1902 ), "Incorrect config loaded."
1904 @pytest.mark.incompatible_with_mypyc
1905 def test_code_option_parent_config(self) -> None:
1907 Test that the code option finds the pyproject.toml in the parent directory.
1909 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1910 with change_directory(THIS_DIR):
1911 args = ["--code", "print"]
1912 CliRunner().invoke(black.main, args)
1914 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1916 len(parse.mock_calls) >= 1
1917 ), "Expected config parse to be called with the current directory."
1919 _, call_args, _ = parse.mock_calls[0]
1921 call_args[0].lower() == str(pyproject_path).lower()
1922 ), "Incorrect config loaded."
1924 def test_for_handled_unexpected_eof_error(self) -> None:
1926 Test that an unexpected EOF SyntaxError is nicely presented.
1928 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1929 black.lib2to3_parse("print(", {})
1931 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1933 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1934 with pytest.raises(AssertionError) as err:
1935 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1938 # Unfortunately the SyntaxError message has changed in newer versions so we
1939 # can't match it directly.
1940 err.match("invalid character")
1941 err.match(r"\(<unknown>, line 1\)")
1945 def test_get_cache_dir(
1948 monkeypatch: pytest.MonkeyPatch,
1950 # Create multiple cache directories
1951 workspace1 = tmp_path / "ws1"
1953 workspace2 = tmp_path / "ws2"
1956 # Force user_cache_dir to use the temporary directory for easier assertions
1957 patch_user_cache_dir = patch(
1958 target="black.cache.user_cache_dir",
1960 return_value=str(workspace1),
1963 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1964 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1965 with patch_user_cache_dir:
1966 assert get_cache_dir() == workspace1
1968 # If it is set, use the path provided in the env var.
1969 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1970 assert get_cache_dir() == workspace2
1972 def test_cache_broken_file(self) -> None:
1974 with cache_dir() as workspace:
1975 cache_file = get_cache_file(mode)
1976 cache_file.write_text("this is not a pickle", encoding="utf-8")
1977 assert black.Cache.read(mode).file_data == {}
1978 src = (workspace / "test.py").resolve()
1979 src.write_text("print('hello')", encoding="utf-8")
1980 invokeBlack([str(src)])
1981 cache = black.Cache.read(mode)
1982 assert not cache.is_changed(src)
1984 def test_cache_single_file_already_cached(self) -> None:
1986 with cache_dir() as workspace:
1987 src = (workspace / "test.py").resolve()
1988 src.write_text("print('hello')", encoding="utf-8")
1989 cache = black.Cache.read(mode)
1991 invokeBlack([str(src)])
1992 assert src.read_text(encoding="utf-8") == "print('hello')"
1995 def test_cache_multiple_files(self) -> None:
1997 with cache_dir() as workspace, patch(
1998 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
2000 one = (workspace / "one.py").resolve()
2001 one.write_text("print('hello')", encoding="utf-8")
2002 two = (workspace / "two.py").resolve()
2003 two.write_text("print('hello')", encoding="utf-8")
2004 cache = black.Cache.read(mode)
2006 invokeBlack([str(workspace)])
2007 assert one.read_text(encoding="utf-8") == "print('hello')"
2008 assert two.read_text(encoding="utf-8") == 'print("hello")\n'
2009 cache = black.Cache.read(mode)
2010 assert not cache.is_changed(one)
2011 assert not cache.is_changed(two)
2013 @pytest.mark.incompatible_with_mypyc
2014 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
2015 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
2017 with cache_dir() as workspace:
2018 src = (workspace / "test.py").resolve()
2019 src.write_text("print('hello')", encoding="utf-8")
2020 with patch.object(black.Cache, "read") as read_cache, patch.object(
2021 black.Cache, "write"
2023 cmd = [str(src), "--diff"]
2025 cmd.append("--color")
2027 cache_file = get_cache_file(mode)
2028 assert cache_file.exists() is False
2029 read_cache.assert_called_once()
2030 write_cache.assert_not_called()
2032 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
2034 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
2035 with cache_dir() as workspace:
2036 for tag in range(0, 4):
2037 src = (workspace / f"test{tag}.py").resolve()
2038 src.write_text("print('hello')", encoding="utf-8")
2040 "black.concurrency.Manager", wraps=multiprocessing.Manager
2042 cmd = ["--diff", str(workspace)]
2044 cmd.append("--color")
2045 invokeBlack(cmd, exit_code=0)
2046 # this isn't quite doing what we want, but if it _isn't_
2047 # called then we cannot be using the lock it provides
2050 def test_no_cache_when_stdin(self) -> None:
2053 result = CliRunner().invoke(
2054 black.main, ["-"], input=BytesIO(b"print('hello')")
2056 assert not result.exit_code
2057 cache_file = get_cache_file(mode)
2058 assert not cache_file.exists()
2060 def test_read_cache_no_cachefile(self) -> None:
2063 assert black.Cache.read(mode).file_data == {}
2065 def test_write_cache_read_cache(self) -> None:
2067 with cache_dir() as workspace:
2068 src = (workspace / "test.py").resolve()
2070 write_cache = black.Cache.read(mode)
2071 write_cache.write([src])
2072 read_cache = black.Cache.read(mode)
2073 assert not read_cache.is_changed(src)
2075 @pytest.mark.incompatible_with_mypyc
2076 def test_filter_cached(self) -> None:
2077 with TemporaryDirectory() as workspace:
2078 path = Path(workspace)
2079 uncached = (path / "uncached").resolve()
2080 cached = (path / "cached").resolve()
2081 cached_but_changed = (path / "changed").resolve()
2084 cached_but_changed.touch()
2085 cache = black.Cache.read(DEFAULT_MODE)
2087 orig_func = black.Cache.get_file_data
2089 def wrapped_func(path: Path) -> FileData:
2091 return orig_func(path)
2092 if path == cached_but_changed:
2093 return FileData(0.0, 0, "")
2094 raise AssertionError
2096 with patch.object(black.Cache, "get_file_data", side_effect=wrapped_func):
2097 cache.write([cached, cached_but_changed])
2098 todo, done = cache.filtered_cached({uncached, cached, cached_but_changed})
2099 assert todo == {uncached, cached_but_changed}
2100 assert done == {cached}
2102 def test_filter_cached_hash(self) -> None:
2103 with TemporaryDirectory() as workspace:
2104 path = Path(workspace)
2105 src = (path / "test.py").resolve()
2106 src.write_text("print('hello')", encoding="utf-8")
2108 cache = black.Cache.read(DEFAULT_MODE)
2110 cached_file_data = cache.file_data[str(src)]
2112 todo, done = cache.filtered_cached([src])
2113 assert todo == set()
2114 assert done == {src}
2115 assert cached_file_data.st_mtime == st.st_mtime
2118 cached_file_data = cache.file_data[str(src)] = FileData(
2119 cached_file_data.st_mtime - 1,
2120 cached_file_data.st_size,
2121 cached_file_data.hash,
2123 todo, done = cache.filtered_cached([src])
2124 assert todo == set()
2125 assert done == {src}
2126 assert cached_file_data.st_mtime < st.st_mtime
2127 assert cached_file_data.st_size == st.st_size
2128 assert cached_file_data.hash == black.Cache.hash_digest(src)
2131 src.write_text("print('hello world')", encoding="utf-8")
2133 todo, done = cache.filtered_cached([src])
2134 assert todo == {src}
2135 assert done == set()
2136 assert cached_file_data.st_mtime < new_st.st_mtime
2137 assert cached_file_data.st_size != new_st.st_size
2138 assert cached_file_data.hash != black.Cache.hash_digest(src)
2140 def test_write_cache_creates_directory_if_needed(self) -> None:
2142 with cache_dir(exists=False) as workspace:
2143 assert not workspace.exists()
2144 cache = black.Cache.read(mode)
2146 assert workspace.exists()
2149 def test_failed_formatting_does_not_get_cached(self) -> None:
2151 with cache_dir() as workspace, patch(
2152 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
2154 failing = (workspace / "failing.py").resolve()
2155 failing.write_text("not actually python", encoding="utf-8")
2156 clean = (workspace / "clean.py").resolve()
2157 clean.write_text('print("hello")\n', encoding="utf-8")
2158 invokeBlack([str(workspace)], exit_code=123)
2159 cache = black.Cache.read(mode)
2160 assert cache.is_changed(failing)
2161 assert not cache.is_changed(clean)
2163 def test_write_cache_write_fail(self) -> None:
2166 cache = black.Cache.read(mode)
2167 with patch.object(Path, "open") as mock:
2168 mock.side_effect = OSError
2171 def test_read_cache_line_lengths(self) -> None:
2173 short_mode = replace(DEFAULT_MODE, line_length=1)
2174 with cache_dir() as workspace:
2175 path = (workspace / "file.py").resolve()
2177 cache = black.Cache.read(mode)
2179 one = black.Cache.read(mode)
2180 assert not one.is_changed(path)
2181 two = black.Cache.read(short_mode)
2182 assert two.is_changed(path)
2185 def assert_collected_sources(
2186 src: Sequence[Union[str, Path]],
2187 expected: Sequence[Union[str, Path]],
2189 root: Optional[Path] = None,
2190 exclude: Optional[str] = None,
2191 include: Optional[str] = None,
2192 extend_exclude: Optional[str] = None,
2193 force_exclude: Optional[str] = None,
2194 stdin_filename: Optional[str] = None,
2196 gs_src = tuple(str(Path(s)) for s in src)
2197 gs_expected = [Path(s) for s in expected]
2198 gs_exclude = None if exclude is None else compile_pattern(exclude)
2199 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
2200 gs_extend_exclude = (
2201 None if extend_exclude is None else compile_pattern(extend_exclude)
2203 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
2204 collected = black.get_sources(
2205 root=root or THIS_DIR,
2211 extend_exclude=gs_extend_exclude,
2212 force_exclude=gs_force_exclude,
2213 report=black.Report(),
2214 stdin_filename=stdin_filename,
2216 assert sorted(collected) == sorted(gs_expected)
2219 class TestFileCollection:
2220 def test_include_exclude(self) -> None:
2221 path = THIS_DIR / "data" / "include_exclude_tests"
2224 Path(path / "b/dont_exclude/a.py"),
2225 Path(path / "b/dont_exclude/a.pyi"),
2227 assert_collected_sources(
2231 exclude=r"/exclude/|/\.definitely_exclude/",
2234 def test_gitignore_used_as_default(self) -> None:
2235 base = Path(DATA_DIR / "include_exclude_tests")
2237 base / "b/.definitely_exclude/a.py",
2238 base / "b/.definitely_exclude/a.pyi",
2241 assert_collected_sources(src, expected, root=base, extend_exclude=r"/exclude/")
2243 def test_gitignore_used_on_multiple_sources(self) -> None:
2244 root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
2246 root / "dir1" / "b.py",
2247 root / "dir2" / "b.py",
2249 src = [root / "dir1", root / "dir2"]
2250 assert_collected_sources(src, expected, root=root)
2252 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2253 def test_exclude_for_issue_1572(self) -> None:
2254 # Exclude shouldn't touch files that were explicitly given to Black through the
2255 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
2256 # https://github.com/psf/black/issues/1572
2257 path = DATA_DIR / "include_exclude_tests"
2258 src = [path / "b/exclude/a.py"]
2259 expected = [path / "b/exclude/a.py"]
2260 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2262 def test_gitignore_exclude(self) -> None:
2263 path = THIS_DIR / "data" / "include_exclude_tests"
2264 include = re.compile(r"\.pyi?$")
2265 exclude = re.compile(r"")
2266 report = black.Report()
2267 gitignore = PathSpec.from_lines(
2268 "gitwildmatch", ["exclude/", ".definitely_exclude"]
2270 sources: List[Path] = []
2272 Path(path / "b/dont_exclude/a.py"),
2273 Path(path / "b/dont_exclude/a.pyi"),
2275 this_abs = THIS_DIR.resolve()
2277 black.gen_python_files(
2290 assert sorted(expected) == sorted(sources)
2292 def test_nested_gitignore(self) -> None:
2293 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
2294 include = re.compile(r"\.pyi?$")
2295 exclude = re.compile(r"")
2296 root_gitignore = black.files.get_gitignore(path)
2297 report = black.Report()
2298 expected: List[Path] = [
2299 Path(path / "x.py"),
2300 Path(path / "root/b.py"),
2301 Path(path / "root/c.py"),
2302 Path(path / "root/child/c.py"),
2304 this_abs = THIS_DIR.resolve()
2306 black.gen_python_files(
2314 {path: root_gitignore},
2319 assert sorted(expected) == sorted(sources)
2321 def test_nested_gitignore_directly_in_source_directory(self) -> None:
2322 # https://github.com/psf/black/issues/2598
2323 path = Path(DATA_DIR / "nested_gitignore_tests")
2324 src = Path(path / "root" / "child")
2325 expected = [src / "a.py", src / "c.py"]
2326 assert_collected_sources([src], expected)
2328 def test_invalid_gitignore(self) -> None:
2329 path = THIS_DIR / "data" / "invalid_gitignore_tests"
2330 empty_config = path / "pyproject.toml"
2331 result = BlackRunner().invoke(
2332 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2334 assert result.exit_code == 1
2335 assert result.stderr_bytes is not None
2337 gitignore = path / ".gitignore"
2338 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2340 def test_invalid_nested_gitignore(self) -> None:
2341 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
2342 empty_config = path / "pyproject.toml"
2343 result = BlackRunner().invoke(
2344 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2346 assert result.exit_code == 1
2347 assert result.stderr_bytes is not None
2349 gitignore = path / "a" / ".gitignore"
2350 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2352 def test_gitignore_that_ignores_subfolders(self) -> None:
2353 # If gitignore with */* is in root
2354 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir")
2355 expected = [root / "b.py"]
2356 assert_collected_sources([root], expected, root=root)
2358 # If .gitignore with */* is nested
2359 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2362 root / "subdir" / "b.py",
2364 assert_collected_sources([root], expected, root=root)
2366 # If command is executed from outer dir
2367 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2368 target = root / "subdir"
2369 expected = [target / "b.py"]
2370 assert_collected_sources([target], expected, root=root)
2372 def test_empty_include(self) -> None:
2373 path = DATA_DIR / "include_exclude_tests"
2376 Path(path / "b/exclude/a.pie"),
2377 Path(path / "b/exclude/a.py"),
2378 Path(path / "b/exclude/a.pyi"),
2379 Path(path / "b/dont_exclude/a.pie"),
2380 Path(path / "b/dont_exclude/a.py"),
2381 Path(path / "b/dont_exclude/a.pyi"),
2382 Path(path / "b/.definitely_exclude/a.pie"),
2383 Path(path / "b/.definitely_exclude/a.py"),
2384 Path(path / "b/.definitely_exclude/a.pyi"),
2385 Path(path / ".gitignore"),
2386 Path(path / "pyproject.toml"),
2388 # Setting exclude explicitly to an empty string to block .gitignore usage.
2389 assert_collected_sources(src, expected, include="", exclude="")
2391 def test_extend_exclude(self) -> None:
2392 path = DATA_DIR / "include_exclude_tests"
2395 Path(path / "b/exclude/a.py"),
2396 Path(path / "b/dont_exclude/a.py"),
2398 assert_collected_sources(
2399 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
2402 @pytest.mark.incompatible_with_mypyc
2403 def test_symlinks(self) -> None:
2405 root = THIS_DIR.resolve()
2406 include = re.compile(black.DEFAULT_INCLUDES)
2407 exclude = re.compile(black.DEFAULT_EXCLUDES)
2408 report = black.Report()
2409 gitignore = PathSpec.from_lines("gitwildmatch", [])
2411 regular = MagicMock()
2412 outside_root_symlink = MagicMock()
2413 ignored_symlink = MagicMock()
2415 path.iterdir.return_value = [regular, outside_root_symlink, ignored_symlink]
2417 regular.absolute.return_value = root / "regular.py"
2418 regular.resolve.return_value = root / "regular.py"
2419 regular.is_dir.return_value = False
2421 outside_root_symlink.absolute.return_value = root / "symlink.py"
2422 outside_root_symlink.resolve.return_value = Path("/nowhere")
2424 ignored_symlink.absolute.return_value = root / ".mypy_cache" / "symlink.py"
2427 black.gen_python_files(
2440 assert files == [regular]
2442 path.iterdir.assert_called_once()
2443 outside_root_symlink.resolve.assert_called_once()
2444 ignored_symlink.resolve.assert_not_called()
2446 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2447 def test_get_sources_with_stdin(self) -> None:
2450 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2452 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2453 def test_get_sources_with_stdin_filename(self) -> None:
2455 stdin_filename = str(THIS_DIR / "data/collections.py")
2456 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2457 assert_collected_sources(
2460 exclude=r"/exclude/a\.py",
2461 stdin_filename=stdin_filename,
2464 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2465 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2466 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2467 # file being passed directly. This is the same as
2468 # test_exclude_for_issue_1572
2469 path = DATA_DIR / "include_exclude_tests"
2471 stdin_filename = str(path / "b/exclude/a.py")
2472 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2473 assert_collected_sources(
2476 exclude=r"/exclude/|a\.py",
2477 stdin_filename=stdin_filename,
2480 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2481 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2482 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2483 # file being passed directly. This is the same as
2484 # test_exclude_for_issue_1572
2486 path = THIS_DIR / "data" / "include_exclude_tests"
2487 stdin_filename = str(path / "b/exclude/a.py")
2488 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2489 assert_collected_sources(
2492 extend_exclude=r"/exclude/|a\.py",
2493 stdin_filename=stdin_filename,
2496 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2497 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2498 # Force exclude should exclude the file when passing it through
2500 path = THIS_DIR / "data" / "include_exclude_tests"
2501 stdin_filename = str(path / "b/exclude/a.py")
2502 assert_collected_sources(
2505 force_exclude=r"/exclude/|a\.py",
2506 stdin_filename=stdin_filename,
2510 class TestDeFactoAPI:
2511 """Test that certain symbols that are commonly used externally keep working.
2513 We don't (yet) formally expose an API (see issue #779), but we should endeavor to
2514 keep certain functions that external users commonly rely on working.
2518 def test_format_str(self) -> None:
2519 # format_str and Mode should keep working
2521 black.format_str("print('hello')", mode=black.Mode()) == 'print("hello")\n'
2524 # you can pass line length
2526 black.format_str("print('hello')", mode=black.Mode(line_length=42))
2527 == 'print("hello")\n'
2530 # invalid input raises InvalidInput
2531 with pytest.raises(black.InvalidInput):
2532 black.format_str("syntax error", mode=black.Mode())
2534 def test_format_file_contents(self) -> None:
2535 # You probably should be using format_str() instead, but let's keep
2536 # this one around since people do use it
2538 black.format_file_contents("x=1", fast=True, mode=black.Mode()) == "x = 1\n"
2541 with pytest.raises(black.NothingChanged):
2542 black.format_file_contents("x = 1\n", fast=True, mode=black.Mode())
2546 with open(black.__file__, "r", encoding="utf-8") as _bf:
2547 black_source_lines = _bf.readlines()
2548 except UnicodeDecodeError:
2549 if not black.COMPILED:
2554 frame: types.FrameType, event: str, arg: Any
2555 ) -> Callable[[types.FrameType, str, Any], Any]:
2556 """Show function calls `from black/__init__.py` as they happen.
2558 Register this with `sys.settrace()` in a test you're debugging.
2563 stack = len(inspect.stack()) - 19
2565 filename = frame.f_code.co_filename
2566 lineno = frame.f_lineno
2567 func_sig_lineno = lineno - 1
2568 funcname = black_source_lines[func_sig_lineno].strip()
2569 while funcname.startswith("@"):
2570 func_sig_lineno += 1
2571 funcname = black_source_lines[func_sig_lineno].strip()
2572 if "black/__init__.py" in filename:
2573 print(f"{' ' * stack}{lineno}:{funcname}")