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 # 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 with open(tmp_file, encoding="utf8") as f:
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("utf8")
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("utf8")),
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("utf8"))
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("utf8"))
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_expression_ff(self) -> None:
275 source, expected = read_data("simple_cases", "expression.py")
276 tmp_file = Path(black.dump_to_file(source))
278 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
279 with open(tmp_file, encoding="utf8") as f:
283 self.assertFormatEqual(expected, actual)
284 with patch("black.dump_to_file", dump_to_stderr):
285 black.assert_equivalent(source, actual)
286 black.assert_stable(source, actual, DEFAULT_MODE)
288 def test_expression_diff(self) -> None:
289 source, _ = read_data("simple_cases", "expression.py")
290 expected, _ = read_data("simple_cases", "expression.diff")
291 tmp_file = Path(black.dump_to_file(source))
292 diff_header = re.compile(
293 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
294 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
297 result = BlackRunner().invoke(
298 black.main, ["--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
300 self.assertEqual(result.exit_code, 0)
303 actual = result.output
304 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
305 if expected != actual:
306 dump = black.dump_to_file(actual)
308 "Expected diff isn't equal to the actual. If you made changes to"
309 " expression.py and this is an anticipated difference, overwrite"
310 f" tests/data/expression.diff with {dump}"
312 self.assertEqual(expected, actual, msg)
314 def test_expression_diff_with_color(self) -> None:
315 source, _ = read_data("simple_cases", "expression.py")
316 expected, _ = read_data("simple_cases", "expression.diff")
317 tmp_file = Path(black.dump_to_file(source))
319 result = BlackRunner().invoke(
321 ["--diff", "--color", str(tmp_file), f"--config={EMPTY_CONFIG}"],
325 actual = result.output
326 # We check the contents of the diff in `test_expression_diff`. All
327 # we need to check here is that color codes exist in the result.
328 self.assertIn("\033[1m", actual)
329 self.assertIn("\033[36m", actual)
330 self.assertIn("\033[32m", actual)
331 self.assertIn("\033[31m", actual)
332 self.assertIn("\033[0m", actual)
334 def test_detect_pos_only_arguments(self) -> None:
335 source, _ = read_data("py_38", "pep_570")
336 root = black.lib2to3_parse(source)
337 features = black.get_features_used(root)
338 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
339 versions = black.detect_target_versions(root)
340 self.assertIn(black.TargetVersion.PY38, versions)
342 def test_detect_debug_f_strings(self) -> None:
343 root = black.lib2to3_parse("""f"{x=}" """)
344 features = black.get_features_used(root)
345 self.assertIn(black.Feature.DEBUG_F_STRINGS, features)
346 versions = black.detect_target_versions(root)
347 self.assertIn(black.TargetVersion.PY38, versions)
349 root = black.lib2to3_parse(
350 """f"{x}"\nf'{"="}'\nf'{(x:=5)}'\nf'{f(a="3=")}'\nf'{x:=10}'\n"""
352 features = black.get_features_used(root)
353 self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
355 # We don't yet support feature version detection in nested f-strings
356 root = black.lib2to3_parse(
357 """f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """
359 features = black.get_features_used(root)
360 self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
362 @patch("black.dump_to_file", dump_to_stderr)
363 def test_string_quotes(self) -> None:
364 source, expected = read_data("miscellaneous", "string_quotes")
365 mode = black.Mode(preview=True)
366 assert_format(source, expected, mode)
367 mode = replace(mode, string_normalization=False)
368 not_normalized = fs(source, mode=mode)
369 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
370 black.assert_equivalent(source, not_normalized)
371 black.assert_stable(source, not_normalized, mode=mode)
373 def test_skip_source_first_line(self) -> None:
374 source, _ = read_data("miscellaneous", "invalid_header")
375 tmp_file = Path(black.dump_to_file(source))
376 # Full source should fail (invalid syntax at header)
377 self.invokeBlack([str(tmp_file), "--diff", "--check"], exit_code=123)
378 # So, skipping the first line should work
379 result = BlackRunner().invoke(
380 black.main, [str(tmp_file), "-x", f"--config={EMPTY_CONFIG}"]
382 self.assertEqual(result.exit_code, 0)
383 with open(tmp_file, encoding="utf8") as f:
385 self.assertFormatEqual(source, actual)
387 def test_skip_source_first_line_when_mixing_newlines(self) -> None:
388 code_mixing_newlines = b"Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n"
389 expected = b"Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n"
390 with TemporaryDirectory() as workspace:
391 test_file = Path(workspace) / "skip_header.py"
392 test_file.write_bytes(code_mixing_newlines)
393 mode = replace(DEFAULT_MODE, skip_source_first_line=True)
394 ff(test_file, mode=mode, write_back=black.WriteBack.YES)
395 self.assertEqual(test_file.read_bytes(), expected)
397 def test_skip_magic_trailing_comma(self) -> None:
398 source, _ = read_data("simple_cases", "expression")
399 expected, _ = read_data(
400 "miscellaneous", "expression_skip_magic_trailing_comma.diff"
402 tmp_file = Path(black.dump_to_file(source))
403 diff_header = re.compile(
404 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
405 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
408 result = BlackRunner().invoke(
409 black.main, ["-C", "--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
411 self.assertEqual(result.exit_code, 0)
414 actual = result.output
415 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
416 actual = actual.rstrip() + "\n" # the diff output has a trailing space
417 if expected != actual:
418 dump = black.dump_to_file(actual)
420 "Expected diff isn't equal to the actual. If you made changes to"
421 " expression.py and this is an anticipated difference, overwrite"
422 " tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff"
425 self.assertEqual(expected, actual, msg)
427 @patch("black.dump_to_file", dump_to_stderr)
428 def test_async_as_identifier(self) -> None:
429 source_path = get_case_path("miscellaneous", "async_as_identifier")
430 source, expected = read_data_from_file(source_path)
432 self.assertFormatEqual(expected, actual)
433 major, minor = sys.version_info[:2]
434 if major < 3 or (major <= 3 and minor < 7):
435 black.assert_equivalent(source, actual)
436 black.assert_stable(source, actual, DEFAULT_MODE)
437 # ensure black can parse this when the target is 3.6
438 self.invokeBlack([str(source_path), "--target-version", "py36"])
439 # but not on 3.7, because async/await is no longer an identifier
440 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
442 @patch("black.dump_to_file", dump_to_stderr)
443 def test_python37(self) -> None:
444 source_path = get_case_path("py_37", "python37")
445 source, expected = read_data_from_file(source_path)
447 self.assertFormatEqual(expected, actual)
448 major, minor = sys.version_info[:2]
449 if major > 3 or (major == 3 and minor >= 7):
450 black.assert_equivalent(source, actual)
451 black.assert_stable(source, actual, DEFAULT_MODE)
452 # ensure black can parse this when the target is 3.7
453 self.invokeBlack([str(source_path), "--target-version", "py37"])
454 # but not on 3.6, because we use async as a reserved keyword
455 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
457 def test_tab_comment_indentation(self) -> None:
458 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
459 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
460 self.assertFormatEqual(contents_spc, fs(contents_spc))
461 self.assertFormatEqual(contents_spc, fs(contents_tab))
463 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
464 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
465 self.assertFormatEqual(contents_spc, fs(contents_spc))
466 self.assertFormatEqual(contents_spc, fs(contents_tab))
468 # mixed tabs and spaces (valid Python 2 code)
469 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
470 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
471 self.assertFormatEqual(contents_spc, fs(contents_spc))
472 self.assertFormatEqual(contents_spc, fs(contents_tab))
474 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
475 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
476 self.assertFormatEqual(contents_spc, fs(contents_spc))
477 self.assertFormatEqual(contents_spc, fs(contents_tab))
479 def test_false_positive_symlink_output_issue_3384(self) -> None:
480 # Emulate the behavior when using the CLI (`black ./child --verbose`), which
481 # involves patching some `pathlib.Path` methods. In particular, `is_dir` is
482 # patched only on its first call: when checking if "./child" is a directory it
483 # should return True. The "./child" folder exists relative to the cwd when
484 # running from CLI, but fails when running the tests because cwd is different
485 project_root = Path(THIS_DIR / "data" / "nested_gitignore_tests")
486 working_directory = project_root / "root"
487 target_abspath = working_directory / "child"
489 src.relative_to(working_directory) for src in target_abspath.iterdir()
492 def mock_n_calls(responses: List[bool]) -> Callable[[], bool]:
493 def _mocked_calls() -> bool:
495 return responses.pop(0)
500 with patch("pathlib.Path.iterdir", return_value=target_contents), patch(
501 "pathlib.Path.cwd", return_value=working_directory
502 ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])):
504 ctx.obj["root"] = project_root
505 report = MagicMock(verbose=True)
511 include=DEFAULT_INCLUDE,
519 mock_args[1].startswith("is a symbolic link that points outside")
520 for _, mock_args, _ in report.path_ignored.mock_calls
521 ), "A symbolic link was reported."
522 report.path_ignored.assert_called_once_with(
523 Path("child", "b.py"), "matches a .gitignore file content"
526 def test_report_verbose(self) -> None:
527 report = Report(verbose=True)
531 def out(msg: str, **kwargs: Any) -> None:
532 out_lines.append(msg)
534 def err(msg: str, **kwargs: Any) -> None:
535 err_lines.append(msg)
537 with patch("black.output._out", out), patch("black.output._err", err):
538 report.done(Path("f1"), black.Changed.NO)
539 self.assertEqual(len(out_lines), 1)
540 self.assertEqual(len(err_lines), 0)
541 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
542 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
543 self.assertEqual(report.return_code, 0)
544 report.done(Path("f2"), black.Changed.YES)
545 self.assertEqual(len(out_lines), 2)
546 self.assertEqual(len(err_lines), 0)
547 self.assertEqual(out_lines[-1], "reformatted f2")
549 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
551 report.done(Path("f3"), black.Changed.CACHED)
552 self.assertEqual(len(out_lines), 3)
553 self.assertEqual(len(err_lines), 0)
555 out_lines[-1], "f3 wasn't modified on disk since last run."
558 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
560 self.assertEqual(report.return_code, 0)
562 self.assertEqual(report.return_code, 1)
564 report.failed(Path("e1"), "boom")
565 self.assertEqual(len(out_lines), 3)
566 self.assertEqual(len(err_lines), 1)
567 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
569 unstyle(str(report)),
570 "1 file reformatted, 2 files left unchanged, 1 file failed to"
573 self.assertEqual(report.return_code, 123)
574 report.done(Path("f3"), black.Changed.YES)
575 self.assertEqual(len(out_lines), 4)
576 self.assertEqual(len(err_lines), 1)
577 self.assertEqual(out_lines[-1], "reformatted f3")
579 unstyle(str(report)),
580 "2 files reformatted, 2 files left unchanged, 1 file failed to"
583 self.assertEqual(report.return_code, 123)
584 report.failed(Path("e2"), "boom")
585 self.assertEqual(len(out_lines), 4)
586 self.assertEqual(len(err_lines), 2)
587 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
589 unstyle(str(report)),
590 "2 files reformatted, 2 files left unchanged, 2 files failed to"
593 self.assertEqual(report.return_code, 123)
594 report.path_ignored(Path("wat"), "no match")
595 self.assertEqual(len(out_lines), 5)
596 self.assertEqual(len(err_lines), 2)
597 self.assertEqual(out_lines[-1], "wat ignored: no match")
599 unstyle(str(report)),
600 "2 files reformatted, 2 files left unchanged, 2 files failed to"
603 self.assertEqual(report.return_code, 123)
604 report.done(Path("f4"), black.Changed.NO)
605 self.assertEqual(len(out_lines), 6)
606 self.assertEqual(len(err_lines), 2)
607 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
609 unstyle(str(report)),
610 "2 files reformatted, 3 files left unchanged, 2 files failed to"
613 self.assertEqual(report.return_code, 123)
616 unstyle(str(report)),
617 "2 files would be reformatted, 3 files would be left unchanged, 2"
618 " files would fail to reformat.",
623 unstyle(str(report)),
624 "2 files would be reformatted, 3 files would be left unchanged, 2"
625 " files would fail to reformat.",
628 def test_report_quiet(self) -> None:
629 report = Report(quiet=True)
633 def out(msg: str, **kwargs: Any) -> None:
634 out_lines.append(msg)
636 def err(msg: str, **kwargs: Any) -> None:
637 err_lines.append(msg)
639 with patch("black.output._out", out), patch("black.output._err", err):
640 report.done(Path("f1"), black.Changed.NO)
641 self.assertEqual(len(out_lines), 0)
642 self.assertEqual(len(err_lines), 0)
643 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
644 self.assertEqual(report.return_code, 0)
645 report.done(Path("f2"), black.Changed.YES)
646 self.assertEqual(len(out_lines), 0)
647 self.assertEqual(len(err_lines), 0)
649 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
651 report.done(Path("f3"), black.Changed.CACHED)
652 self.assertEqual(len(out_lines), 0)
653 self.assertEqual(len(err_lines), 0)
655 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
657 self.assertEqual(report.return_code, 0)
659 self.assertEqual(report.return_code, 1)
661 report.failed(Path("e1"), "boom")
662 self.assertEqual(len(out_lines), 0)
663 self.assertEqual(len(err_lines), 1)
664 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
666 unstyle(str(report)),
667 "1 file reformatted, 2 files left unchanged, 1 file failed to"
670 self.assertEqual(report.return_code, 123)
671 report.done(Path("f3"), black.Changed.YES)
672 self.assertEqual(len(out_lines), 0)
673 self.assertEqual(len(err_lines), 1)
675 unstyle(str(report)),
676 "2 files reformatted, 2 files left unchanged, 1 file failed to"
679 self.assertEqual(report.return_code, 123)
680 report.failed(Path("e2"), "boom")
681 self.assertEqual(len(out_lines), 0)
682 self.assertEqual(len(err_lines), 2)
683 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
685 unstyle(str(report)),
686 "2 files reformatted, 2 files left unchanged, 2 files failed to"
689 self.assertEqual(report.return_code, 123)
690 report.path_ignored(Path("wat"), "no match")
691 self.assertEqual(len(out_lines), 0)
692 self.assertEqual(len(err_lines), 2)
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.done(Path("f4"), black.Changed.NO)
700 self.assertEqual(len(out_lines), 0)
701 self.assertEqual(len(err_lines), 2)
703 unstyle(str(report)),
704 "2 files reformatted, 3 files left unchanged, 2 files failed to"
707 self.assertEqual(report.return_code, 123)
710 unstyle(str(report)),
711 "2 files would be reformatted, 3 files would be left unchanged, 2"
712 " files would fail to reformat.",
717 unstyle(str(report)),
718 "2 files would be reformatted, 3 files would be left unchanged, 2"
719 " files would fail to reformat.",
722 def test_report_normal(self) -> None:
723 report = black.Report()
727 def out(msg: str, **kwargs: Any) -> None:
728 out_lines.append(msg)
730 def err(msg: str, **kwargs: Any) -> None:
731 err_lines.append(msg)
733 with patch("black.output._out", out), patch("black.output._err", err):
734 report.done(Path("f1"), black.Changed.NO)
735 self.assertEqual(len(out_lines), 0)
736 self.assertEqual(len(err_lines), 0)
737 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
738 self.assertEqual(report.return_code, 0)
739 report.done(Path("f2"), black.Changed.YES)
740 self.assertEqual(len(out_lines), 1)
741 self.assertEqual(len(err_lines), 0)
742 self.assertEqual(out_lines[-1], "reformatted f2")
744 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
746 report.done(Path("f3"), black.Changed.CACHED)
747 self.assertEqual(len(out_lines), 1)
748 self.assertEqual(len(err_lines), 0)
749 self.assertEqual(out_lines[-1], "reformatted f2")
751 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
753 self.assertEqual(report.return_code, 0)
755 self.assertEqual(report.return_code, 1)
757 report.failed(Path("e1"), "boom")
758 self.assertEqual(len(out_lines), 1)
759 self.assertEqual(len(err_lines), 1)
760 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
762 unstyle(str(report)),
763 "1 file reformatted, 2 files left unchanged, 1 file failed to"
766 self.assertEqual(report.return_code, 123)
767 report.done(Path("f3"), black.Changed.YES)
768 self.assertEqual(len(out_lines), 2)
769 self.assertEqual(len(err_lines), 1)
770 self.assertEqual(out_lines[-1], "reformatted f3")
772 unstyle(str(report)),
773 "2 files reformatted, 2 files left unchanged, 1 file failed to"
776 self.assertEqual(report.return_code, 123)
777 report.failed(Path("e2"), "boom")
778 self.assertEqual(len(out_lines), 2)
779 self.assertEqual(len(err_lines), 2)
780 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
782 unstyle(str(report)),
783 "2 files reformatted, 2 files left unchanged, 2 files failed to"
786 self.assertEqual(report.return_code, 123)
787 report.path_ignored(Path("wat"), "no match")
788 self.assertEqual(len(out_lines), 2)
789 self.assertEqual(len(err_lines), 2)
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.done(Path("f4"), black.Changed.NO)
797 self.assertEqual(len(out_lines), 2)
798 self.assertEqual(len(err_lines), 2)
800 unstyle(str(report)),
801 "2 files reformatted, 3 files left unchanged, 2 files failed to"
804 self.assertEqual(report.return_code, 123)
807 unstyle(str(report)),
808 "2 files would be reformatted, 3 files would be left unchanged, 2"
809 " files would fail to reformat.",
814 unstyle(str(report)),
815 "2 files would be reformatted, 3 files would be left unchanged, 2"
816 " files would fail to reformat.",
819 def test_lib2to3_parse(self) -> None:
820 with self.assertRaises(black.InvalidInput):
821 black.lib2to3_parse("invalid syntax")
824 black.lib2to3_parse(straddling)
825 black.lib2to3_parse(straddling, {TargetVersion.PY36})
828 with self.assertRaises(black.InvalidInput):
829 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
831 py3_only = "exec(x, end=y)"
832 black.lib2to3_parse(py3_only)
833 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
835 def test_get_features_used_decorator(self) -> None:
836 # Test the feature detection of new decorator syntax
837 # since this makes some test cases of test_get_features_used()
838 # fails if it fails, this is tested first so that a useful case
840 simples, relaxed = read_data("miscellaneous", "decorators")
841 # skip explanation comments at the top of the file
842 for simple_test in simples.split("##")[1:]:
843 node = black.lib2to3_parse(simple_test)
844 decorator = str(node.children[0].children[0]).strip()
846 Feature.RELAXED_DECORATORS,
847 black.get_features_used(node),
849 f"decorator '{decorator}' follows python<=3.8 syntax"
850 "but is detected as 3.9+"
851 # f"The full node is\n{node!r}"
854 # skip the '# output' comment at the top of the output part
855 for relaxed_test in relaxed.split("##")[1:]:
856 node = black.lib2to3_parse(relaxed_test)
857 decorator = str(node.children[0].children[0]).strip()
859 Feature.RELAXED_DECORATORS,
860 black.get_features_used(node),
862 f"decorator '{decorator}' uses python3.9+ syntax"
863 "but is detected as python<=3.8"
864 # f"The full node is\n{node!r}"
868 def test_get_features_used(self) -> None:
869 node = black.lib2to3_parse("def f(*, arg): ...\n")
870 self.assertEqual(black.get_features_used(node), set())
871 node = black.lib2to3_parse("def f(*, arg,): ...\n")
872 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
873 node = black.lib2to3_parse("f(*arg,)\n")
875 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
877 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
878 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
879 node = black.lib2to3_parse("123_456\n")
880 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
881 node = black.lib2to3_parse("123456\n")
882 self.assertEqual(black.get_features_used(node), set())
883 source, expected = read_data("simple_cases", "function")
884 node = black.lib2to3_parse(source)
885 expected_features = {
886 Feature.TRAILING_COMMA_IN_CALL,
887 Feature.TRAILING_COMMA_IN_DEF,
890 self.assertEqual(black.get_features_used(node), expected_features)
891 node = black.lib2to3_parse(expected)
892 self.assertEqual(black.get_features_used(node), expected_features)
893 source, expected = read_data("simple_cases", "expression")
894 node = black.lib2to3_parse(source)
895 self.assertEqual(black.get_features_used(node), set())
896 node = black.lib2to3_parse(expected)
897 self.assertEqual(black.get_features_used(node), set())
898 node = black.lib2to3_parse("lambda a, /, b: ...")
899 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
900 node = black.lib2to3_parse("def fn(a, /, b): ...")
901 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
902 node = black.lib2to3_parse("def fn(): yield a, b")
903 self.assertEqual(black.get_features_used(node), set())
904 node = black.lib2to3_parse("def fn(): return a, b")
905 self.assertEqual(black.get_features_used(node), set())
906 node = black.lib2to3_parse("def fn(): yield *b, c")
907 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
908 node = black.lib2to3_parse("def fn(): return a, *b, c")
909 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
910 node = black.lib2to3_parse("x = a, *b, c")
911 self.assertEqual(black.get_features_used(node), set())
912 node = black.lib2to3_parse("x: Any = regular")
913 self.assertEqual(black.get_features_used(node), set())
914 node = black.lib2to3_parse("x: Any = (regular, regular)")
915 self.assertEqual(black.get_features_used(node), set())
916 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
917 self.assertEqual(black.get_features_used(node), set())
918 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
920 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
922 node = black.lib2to3_parse("try: pass\nexcept Something: pass")
923 self.assertEqual(black.get_features_used(node), set())
924 node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass")
925 self.assertEqual(black.get_features_used(node), set())
926 node = black.lib2to3_parse("try: pass\nexcept *Group: pass")
927 self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR})
928 node = black.lib2to3_parse("a[*b]")
929 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
930 node = black.lib2to3_parse("a[x, *y(), z] = t")
931 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
932 node = black.lib2to3_parse("def fn(*args: *T): pass")
933 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
935 def test_get_features_used_for_future_flags(self) -> None:
936 for src, features in [
937 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
939 "from __future__ import (other, annotations)",
940 {Feature.FUTURE_ANNOTATIONS},
942 ("a = 1 + 2\nfrom something import annotations", set()),
943 ("from __future__ import x, y", set()),
945 with self.subTest(src=src, features=features):
946 node = black.lib2to3_parse(src)
947 future_imports = black.get_future_imports(node)
949 black.get_features_used(node, future_imports=future_imports),
953 def test_get_future_imports(self) -> None:
954 node = black.lib2to3_parse("\n")
955 self.assertEqual(set(), black.get_future_imports(node))
956 node = black.lib2to3_parse("from __future__ import black\n")
957 self.assertEqual({"black"}, black.get_future_imports(node))
958 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
959 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
960 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
961 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
962 node = black.lib2to3_parse(
963 "from __future__ import multiple\nfrom __future__ import imports\n"
965 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
966 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
967 self.assertEqual({"black"}, black.get_future_imports(node))
968 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
969 self.assertEqual({"black"}, black.get_future_imports(node))
970 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
971 self.assertEqual(set(), black.get_future_imports(node))
972 node = black.lib2to3_parse("from some.module import black\n")
973 self.assertEqual(set(), black.get_future_imports(node))
974 node = black.lib2to3_parse(
975 "from __future__ import unicode_literals as _unicode_literals"
977 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
978 node = black.lib2to3_parse(
979 "from __future__ import unicode_literals as _lol, print"
981 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
983 @pytest.mark.incompatible_with_mypyc
984 def test_debug_visitor(self) -> None:
985 source, _ = read_data("miscellaneous", "debug_visitor")
986 expected, _ = read_data("miscellaneous", "debug_visitor.out")
990 def out(msg: str, **kwargs: Any) -> None:
991 out_lines.append(msg)
993 def err(msg: str, **kwargs: Any) -> None:
994 err_lines.append(msg)
996 with patch("black.debug.out", out):
997 DebugVisitor.show(source)
998 actual = "\n".join(out_lines) + "\n"
1000 if expected != actual:
1001 log_name = black.dump_to_file(*out_lines)
1005 f"AST print out is different. Actual version dumped to {log_name}",
1008 def test_format_file_contents(self) -> None:
1011 with self.assertRaises(black.NothingChanged):
1012 black.format_file_contents(empty, mode=mode, fast=False)
1014 with self.assertRaises(black.NothingChanged):
1015 black.format_file_contents(just_nl, mode=mode, fast=False)
1016 same = "j = [1, 2, 3]\n"
1017 with self.assertRaises(black.NothingChanged):
1018 black.format_file_contents(same, mode=mode, fast=False)
1019 different = "j = [1,2,3]"
1021 actual = black.format_file_contents(different, mode=mode, fast=False)
1022 self.assertEqual(expected, actual)
1023 invalid = "return if you can"
1024 with self.assertRaises(black.InvalidInput) as e:
1025 black.format_file_contents(invalid, mode=mode, fast=False)
1026 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
1028 mode = black.Mode(preview=True)
1030 with self.assertRaises(black.NothingChanged):
1031 black.format_file_contents(just_crlf, mode=mode, fast=False)
1032 just_whitespace_nl = "\n\t\n \n\t \n \t\n\n"
1033 actual = black.format_file_contents(just_whitespace_nl, mode=mode, fast=False)
1034 self.assertEqual("\n", actual)
1035 just_whitespace_crlf = "\r\n\t\r\n \r\n\t \r\n \t\r\n\r\n"
1036 actual = black.format_file_contents(just_whitespace_crlf, mode=mode, fast=False)
1037 self.assertEqual("\r\n", actual)
1039 def test_endmarker(self) -> None:
1040 n = black.lib2to3_parse("\n")
1041 self.assertEqual(n.type, black.syms.file_input)
1042 self.assertEqual(len(n.children), 1)
1043 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
1045 @pytest.mark.incompatible_with_mypyc
1046 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
1047 def test_assertFormatEqual(self) -> None:
1051 def out(msg: str, **kwargs: Any) -> None:
1052 out_lines.append(msg)
1054 def err(msg: str, **kwargs: Any) -> None:
1055 err_lines.append(msg)
1057 with patch("black.output._out", out), patch("black.output._err", err):
1058 with self.assertRaises(AssertionError):
1059 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
1061 out_str = "".join(out_lines)
1062 self.assertIn("Expected tree:", out_str)
1063 self.assertIn("Actual tree:", out_str)
1064 self.assertEqual("".join(err_lines), "")
1067 @patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1068 def test_works_in_mono_process_only_environment(self) -> None:
1069 with cache_dir() as workspace:
1071 (workspace / "one.py").resolve(),
1072 (workspace / "two.py").resolve(),
1074 f.write_text('print("hello")\n')
1075 self.invokeBlack([str(workspace)])
1078 def test_check_diff_use_together(self) -> None:
1080 # Files which will be reformatted.
1081 src1 = get_case_path("miscellaneous", "string_quotes")
1082 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1083 # Files which will not be reformatted.
1084 src2 = get_case_path("simple_cases", "composition")
1085 self.invokeBlack([str(src2), "--diff", "--check"])
1086 # Multi file command.
1087 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1089 def test_no_src_fails(self) -> None:
1091 self.invokeBlack([], exit_code=1)
1093 def test_src_and_code_fails(self) -> None:
1095 self.invokeBlack([".", "-c", "0"], exit_code=1)
1097 def test_broken_symlink(self) -> None:
1098 with cache_dir() as workspace:
1099 symlink = workspace / "broken_link.py"
1101 symlink.symlink_to("nonexistent.py")
1102 except (OSError, NotImplementedError) as e:
1103 self.skipTest(f"Can't create symlinks: {e}")
1104 self.invokeBlack([str(workspace.resolve())])
1106 def test_single_file_force_pyi(self) -> None:
1107 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1108 contents, expected = read_data("miscellaneous", "force_pyi")
1109 with cache_dir() as workspace:
1110 path = (workspace / "file.py").resolve()
1111 with open(path, "w") as fh:
1113 self.invokeBlack([str(path), "--pyi"])
1114 with open(path, "r") as fh:
1116 # verify cache with --pyi is separate
1117 pyi_cache = black.read_cache(pyi_mode)
1118 self.assertIn(str(path), pyi_cache)
1119 normal_cache = black.read_cache(DEFAULT_MODE)
1120 self.assertNotIn(str(path), normal_cache)
1121 self.assertFormatEqual(expected, actual)
1122 black.assert_equivalent(contents, actual)
1123 black.assert_stable(contents, actual, pyi_mode)
1126 def test_multi_file_force_pyi(self) -> None:
1127 reg_mode = DEFAULT_MODE
1128 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1129 contents, expected = read_data("miscellaneous", "force_pyi")
1130 with cache_dir() as workspace:
1132 (workspace / "file1.py").resolve(),
1133 (workspace / "file2.py").resolve(),
1136 with open(path, "w") as fh:
1138 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1140 with open(path, "r") as fh:
1142 self.assertEqual(actual, expected)
1143 # verify cache with --pyi is separate
1144 pyi_cache = black.read_cache(pyi_mode)
1145 normal_cache = black.read_cache(reg_mode)
1147 self.assertIn(str(path), pyi_cache)
1148 self.assertNotIn(str(path), normal_cache)
1150 def test_pipe_force_pyi(self) -> None:
1151 source, expected = read_data("miscellaneous", "force_pyi")
1152 result = CliRunner().invoke(
1153 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1155 self.assertEqual(result.exit_code, 0)
1156 actual = result.output
1157 self.assertFormatEqual(actual, expected)
1159 def test_single_file_force_py36(self) -> None:
1160 reg_mode = DEFAULT_MODE
1161 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1162 source, expected = read_data("miscellaneous", "force_py36")
1163 with cache_dir() as workspace:
1164 path = (workspace / "file.py").resolve()
1165 with open(path, "w") as fh:
1167 self.invokeBlack([str(path), *PY36_ARGS])
1168 with open(path, "r") as fh:
1170 # verify cache with --target-version is separate
1171 py36_cache = black.read_cache(py36_mode)
1172 self.assertIn(str(path), py36_cache)
1173 normal_cache = black.read_cache(reg_mode)
1174 self.assertNotIn(str(path), normal_cache)
1175 self.assertEqual(actual, expected)
1178 def test_multi_file_force_py36(self) -> None:
1179 reg_mode = DEFAULT_MODE
1180 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1181 source, expected = read_data("miscellaneous", "force_py36")
1182 with cache_dir() as workspace:
1184 (workspace / "file1.py").resolve(),
1185 (workspace / "file2.py").resolve(),
1188 with open(path, "w") as fh:
1190 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1192 with open(path, "r") as fh:
1194 self.assertEqual(actual, expected)
1195 # verify cache with --target-version is separate
1196 pyi_cache = black.read_cache(py36_mode)
1197 normal_cache = black.read_cache(reg_mode)
1199 self.assertIn(str(path), pyi_cache)
1200 self.assertNotIn(str(path), normal_cache)
1202 def test_pipe_force_py36(self) -> None:
1203 source, expected = read_data("miscellaneous", "force_py36")
1204 result = CliRunner().invoke(
1206 ["-", "-q", "--target-version=py36"],
1207 input=BytesIO(source.encode("utf8")),
1209 self.assertEqual(result.exit_code, 0)
1210 actual = result.output
1211 self.assertFormatEqual(actual, expected)
1213 @pytest.mark.incompatible_with_mypyc
1214 def test_reformat_one_with_stdin(self) -> None:
1216 "black.format_stdin_to_stdout",
1217 return_value=lambda *args, **kwargs: black.Changed.YES,
1219 report = MagicMock()
1224 write_back=black.WriteBack.YES,
1228 fsts.assert_called_once()
1229 report.done.assert_called_with(path, black.Changed.YES)
1231 @pytest.mark.incompatible_with_mypyc
1232 def test_reformat_one_with_stdin_filename(self) -> None:
1234 "black.format_stdin_to_stdout",
1235 return_value=lambda *args, **kwargs: black.Changed.YES,
1237 report = MagicMock()
1239 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1244 write_back=black.WriteBack.YES,
1248 fsts.assert_called_once_with(
1249 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1251 # __BLACK_STDIN_FILENAME__ should have been stripped
1252 report.done.assert_called_with(expected, black.Changed.YES)
1254 @pytest.mark.incompatible_with_mypyc
1255 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1257 "black.format_stdin_to_stdout",
1258 return_value=lambda *args, **kwargs: black.Changed.YES,
1260 report = MagicMock()
1262 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1267 write_back=black.WriteBack.YES,
1271 fsts.assert_called_once_with(
1273 write_back=black.WriteBack.YES,
1274 mode=replace(DEFAULT_MODE, is_pyi=True),
1276 # __BLACK_STDIN_FILENAME__ should have been stripped
1277 report.done.assert_called_with(expected, black.Changed.YES)
1279 @pytest.mark.incompatible_with_mypyc
1280 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1282 "black.format_stdin_to_stdout",
1283 return_value=lambda *args, **kwargs: black.Changed.YES,
1285 report = MagicMock()
1287 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1292 write_back=black.WriteBack.YES,
1296 fsts.assert_called_once_with(
1298 write_back=black.WriteBack.YES,
1299 mode=replace(DEFAULT_MODE, is_ipynb=True),
1301 # __BLACK_STDIN_FILENAME__ should have been stripped
1302 report.done.assert_called_with(expected, black.Changed.YES)
1304 @pytest.mark.incompatible_with_mypyc
1305 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1307 "black.format_stdin_to_stdout",
1308 return_value=lambda *args, **kwargs: black.Changed.YES,
1310 report = MagicMock()
1311 # Even with an existing file, since we are forcing stdin, black
1312 # should output to stdout and not modify the file inplace
1313 p = THIS_DIR / "data" / "simple_cases" / "collections.py"
1314 # Make sure is_file actually returns True
1315 self.assertTrue(p.is_file())
1316 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1321 write_back=black.WriteBack.YES,
1325 fsts.assert_called_once()
1326 # __BLACK_STDIN_FILENAME__ should have been stripped
1327 report.done.assert_called_with(expected, black.Changed.YES)
1329 def test_reformat_one_with_stdin_empty(self) -> None:
1336 (" \t\r\n\t ", "\r\n"),
1340 output: io.StringIO, io_TextIOWrapper: Type[io.TextIOWrapper]
1341 ) -> Callable[[Any, Any], io.TextIOWrapper]:
1342 def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper:
1343 if args == (sys.stdout.buffer,):
1344 # It's `format_stdin_to_stdout()` calling `io.TextIOWrapper()`,
1345 # return our mock object.
1347 # It's something else (i.e. `decode_bytes()`) calling
1348 # `io.TextIOWrapper()`, pass through to the original implementation.
1349 # See discussion in https://github.com/psf/black/pull/2489
1350 return io_TextIOWrapper(*args, **kwargs)
1354 mode = black.Mode(preview=True)
1355 for content, expected in cases:
1356 output = io.StringIO()
1357 io_TextIOWrapper = io.TextIOWrapper
1359 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1361 black.format_stdin_to_stdout(
1364 write_back=black.WriteBack.YES,
1367 except io.UnsupportedOperation:
1368 pass # StringIO does not support detach
1369 assert output.getvalue() == expected
1371 # An empty string is the only test case for `preview=False`
1372 output = io.StringIO()
1373 io_TextIOWrapper = io.TextIOWrapper
1374 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1376 black.format_stdin_to_stdout(
1379 write_back=black.WriteBack.YES,
1382 except io.UnsupportedOperation:
1383 pass # StringIO does not support detach
1384 assert output.getvalue() == ""
1386 def test_invalid_cli_regex(self) -> None:
1387 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1388 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1390 def test_required_version_matches_version(self) -> None:
1392 ["--required-version", black.__version__, "-c", "0"],
1397 def test_required_version_matches_partial_version(self) -> None:
1399 ["--required-version", black.__version__.split(".")[0], "-c", "0"],
1404 def test_required_version_does_not_match_on_minor_version(self) -> None:
1406 ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
1411 def test_required_version_does_not_match_version(self) -> None:
1412 result = BlackRunner().invoke(
1414 ["--required-version", "20.99b", "-c", "0"],
1416 self.assertEqual(result.exit_code, 1)
1417 self.assertIn("required version", result.stderr)
1419 def test_preserves_line_endings(self) -> None:
1420 with TemporaryDirectory() as workspace:
1421 test_file = Path(workspace) / "test.py"
1422 for nl in ["\n", "\r\n"]:
1423 contents = nl.join(["def f( ):", " pass"])
1424 test_file.write_bytes(contents.encode())
1425 ff(test_file, write_back=black.WriteBack.YES)
1426 updated_contents: bytes = test_file.read_bytes()
1427 self.assertIn(nl.encode(), updated_contents)
1429 self.assertNotIn(b"\r\n", updated_contents)
1431 def test_preserves_line_endings_via_stdin(self) -> None:
1432 for nl in ["\n", "\r\n"]:
1433 contents = nl.join(["def f( ):", " pass"])
1434 runner = BlackRunner()
1435 result = runner.invoke(
1436 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1438 self.assertEqual(result.exit_code, 0)
1439 output = result.stdout_bytes
1440 self.assertIn(nl.encode("utf8"), output)
1442 self.assertNotIn(b"\r\n", output)
1444 def test_normalize_line_endings(self) -> None:
1445 with TemporaryDirectory() as workspace:
1446 test_file = Path(workspace) / "test.py"
1447 for data, expected in (
1448 (b"c\r\nc\n ", b"c\r\nc\r\n"),
1449 (b"l\nl\r\n ", b"l\nl\n"),
1451 test_file.write_bytes(data)
1452 ff(test_file, write_back=black.WriteBack.YES)
1453 self.assertEqual(test_file.read_bytes(), expected)
1455 def test_assert_equivalent_different_asts(self) -> None:
1456 with self.assertRaises(AssertionError):
1457 black.assert_equivalent("{}", "None")
1459 def test_shhh_click(self) -> None:
1461 from click import _unicodefun # type: ignore
1463 self.skipTest("Incompatible Click version")
1465 if not hasattr(_unicodefun, "_verify_python_env"):
1466 self.skipTest("Incompatible Click version")
1468 # First, let's see if Click is crashing with a preferred ASCII charset.
1469 with patch("locale.getpreferredencoding") as gpe:
1470 gpe.return_value = "ASCII"
1471 with self.assertRaises(RuntimeError):
1472 _unicodefun._verify_python_env()
1473 # Now, let's silence Click...
1475 # ...and confirm it's silent.
1476 with patch("locale.getpreferredencoding") as gpe:
1477 gpe.return_value = "ASCII"
1479 _unicodefun._verify_python_env()
1480 except RuntimeError as re:
1481 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1483 def test_root_logger_not_used_directly(self) -> None:
1484 def fail(*args: Any, **kwargs: Any) -> None:
1485 self.fail("Record created with root logger")
1487 with patch.multiple(
1496 ff(THIS_DIR / "util.py")
1498 def test_invalid_config_return_code(self) -> None:
1499 tmp_file = Path(black.dump_to_file())
1501 tmp_config = Path(black.dump_to_file())
1503 args = ["--config", str(tmp_config), str(tmp_file)]
1504 self.invokeBlack(args, exit_code=2, ignore_config=False)
1508 def test_parse_pyproject_toml(self) -> None:
1509 test_toml_file = THIS_DIR / "test.toml"
1510 config = black.parse_pyproject_toml(str(test_toml_file))
1511 self.assertEqual(config["verbose"], 1)
1512 self.assertEqual(config["check"], "no")
1513 self.assertEqual(config["diff"], "y")
1514 self.assertEqual(config["color"], True)
1515 self.assertEqual(config["line_length"], 79)
1516 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1517 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1518 self.assertEqual(config["exclude"], r"\.pyi?$")
1519 self.assertEqual(config["include"], r"\.py?$")
1521 def test_parse_pyproject_toml_project_metadata(self) -> None:
1522 for test_toml, expected in [
1523 ("only_black_pyproject.toml", ["py310"]),
1524 ("only_metadata_pyproject.toml", ["py37", "py38", "py39", "py310"]),
1525 ("neither_pyproject.toml", None),
1526 ("both_pyproject.toml", ["py310"]),
1528 test_toml_file = THIS_DIR / "data" / "project_metadata" / test_toml
1529 config = black.parse_pyproject_toml(str(test_toml_file))
1530 self.assertEqual(config.get("target_version"), expected)
1532 def test_infer_target_version(self) -> None:
1533 for version, expected in [
1534 ("3.6", [TargetVersion.PY36]),
1535 ("3.11.0rc1", [TargetVersion.PY311]),
1536 (">=3.10", [TargetVersion.PY310, TargetVersion.PY311]),
1537 (">=3.10.6", [TargetVersion.PY310, TargetVersion.PY311]),
1538 ("<3.6", [TargetVersion.PY33, TargetVersion.PY34, TargetVersion.PY35]),
1539 (">3.7,<3.10", [TargetVersion.PY38, TargetVersion.PY39]),
1540 (">3.7,!=3.8,!=3.9", [TargetVersion.PY310, TargetVersion.PY311]),
1542 "> 3.9.4, != 3.10.3",
1543 [TargetVersion.PY39, TargetVersion.PY310, TargetVersion.PY311],
1553 TargetVersion.PY310,
1554 TargetVersion.PY311,
1567 TargetVersion.PY310,
1568 TargetVersion.PY311,
1571 ("==3.8.*", [TargetVersion.PY38]),
1575 ("==invalid", None),
1576 (">3.9,!=invalid", None),
1581 (">3.10,<3.11", None),
1583 test_toml = {"project": {"requires-python": version}}
1584 result = black.files.infer_target_version(test_toml)
1585 self.assertEqual(result, expected)
1587 def test_read_pyproject_toml(self) -> None:
1588 test_toml_file = THIS_DIR / "test.toml"
1589 fake_ctx = FakeContext()
1590 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1591 config = fake_ctx.default_map
1592 self.assertEqual(config["verbose"], "1")
1593 self.assertEqual(config["check"], "no")
1594 self.assertEqual(config["diff"], "y")
1595 self.assertEqual(config["color"], "True")
1596 self.assertEqual(config["line_length"], "79")
1597 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1598 self.assertEqual(config["exclude"], r"\.pyi?$")
1599 self.assertEqual(config["include"], r"\.py?$")
1601 @pytest.mark.incompatible_with_mypyc
1602 def test_find_project_root(self) -> None:
1603 with TemporaryDirectory() as workspace:
1604 root = Path(workspace)
1605 test_dir = root / "test"
1608 src_dir = root / "src"
1611 root_pyproject = root / "pyproject.toml"
1612 root_pyproject.touch()
1613 src_pyproject = src_dir / "pyproject.toml"
1614 src_pyproject.touch()
1615 src_python = src_dir / "foo.py"
1619 black.find_project_root((src_dir, test_dir)),
1620 (root.resolve(), "pyproject.toml"),
1623 black.find_project_root((src_dir,)),
1624 (src_dir.resolve(), "pyproject.toml"),
1627 black.find_project_root((src_python,)),
1628 (src_dir.resolve(), "pyproject.toml"),
1631 with change_directory(test_dir):
1633 black.find_project_root(("-",), stdin_filename="../src/a.py"),
1634 (src_dir.resolve(), "pyproject.toml"),
1638 "black.files.find_user_pyproject_toml",
1640 def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
1641 find_user_pyproject_toml.side_effect = RuntimeError()
1643 with redirect_stderr(io.StringIO()) as stderr:
1644 result = black.files.find_pyproject_toml(
1645 path_search_start=(str(Path.cwd().root),)
1648 assert result is None
1649 err = stderr.getvalue()
1650 assert "Ignoring user configuration" in err
1653 "black.files.find_user_pyproject_toml",
1654 black.files.find_user_pyproject_toml.__wrapped__,
1656 def test_find_user_pyproject_toml_linux(self) -> None:
1657 if system() == "Windows":
1660 # Test if XDG_CONFIG_HOME is checked
1661 with TemporaryDirectory() as workspace:
1662 tmp_user_config = Path(workspace) / "black"
1663 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1665 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1668 # Test fallback for XDG_CONFIG_HOME
1669 with patch.dict("os.environ"):
1670 os.environ.pop("XDG_CONFIG_HOME", None)
1671 fallback_user_config = Path("~/.config").expanduser() / "black"
1673 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1676 def test_find_user_pyproject_toml_windows(self) -> None:
1677 if system() != "Windows":
1680 user_config_path = Path.home() / ".black"
1682 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1685 def test_bpo_33660_workaround(self) -> None:
1686 if system() == "Windows":
1689 # https://bugs.python.org/issue33660
1691 with change_directory(root):
1692 path = Path("workspace") / "project"
1693 report = black.Report(verbose=True)
1694 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1695 self.assertEqual(normalized_path, "workspace/project")
1697 def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
1698 if system() != "Windows":
1701 with TemporaryDirectory() as workspace:
1702 root = Path(workspace)
1703 junction_dir = root / "junction"
1704 junction_target_outside_of_root = root / ".."
1705 os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
1707 report = black.Report(verbose=True)
1708 normalized_path = black.normalize_path_maybe_ignore(
1709 junction_dir, root, report
1711 # Manually delete for Python < 3.8
1712 os.system(f"rmdir {junction_dir}")
1714 self.assertEqual(normalized_path, None)
1716 def test_newline_comment_interaction(self) -> None:
1717 source = "class A:\\\r\n# type: ignore\n pass\n"
1718 output = black.format_str(source, mode=DEFAULT_MODE)
1719 black.assert_stable(source, output, mode=DEFAULT_MODE)
1721 def test_bpo_2142_workaround(self) -> None:
1722 # https://bugs.python.org/issue2142
1724 source, _ = read_data("miscellaneous", "missing_final_newline")
1725 # read_data adds a trailing newline
1726 source = source.rstrip()
1727 expected, _ = read_data("miscellaneous", "missing_final_newline.diff")
1728 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1729 diff_header = re.compile(
1730 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1731 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1734 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1735 self.assertEqual(result.exit_code, 0)
1738 actual = result.output
1739 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1740 self.assertEqual(actual, expected)
1743 def compare_results(
1744 result: click.testing.Result, expected_value: str, expected_exit_code: int
1746 """Helper method to test the value and exit code of a click Result."""
1748 result.output == expected_value
1749 ), "The output did not match the expected value."
1750 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1752 def test_code_option(self) -> None:
1753 """Test the code option with no changes."""
1754 code = 'print("Hello world")\n'
1755 args = ["--code", code]
1756 result = CliRunner().invoke(black.main, args)
1758 self.compare_results(result, code, 0)
1760 def test_code_option_changed(self) -> None:
1761 """Test the code option when changes are required."""
1762 code = "print('hello world')"
1763 formatted = black.format_str(code, mode=DEFAULT_MODE)
1765 args = ["--code", code]
1766 result = CliRunner().invoke(black.main, args)
1768 self.compare_results(result, formatted, 0)
1770 def test_code_option_check(self) -> None:
1771 """Test the code option when check is passed."""
1772 args = ["--check", "--code", 'print("Hello world")\n']
1773 result = CliRunner().invoke(black.main, args)
1774 self.compare_results(result, "", 0)
1776 def test_code_option_check_changed(self) -> None:
1777 """Test the code option when changes are required, and check is passed."""
1778 args = ["--check", "--code", "print('hello world')"]
1779 result = CliRunner().invoke(black.main, args)
1780 self.compare_results(result, "", 1)
1782 def test_code_option_diff(self) -> None:
1783 """Test the code option when diff is passed."""
1784 code = "print('hello world')"
1785 formatted = black.format_str(code, mode=DEFAULT_MODE)
1786 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1788 args = ["--diff", "--code", code]
1789 result = CliRunner().invoke(black.main, args)
1791 # Remove time from diff
1792 output = DIFF_TIME.sub("", result.output)
1794 assert output == result_diff, "The output did not match the expected value."
1795 assert result.exit_code == 0, "The exit code is incorrect."
1797 def test_code_option_color_diff(self) -> None:
1798 """Test the code option when color and diff are passed."""
1799 code = "print('hello world')"
1800 formatted = black.format_str(code, mode=DEFAULT_MODE)
1802 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1803 result_diff = color_diff(result_diff)
1805 args = ["--diff", "--color", "--code", code]
1806 result = CliRunner().invoke(black.main, args)
1808 # Remove time from diff
1809 output = DIFF_TIME.sub("", result.output)
1811 assert output == result_diff, "The output did not match the expected value."
1812 assert result.exit_code == 0, "The exit code is incorrect."
1814 @pytest.mark.incompatible_with_mypyc
1815 def test_code_option_safe(self) -> None:
1816 """Test that the code option throws an error when the sanity checks fail."""
1817 # Patch black.assert_equivalent to ensure the sanity checks fail
1818 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1819 code = 'print("Hello world")'
1820 error_msg = f"{code}\nerror: cannot format <string>: \n"
1822 args = ["--safe", "--code", code]
1823 result = CliRunner().invoke(black.main, args)
1825 self.compare_results(result, error_msg, 123)
1827 def test_code_option_fast(self) -> None:
1828 """Test that the code option ignores errors when the sanity checks fail."""
1829 # Patch black.assert_equivalent to ensure the sanity checks fail
1830 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1831 code = 'print("Hello world")'
1832 formatted = black.format_str(code, mode=DEFAULT_MODE)
1834 args = ["--fast", "--code", code]
1835 result = CliRunner().invoke(black.main, args)
1837 self.compare_results(result, formatted, 0)
1839 @pytest.mark.incompatible_with_mypyc
1840 def test_code_option_config(self) -> None:
1842 Test that the code option finds the pyproject.toml in the current directory.
1844 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1845 args = ["--code", "print"]
1846 # This is the only directory known to contain a pyproject.toml
1847 with change_directory(PROJECT_ROOT):
1848 CliRunner().invoke(black.main, args)
1849 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1852 len(parse.mock_calls) >= 1
1853 ), "Expected config parse to be called with the current directory."
1855 _, call_args, _ = parse.mock_calls[0]
1857 call_args[0].lower() == str(pyproject_path).lower()
1858 ), "Incorrect config loaded."
1860 @pytest.mark.incompatible_with_mypyc
1861 def test_code_option_parent_config(self) -> None:
1863 Test that the code option finds the pyproject.toml in the parent directory.
1865 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1866 with change_directory(THIS_DIR):
1867 args = ["--code", "print"]
1868 CliRunner().invoke(black.main, args)
1870 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1872 len(parse.mock_calls) >= 1
1873 ), "Expected config parse to be called with the current directory."
1875 _, call_args, _ = parse.mock_calls[0]
1877 call_args[0].lower() == str(pyproject_path).lower()
1878 ), "Incorrect config loaded."
1880 def test_for_handled_unexpected_eof_error(self) -> None:
1882 Test that an unexpected EOF SyntaxError is nicely presented.
1884 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1885 black.lib2to3_parse("print(", {})
1887 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1889 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1890 with pytest.raises(AssertionError) as err:
1891 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1894 # Unfortunately the SyntaxError message has changed in newer versions so we
1895 # can't match it directly.
1896 err.match("invalid character")
1897 err.match(r"\(<unknown>, line 1\)")
1901 def test_get_cache_dir(
1904 monkeypatch: pytest.MonkeyPatch,
1906 # Create multiple cache directories
1907 workspace1 = tmp_path / "ws1"
1909 workspace2 = tmp_path / "ws2"
1912 # Force user_cache_dir to use the temporary directory for easier assertions
1913 patch_user_cache_dir = patch(
1914 target="black.cache.user_cache_dir",
1916 return_value=str(workspace1),
1919 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1920 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1921 with patch_user_cache_dir:
1922 assert get_cache_dir() == workspace1
1924 # If it is set, use the path provided in the env var.
1925 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1926 assert get_cache_dir() == workspace2
1928 def test_cache_broken_file(self) -> None:
1930 with cache_dir() as workspace:
1931 cache_file = get_cache_file(mode)
1932 cache_file.write_text("this is not a pickle")
1933 assert black.read_cache(mode) == {}
1934 src = (workspace / "test.py").resolve()
1935 src.write_text("print('hello')")
1936 invokeBlack([str(src)])
1937 cache = black.read_cache(mode)
1938 assert str(src) in cache
1940 def test_cache_single_file_already_cached(self) -> None:
1942 with cache_dir() as workspace:
1943 src = (workspace / "test.py").resolve()
1944 src.write_text("print('hello')")
1945 black.write_cache({}, [src], mode)
1946 invokeBlack([str(src)])
1947 assert src.read_text() == "print('hello')"
1950 def test_cache_multiple_files(self) -> None:
1952 with cache_dir() as workspace, patch(
1953 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
1955 one = (workspace / "one.py").resolve()
1956 with one.open("w") as fobj:
1957 fobj.write("print('hello')")
1958 two = (workspace / "two.py").resolve()
1959 with two.open("w") as fobj:
1960 fobj.write("print('hello')")
1961 black.write_cache({}, [one], mode)
1962 invokeBlack([str(workspace)])
1963 with one.open("r") as fobj:
1964 assert fobj.read() == "print('hello')"
1965 with two.open("r") as fobj:
1966 assert fobj.read() == 'print("hello")\n'
1967 cache = black.read_cache(mode)
1968 assert str(one) in cache
1969 assert str(two) in cache
1971 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1972 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1974 with cache_dir() as workspace:
1975 src = (workspace / "test.py").resolve()
1976 with src.open("w") as fobj:
1977 fobj.write("print('hello')")
1978 with patch("black.read_cache") as read_cache, patch(
1981 cmd = [str(src), "--diff"]
1983 cmd.append("--color")
1985 cache_file = get_cache_file(mode)
1986 assert cache_file.exists() is False
1987 write_cache.assert_not_called()
1988 read_cache.assert_not_called()
1990 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1992 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1993 with cache_dir() as workspace:
1994 for tag in range(0, 4):
1995 src = (workspace / f"test{tag}.py").resolve()
1996 with src.open("w") as fobj:
1997 fobj.write("print('hello')")
1999 "black.concurrency.Manager", wraps=multiprocessing.Manager
2001 cmd = ["--diff", str(workspace)]
2003 cmd.append("--color")
2004 invokeBlack(cmd, exit_code=0)
2005 # this isn't quite doing what we want, but if it _isn't_
2006 # called then we cannot be using the lock it provides
2009 def test_no_cache_when_stdin(self) -> None:
2012 result = CliRunner().invoke(
2013 black.main, ["-"], input=BytesIO(b"print('hello')")
2015 assert not result.exit_code
2016 cache_file = get_cache_file(mode)
2017 assert not cache_file.exists()
2019 def test_read_cache_no_cachefile(self) -> None:
2022 assert black.read_cache(mode) == {}
2024 def test_write_cache_read_cache(self) -> None:
2026 with cache_dir() as workspace:
2027 src = (workspace / "test.py").resolve()
2029 black.write_cache({}, [src], mode)
2030 cache = black.read_cache(mode)
2031 assert str(src) in cache
2032 assert cache[str(src)] == black.get_cache_info(src)
2034 def test_filter_cached(self) -> None:
2035 with TemporaryDirectory() as workspace:
2036 path = Path(workspace)
2037 uncached = (path / "uncached").resolve()
2038 cached = (path / "cached").resolve()
2039 cached_but_changed = (path / "changed").resolve()
2042 cached_but_changed.touch()
2044 str(cached): black.get_cache_info(cached),
2045 str(cached_but_changed): (0.0, 0),
2047 todo, done = black.cache.filter_cached(
2048 cache, {uncached, cached, cached_but_changed}
2050 assert todo == {uncached, cached_but_changed}
2051 assert done == {cached}
2053 def test_write_cache_creates_directory_if_needed(self) -> None:
2055 with cache_dir(exists=False) as workspace:
2056 assert not workspace.exists()
2057 black.write_cache({}, [], mode)
2058 assert workspace.exists()
2061 def test_failed_formatting_does_not_get_cached(self) -> None:
2063 with cache_dir() as workspace, patch(
2064 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
2066 failing = (workspace / "failing.py").resolve()
2067 with failing.open("w") as fobj:
2068 fobj.write("not actually python")
2069 clean = (workspace / "clean.py").resolve()
2070 with clean.open("w") as fobj:
2071 fobj.write('print("hello")\n')
2072 invokeBlack([str(workspace)], exit_code=123)
2073 cache = black.read_cache(mode)
2074 assert str(failing) not in cache
2075 assert str(clean) in cache
2077 def test_write_cache_write_fail(self) -> None:
2079 with cache_dir(), patch.object(Path, "open") as mock:
2080 mock.side_effect = OSError
2081 black.write_cache({}, [], mode)
2083 def test_read_cache_line_lengths(self) -> None:
2085 short_mode = replace(DEFAULT_MODE, line_length=1)
2086 with cache_dir() as workspace:
2087 path = (workspace / "file.py").resolve()
2089 black.write_cache({}, [path], mode)
2090 one = black.read_cache(mode)
2091 assert str(path) in one
2092 two = black.read_cache(short_mode)
2093 assert str(path) not in two
2096 def assert_collected_sources(
2097 src: Sequence[Union[str, Path]],
2098 expected: Sequence[Union[str, Path]],
2100 ctx: Optional[FakeContext] = None,
2101 exclude: Optional[str] = None,
2102 include: Optional[str] = None,
2103 extend_exclude: Optional[str] = None,
2104 force_exclude: Optional[str] = None,
2105 stdin_filename: Optional[str] = None,
2107 gs_src = tuple(str(Path(s)) for s in src)
2108 gs_expected = [Path(s) for s in expected]
2109 gs_exclude = None if exclude is None else compile_pattern(exclude)
2110 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
2111 gs_extend_exclude = (
2112 None if extend_exclude is None else compile_pattern(extend_exclude)
2114 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
2115 collected = black.get_sources(
2116 ctx=ctx or FakeContext(),
2122 extend_exclude=gs_extend_exclude,
2123 force_exclude=gs_force_exclude,
2124 report=black.Report(),
2125 stdin_filename=stdin_filename,
2127 assert sorted(collected) == sorted(gs_expected)
2130 class TestFileCollection:
2131 def test_include_exclude(self) -> None:
2132 path = THIS_DIR / "data" / "include_exclude_tests"
2135 Path(path / "b/dont_exclude/a.py"),
2136 Path(path / "b/dont_exclude/a.pyi"),
2138 assert_collected_sources(
2142 exclude=r"/exclude/|/\.definitely_exclude/",
2145 def test_gitignore_used_as_default(self) -> None:
2146 base = Path(DATA_DIR / "include_exclude_tests")
2148 base / "b/.definitely_exclude/a.py",
2149 base / "b/.definitely_exclude/a.pyi",
2153 ctx.obj["root"] = base
2154 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
2156 def test_gitignore_used_on_multiple_sources(self) -> None:
2157 root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
2159 root / "dir1" / "b.py",
2160 root / "dir2" / "b.py",
2163 ctx.obj["root"] = root
2164 src = [root / "dir1", root / "dir2"]
2165 assert_collected_sources(src, expected, ctx=ctx)
2167 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2168 def test_exclude_for_issue_1572(self) -> None:
2169 # Exclude shouldn't touch files that were explicitly given to Black through the
2170 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
2171 # https://github.com/psf/black/issues/1572
2172 path = DATA_DIR / "include_exclude_tests"
2173 src = [path / "b/exclude/a.py"]
2174 expected = [path / "b/exclude/a.py"]
2175 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2177 def test_gitignore_exclude(self) -> None:
2178 path = THIS_DIR / "data" / "include_exclude_tests"
2179 include = re.compile(r"\.pyi?$")
2180 exclude = re.compile(r"")
2181 report = black.Report()
2182 gitignore = PathSpec.from_lines(
2183 "gitwildmatch", ["exclude/", ".definitely_exclude"]
2185 sources: List[Path] = []
2187 Path(path / "b/dont_exclude/a.py"),
2188 Path(path / "b/dont_exclude/a.pyi"),
2190 this_abs = THIS_DIR.resolve()
2192 black.gen_python_files(
2205 assert sorted(expected) == sorted(sources)
2207 def test_nested_gitignore(self) -> None:
2208 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
2209 include = re.compile(r"\.pyi?$")
2210 exclude = re.compile(r"")
2211 root_gitignore = black.files.get_gitignore(path)
2212 report = black.Report()
2213 expected: List[Path] = [
2214 Path(path / "x.py"),
2215 Path(path / "root/b.py"),
2216 Path(path / "root/c.py"),
2217 Path(path / "root/child/c.py"),
2219 this_abs = THIS_DIR.resolve()
2221 black.gen_python_files(
2229 {path: root_gitignore},
2234 assert sorted(expected) == sorted(sources)
2236 def test_nested_gitignore_directly_in_source_directory(self) -> None:
2237 # https://github.com/psf/black/issues/2598
2238 path = Path(DATA_DIR / "nested_gitignore_tests")
2239 src = Path(path / "root" / "child")
2240 expected = [src / "a.py", src / "c.py"]
2241 assert_collected_sources([src], expected)
2243 def test_invalid_gitignore(self) -> None:
2244 path = THIS_DIR / "data" / "invalid_gitignore_tests"
2245 empty_config = path / "pyproject.toml"
2246 result = BlackRunner().invoke(
2247 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2249 assert result.exit_code == 1
2250 assert result.stderr_bytes is not None
2252 gitignore = path / ".gitignore"
2253 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2255 def test_invalid_nested_gitignore(self) -> None:
2256 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
2257 empty_config = path / "pyproject.toml"
2258 result = BlackRunner().invoke(
2259 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2261 assert result.exit_code == 1
2262 assert result.stderr_bytes is not None
2264 gitignore = path / "a" / ".gitignore"
2265 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2267 def test_gitignore_that_ignores_subfolders(self) -> None:
2268 # If gitignore with */* is in root
2269 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir")
2270 expected = [root / "b.py"]
2272 ctx.obj["root"] = root
2273 assert_collected_sources([root], expected, ctx=ctx)
2275 # If .gitignore with */* is nested
2276 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2279 root / "subdir" / "b.py",
2282 ctx.obj["root"] = root
2283 assert_collected_sources([root], expected, ctx=ctx)
2285 # If command is executed from outer dir
2286 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2287 target = root / "subdir"
2288 expected = [target / "b.py"]
2290 ctx.obj["root"] = root
2291 assert_collected_sources([target], expected, ctx=ctx)
2293 def test_empty_include(self) -> None:
2294 path = DATA_DIR / "include_exclude_tests"
2297 Path(path / "b/exclude/a.pie"),
2298 Path(path / "b/exclude/a.py"),
2299 Path(path / "b/exclude/a.pyi"),
2300 Path(path / "b/dont_exclude/a.pie"),
2301 Path(path / "b/dont_exclude/a.py"),
2302 Path(path / "b/dont_exclude/a.pyi"),
2303 Path(path / "b/.definitely_exclude/a.pie"),
2304 Path(path / "b/.definitely_exclude/a.py"),
2305 Path(path / "b/.definitely_exclude/a.pyi"),
2306 Path(path / ".gitignore"),
2307 Path(path / "pyproject.toml"),
2309 # Setting exclude explicitly to an empty string to block .gitignore usage.
2310 assert_collected_sources(src, expected, include="", exclude="")
2312 def test_extend_exclude(self) -> None:
2313 path = DATA_DIR / "include_exclude_tests"
2316 Path(path / "b/exclude/a.py"),
2317 Path(path / "b/dont_exclude/a.py"),
2319 assert_collected_sources(
2320 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
2323 @pytest.mark.incompatible_with_mypyc
2324 def test_symlink_out_of_root_directory(self) -> None:
2326 root = THIS_DIR.resolve()
2328 include = re.compile(black.DEFAULT_INCLUDES)
2329 exclude = re.compile(black.DEFAULT_EXCLUDES)
2330 report = black.Report()
2331 gitignore = PathSpec.from_lines("gitwildmatch", [])
2332 # `child` should behave like a symlink which resolved path is clearly
2333 # outside of the `root` directory.
2334 path.iterdir.return_value = [child]
2335 child.resolve.return_value = Path("/a/b/c")
2336 child.as_posix.return_value = "/a/b/c"
2339 black.gen_python_files(
2352 except ValueError as ve:
2353 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
2354 path.iterdir.assert_called_once()
2355 child.resolve.assert_called_once()
2357 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2358 def test_get_sources_with_stdin(self) -> None:
2361 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2363 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2364 def test_get_sources_with_stdin_filename(self) -> None:
2366 stdin_filename = str(THIS_DIR / "data/collections.py")
2367 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2368 assert_collected_sources(
2371 exclude=r"/exclude/a\.py",
2372 stdin_filename=stdin_filename,
2375 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2376 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2377 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2378 # file being passed directly. This is the same as
2379 # test_exclude_for_issue_1572
2380 path = DATA_DIR / "include_exclude_tests"
2382 stdin_filename = str(path / "b/exclude/a.py")
2383 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2384 assert_collected_sources(
2387 exclude=r"/exclude/|a\.py",
2388 stdin_filename=stdin_filename,
2391 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2392 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2393 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2394 # file being passed directly. This is the same as
2395 # test_exclude_for_issue_1572
2397 path = THIS_DIR / "data" / "include_exclude_tests"
2398 stdin_filename = str(path / "b/exclude/a.py")
2399 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2400 assert_collected_sources(
2403 extend_exclude=r"/exclude/|a\.py",
2404 stdin_filename=stdin_filename,
2407 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2408 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2409 # Force exclude should exclude the file when passing it through
2411 path = THIS_DIR / "data" / "include_exclude_tests"
2412 stdin_filename = str(path / "b/exclude/a.py")
2413 assert_collected_sources(
2416 force_exclude=r"/exclude/|a\.py",
2417 stdin_filename=stdin_filename,
2422 with open(black.__file__, "r", encoding="utf-8") as _bf:
2423 black_source_lines = _bf.readlines()
2424 except UnicodeDecodeError:
2425 if not black.COMPILED:
2430 frame: types.FrameType, event: str, arg: Any
2431 ) -> Callable[[types.FrameType, str, Any], Any]:
2432 """Show function calls `from black/__init__.py` as they happen.
2434 Register this with `sys.settrace()` in a test you're debugging.
2439 stack = len(inspect.stack()) - 19
2441 filename = frame.f_code.co_filename
2442 lineno = frame.f_lineno
2443 func_sig_lineno = lineno - 1
2444 funcname = black_source_lines[func_sig_lineno].strip()
2445 while funcname.startswith("@"):
2446 func_sig_lineno += 1
2447 funcname = black_source_lines[func_sig_lineno].strip()
2448 if "black/__init__.py" in filename:
2449 print(f"{' ' * stack}{lineno}:{funcname}")