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 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
424 self.assertEqual(expected, actual, msg)
426 @patch("black.dump_to_file", dump_to_stderr)
427 def test_async_as_identifier(self) -> None:
428 source_path = get_case_path("miscellaneous", "async_as_identifier")
429 source, expected = read_data_from_file(source_path)
431 self.assertFormatEqual(expected, actual)
432 major, minor = sys.version_info[:2]
433 if major < 3 or (major <= 3 and minor < 7):
434 black.assert_equivalent(source, actual)
435 black.assert_stable(source, actual, DEFAULT_MODE)
436 # ensure black can parse this when the target is 3.6
437 self.invokeBlack([str(source_path), "--target-version", "py36"])
438 # but not on 3.7, because async/await is no longer an identifier
439 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
441 @patch("black.dump_to_file", dump_to_stderr)
442 def test_python37(self) -> None:
443 source_path = get_case_path("py_37", "python37")
444 source, expected = read_data_from_file(source_path)
446 self.assertFormatEqual(expected, actual)
447 major, minor = sys.version_info[:2]
448 if major > 3 or (major == 3 and minor >= 7):
449 black.assert_equivalent(source, actual)
450 black.assert_stable(source, actual, DEFAULT_MODE)
451 # ensure black can parse this when the target is 3.7
452 self.invokeBlack([str(source_path), "--target-version", "py37"])
453 # but not on 3.6, because we use async as a reserved keyword
454 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
456 def test_tab_comment_indentation(self) -> None:
457 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
458 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
459 self.assertFormatEqual(contents_spc, fs(contents_spc))
460 self.assertFormatEqual(contents_spc, fs(contents_tab))
462 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
463 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
464 self.assertFormatEqual(contents_spc, fs(contents_spc))
465 self.assertFormatEqual(contents_spc, fs(contents_tab))
467 # mixed tabs and spaces (valid Python 2 code)
468 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
469 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
470 self.assertFormatEqual(contents_spc, fs(contents_spc))
471 self.assertFormatEqual(contents_spc, fs(contents_tab))
473 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
474 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
475 self.assertFormatEqual(contents_spc, fs(contents_spc))
476 self.assertFormatEqual(contents_spc, fs(contents_tab))
478 def test_false_positive_symlink_output_issue_3384(self) -> None:
479 # Emulate the behavior when using the CLI (`black ./child --verbose`), which
480 # involves patching some `pathlib.Path` methods. In particular, `is_dir` is
481 # patched only on its first call: when checking if "./child" is a directory it
482 # should return True. The "./child" folder exists relative to the cwd when
483 # running from CLI, but fails when running the tests because cwd is different
484 project_root = Path(THIS_DIR / "data" / "nested_gitignore_tests")
485 working_directory = project_root / "root"
486 target_abspath = working_directory / "child"
488 src.relative_to(working_directory) for src in target_abspath.iterdir()
491 def mock_n_calls(responses: List[bool]) -> Callable[[], bool]:
492 def _mocked_calls() -> bool:
494 return responses.pop(0)
499 with patch("pathlib.Path.iterdir", return_value=target_contents), patch(
500 "pathlib.Path.cwd", return_value=working_directory
501 ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])):
503 ctx.obj["root"] = project_root
504 report = MagicMock(verbose=True)
510 include=DEFAULT_INCLUDE,
518 mock_args[1].startswith("is a symbolic link that points outside")
519 for _, mock_args, _ in report.path_ignored.mock_calls
520 ), "A symbolic link was reported."
521 report.path_ignored.assert_called_once_with(
522 Path("child", "b.py"), "matches a .gitignore file content"
525 def test_report_verbose(self) -> None:
526 report = Report(verbose=True)
530 def out(msg: str, **kwargs: Any) -> None:
531 out_lines.append(msg)
533 def err(msg: str, **kwargs: Any) -> None:
534 err_lines.append(msg)
536 with patch("black.output._out", out), patch("black.output._err", err):
537 report.done(Path("f1"), black.Changed.NO)
538 self.assertEqual(len(out_lines), 1)
539 self.assertEqual(len(err_lines), 0)
540 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
541 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
542 self.assertEqual(report.return_code, 0)
543 report.done(Path("f2"), black.Changed.YES)
544 self.assertEqual(len(out_lines), 2)
545 self.assertEqual(len(err_lines), 0)
546 self.assertEqual(out_lines[-1], "reformatted f2")
548 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
550 report.done(Path("f3"), black.Changed.CACHED)
551 self.assertEqual(len(out_lines), 3)
552 self.assertEqual(len(err_lines), 0)
554 out_lines[-1], "f3 wasn't modified on disk since last run."
557 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
559 self.assertEqual(report.return_code, 0)
561 self.assertEqual(report.return_code, 1)
563 report.failed(Path("e1"), "boom")
564 self.assertEqual(len(out_lines), 3)
565 self.assertEqual(len(err_lines), 1)
566 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
568 unstyle(str(report)),
570 "1 file reformatted, 2 files left unchanged, 1 file failed to"
574 self.assertEqual(report.return_code, 123)
575 report.done(Path("f3"), black.Changed.YES)
576 self.assertEqual(len(out_lines), 4)
577 self.assertEqual(len(err_lines), 1)
578 self.assertEqual(out_lines[-1], "reformatted f3")
580 unstyle(str(report)),
582 "2 files reformatted, 2 files left unchanged, 1 file failed to"
586 self.assertEqual(report.return_code, 123)
587 report.failed(Path("e2"), "boom")
588 self.assertEqual(len(out_lines), 4)
589 self.assertEqual(len(err_lines), 2)
590 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
592 unstyle(str(report)),
594 "2 files reformatted, 2 files left unchanged, 2 files failed to"
598 self.assertEqual(report.return_code, 123)
599 report.path_ignored(Path("wat"), "no match")
600 self.assertEqual(len(out_lines), 5)
601 self.assertEqual(len(err_lines), 2)
602 self.assertEqual(out_lines[-1], "wat ignored: no match")
604 unstyle(str(report)),
606 "2 files reformatted, 2 files left unchanged, 2 files failed to"
610 self.assertEqual(report.return_code, 123)
611 report.done(Path("f4"), black.Changed.NO)
612 self.assertEqual(len(out_lines), 6)
613 self.assertEqual(len(err_lines), 2)
614 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
616 unstyle(str(report)),
618 "2 files reformatted, 3 files left unchanged, 2 files failed to"
622 self.assertEqual(report.return_code, 123)
625 unstyle(str(report)),
627 "2 files would be reformatted, 3 files would be left unchanged, 2"
628 " files would fail to reformat."
634 unstyle(str(report)),
636 "2 files would be reformatted, 3 files would be left unchanged, 2"
637 " files would fail to reformat."
641 def test_report_quiet(self) -> None:
642 report = Report(quiet=True)
646 def out(msg: str, **kwargs: Any) -> None:
647 out_lines.append(msg)
649 def err(msg: str, **kwargs: Any) -> None:
650 err_lines.append(msg)
652 with patch("black.output._out", out), patch("black.output._err", err):
653 report.done(Path("f1"), black.Changed.NO)
654 self.assertEqual(len(out_lines), 0)
655 self.assertEqual(len(err_lines), 0)
656 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
657 self.assertEqual(report.return_code, 0)
658 report.done(Path("f2"), black.Changed.YES)
659 self.assertEqual(len(out_lines), 0)
660 self.assertEqual(len(err_lines), 0)
662 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
664 report.done(Path("f3"), black.Changed.CACHED)
665 self.assertEqual(len(out_lines), 0)
666 self.assertEqual(len(err_lines), 0)
668 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
670 self.assertEqual(report.return_code, 0)
672 self.assertEqual(report.return_code, 1)
674 report.failed(Path("e1"), "boom")
675 self.assertEqual(len(out_lines), 0)
676 self.assertEqual(len(err_lines), 1)
677 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
679 unstyle(str(report)),
681 "1 file reformatted, 2 files left unchanged, 1 file failed to"
685 self.assertEqual(report.return_code, 123)
686 report.done(Path("f3"), black.Changed.YES)
687 self.assertEqual(len(out_lines), 0)
688 self.assertEqual(len(err_lines), 1)
690 unstyle(str(report)),
692 "2 files reformatted, 2 files left unchanged, 1 file failed to"
696 self.assertEqual(report.return_code, 123)
697 report.failed(Path("e2"), "boom")
698 self.assertEqual(len(out_lines), 0)
699 self.assertEqual(len(err_lines), 2)
700 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
702 unstyle(str(report)),
704 "2 files reformatted, 2 files left unchanged, 2 files failed to"
708 self.assertEqual(report.return_code, 123)
709 report.path_ignored(Path("wat"), "no match")
710 self.assertEqual(len(out_lines), 0)
711 self.assertEqual(len(err_lines), 2)
713 unstyle(str(report)),
715 "2 files reformatted, 2 files left unchanged, 2 files failed to"
719 self.assertEqual(report.return_code, 123)
720 report.done(Path("f4"), black.Changed.NO)
721 self.assertEqual(len(out_lines), 0)
722 self.assertEqual(len(err_lines), 2)
724 unstyle(str(report)),
726 "2 files reformatted, 3 files left unchanged, 2 files failed to"
730 self.assertEqual(report.return_code, 123)
733 unstyle(str(report)),
735 "2 files would be reformatted, 3 files would be left unchanged, 2"
736 " files would fail to reformat."
742 unstyle(str(report)),
744 "2 files would be reformatted, 3 files would be left unchanged, 2"
745 " files would fail to reformat."
749 def test_report_normal(self) -> None:
750 report = black.Report()
754 def out(msg: str, **kwargs: Any) -> None:
755 out_lines.append(msg)
757 def err(msg: str, **kwargs: Any) -> None:
758 err_lines.append(msg)
760 with patch("black.output._out", out), patch("black.output._err", err):
761 report.done(Path("f1"), black.Changed.NO)
762 self.assertEqual(len(out_lines), 0)
763 self.assertEqual(len(err_lines), 0)
764 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
765 self.assertEqual(report.return_code, 0)
766 report.done(Path("f2"), black.Changed.YES)
767 self.assertEqual(len(out_lines), 1)
768 self.assertEqual(len(err_lines), 0)
769 self.assertEqual(out_lines[-1], "reformatted f2")
771 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
773 report.done(Path("f3"), black.Changed.CACHED)
774 self.assertEqual(len(out_lines), 1)
775 self.assertEqual(len(err_lines), 0)
776 self.assertEqual(out_lines[-1], "reformatted f2")
778 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
780 self.assertEqual(report.return_code, 0)
782 self.assertEqual(report.return_code, 1)
784 report.failed(Path("e1"), "boom")
785 self.assertEqual(len(out_lines), 1)
786 self.assertEqual(len(err_lines), 1)
787 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
789 unstyle(str(report)),
791 "1 file reformatted, 2 files left unchanged, 1 file failed to"
795 self.assertEqual(report.return_code, 123)
796 report.done(Path("f3"), black.Changed.YES)
797 self.assertEqual(len(out_lines), 2)
798 self.assertEqual(len(err_lines), 1)
799 self.assertEqual(out_lines[-1], "reformatted f3")
801 unstyle(str(report)),
803 "2 files reformatted, 2 files left unchanged, 1 file failed to"
807 self.assertEqual(report.return_code, 123)
808 report.failed(Path("e2"), "boom")
809 self.assertEqual(len(out_lines), 2)
810 self.assertEqual(len(err_lines), 2)
811 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
813 unstyle(str(report)),
815 "2 files reformatted, 2 files left unchanged, 2 files failed to"
819 self.assertEqual(report.return_code, 123)
820 report.path_ignored(Path("wat"), "no match")
821 self.assertEqual(len(out_lines), 2)
822 self.assertEqual(len(err_lines), 2)
824 unstyle(str(report)),
826 "2 files reformatted, 2 files left unchanged, 2 files failed to"
830 self.assertEqual(report.return_code, 123)
831 report.done(Path("f4"), black.Changed.NO)
832 self.assertEqual(len(out_lines), 2)
833 self.assertEqual(len(err_lines), 2)
835 unstyle(str(report)),
837 "2 files reformatted, 3 files left unchanged, 2 files failed to"
841 self.assertEqual(report.return_code, 123)
844 unstyle(str(report)),
846 "2 files would be reformatted, 3 files would be left unchanged, 2"
847 " files would fail to reformat."
853 unstyle(str(report)),
855 "2 files would be reformatted, 3 files would be left unchanged, 2"
856 " files would fail to reformat."
860 def test_lib2to3_parse(self) -> None:
861 with self.assertRaises(black.InvalidInput):
862 black.lib2to3_parse("invalid syntax")
865 black.lib2to3_parse(straddling)
866 black.lib2to3_parse(straddling, {TargetVersion.PY36})
869 with self.assertRaises(black.InvalidInput):
870 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
872 py3_only = "exec(x, end=y)"
873 black.lib2to3_parse(py3_only)
874 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
876 def test_get_features_used_decorator(self) -> None:
877 # Test the feature detection of new decorator syntax
878 # since this makes some test cases of test_get_features_used()
879 # fails if it fails, this is tested first so that a useful case
881 simples, relaxed = read_data("miscellaneous", "decorators")
882 # skip explanation comments at the top of the file
883 for simple_test in simples.split("##")[1:]:
884 node = black.lib2to3_parse(simple_test)
885 decorator = str(node.children[0].children[0]).strip()
887 Feature.RELAXED_DECORATORS,
888 black.get_features_used(node),
890 f"decorator '{decorator}' follows python<=3.8 syntax"
891 "but is detected as 3.9+"
892 # f"The full node is\n{node!r}"
895 # skip the '# output' comment at the top of the output part
896 for relaxed_test in relaxed.split("##")[1:]:
897 node = black.lib2to3_parse(relaxed_test)
898 decorator = str(node.children[0].children[0]).strip()
900 Feature.RELAXED_DECORATORS,
901 black.get_features_used(node),
903 f"decorator '{decorator}' uses python3.9+ syntax"
904 "but is detected as python<=3.8"
905 # f"The full node is\n{node!r}"
909 def test_get_features_used(self) -> None:
910 node = black.lib2to3_parse("def f(*, arg): ...\n")
911 self.assertEqual(black.get_features_used(node), set())
912 node = black.lib2to3_parse("def f(*, arg,): ...\n")
913 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
914 node = black.lib2to3_parse("f(*arg,)\n")
916 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
918 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
919 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
920 node = black.lib2to3_parse("123_456\n")
921 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
922 node = black.lib2to3_parse("123456\n")
923 self.assertEqual(black.get_features_used(node), set())
924 source, expected = read_data("simple_cases", "function")
925 node = black.lib2to3_parse(source)
926 expected_features = {
927 Feature.TRAILING_COMMA_IN_CALL,
928 Feature.TRAILING_COMMA_IN_DEF,
931 self.assertEqual(black.get_features_used(node), expected_features)
932 node = black.lib2to3_parse(expected)
933 self.assertEqual(black.get_features_used(node), expected_features)
934 source, expected = read_data("simple_cases", "expression")
935 node = black.lib2to3_parse(source)
936 self.assertEqual(black.get_features_used(node), set())
937 node = black.lib2to3_parse(expected)
938 self.assertEqual(black.get_features_used(node), set())
939 node = black.lib2to3_parse("lambda a, /, b: ...")
940 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
941 node = black.lib2to3_parse("def fn(a, /, b): ...")
942 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
943 node = black.lib2to3_parse("def fn(): yield a, b")
944 self.assertEqual(black.get_features_used(node), set())
945 node = black.lib2to3_parse("def fn(): return a, b")
946 self.assertEqual(black.get_features_used(node), set())
947 node = black.lib2to3_parse("def fn(): yield *b, c")
948 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
949 node = black.lib2to3_parse("def fn(): return a, *b, c")
950 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
951 node = black.lib2to3_parse("x = a, *b, c")
952 self.assertEqual(black.get_features_used(node), set())
953 node = black.lib2to3_parse("x: Any = regular")
954 self.assertEqual(black.get_features_used(node), set())
955 node = black.lib2to3_parse("x: Any = (regular, regular)")
956 self.assertEqual(black.get_features_used(node), set())
957 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
958 self.assertEqual(black.get_features_used(node), set())
959 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
961 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
963 node = black.lib2to3_parse("try: pass\nexcept Something: pass")
964 self.assertEqual(black.get_features_used(node), set())
965 node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass")
966 self.assertEqual(black.get_features_used(node), set())
967 node = black.lib2to3_parse("try: pass\nexcept *Group: pass")
968 self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR})
969 node = black.lib2to3_parse("a[*b]")
970 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
971 node = black.lib2to3_parse("a[x, *y(), z] = t")
972 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
973 node = black.lib2to3_parse("def fn(*args: *T): pass")
974 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
976 def test_get_features_used_for_future_flags(self) -> None:
977 for src, features in [
978 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
980 "from __future__ import (other, annotations)",
981 {Feature.FUTURE_ANNOTATIONS},
983 ("a = 1 + 2\nfrom something import annotations", set()),
984 ("from __future__ import x, y", set()),
986 with self.subTest(src=src, features=features):
987 node = black.lib2to3_parse(src)
988 future_imports = black.get_future_imports(node)
990 black.get_features_used(node, future_imports=future_imports),
994 def test_get_future_imports(self) -> None:
995 node = black.lib2to3_parse("\n")
996 self.assertEqual(set(), black.get_future_imports(node))
997 node = black.lib2to3_parse("from __future__ import black\n")
998 self.assertEqual({"black"}, black.get_future_imports(node))
999 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
1000 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
1001 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
1002 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
1003 node = black.lib2to3_parse(
1004 "from __future__ import multiple\nfrom __future__ import imports\n"
1006 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
1007 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
1008 self.assertEqual({"black"}, black.get_future_imports(node))
1009 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
1010 self.assertEqual({"black"}, black.get_future_imports(node))
1011 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
1012 self.assertEqual(set(), black.get_future_imports(node))
1013 node = black.lib2to3_parse("from some.module import black\n")
1014 self.assertEqual(set(), black.get_future_imports(node))
1015 node = black.lib2to3_parse(
1016 "from __future__ import unicode_literals as _unicode_literals"
1018 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
1019 node = black.lib2to3_parse(
1020 "from __future__ import unicode_literals as _lol, print"
1022 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
1024 @pytest.mark.incompatible_with_mypyc
1025 def test_debug_visitor(self) -> None:
1026 source, _ = read_data("miscellaneous", "debug_visitor")
1027 expected, _ = read_data("miscellaneous", "debug_visitor.out")
1031 def out(msg: str, **kwargs: Any) -> None:
1032 out_lines.append(msg)
1034 def err(msg: str, **kwargs: Any) -> None:
1035 err_lines.append(msg)
1037 with patch("black.debug.out", out):
1038 DebugVisitor.show(source)
1039 actual = "\n".join(out_lines) + "\n"
1041 if expected != actual:
1042 log_name = black.dump_to_file(*out_lines)
1046 f"AST print out is different. Actual version dumped to {log_name}",
1049 def test_format_file_contents(self) -> None:
1052 with self.assertRaises(black.NothingChanged):
1053 black.format_file_contents(empty, mode=mode, fast=False)
1055 with self.assertRaises(black.NothingChanged):
1056 black.format_file_contents(just_nl, mode=mode, fast=False)
1057 same = "j = [1, 2, 3]\n"
1058 with self.assertRaises(black.NothingChanged):
1059 black.format_file_contents(same, mode=mode, fast=False)
1060 different = "j = [1,2,3]"
1062 actual = black.format_file_contents(different, mode=mode, fast=False)
1063 self.assertEqual(expected, actual)
1064 invalid = "return if you can"
1065 with self.assertRaises(black.InvalidInput) as e:
1066 black.format_file_contents(invalid, mode=mode, fast=False)
1067 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
1069 mode = black.Mode(preview=True)
1071 with self.assertRaises(black.NothingChanged):
1072 black.format_file_contents(just_crlf, mode=mode, fast=False)
1073 just_whitespace_nl = "\n\t\n \n\t \n \t\n\n"
1074 actual = black.format_file_contents(just_whitespace_nl, mode=mode, fast=False)
1075 self.assertEqual("\n", actual)
1076 just_whitespace_crlf = "\r\n\t\r\n \r\n\t \r\n \t\r\n\r\n"
1077 actual = black.format_file_contents(just_whitespace_crlf, mode=mode, fast=False)
1078 self.assertEqual("\r\n", actual)
1080 def test_endmarker(self) -> None:
1081 n = black.lib2to3_parse("\n")
1082 self.assertEqual(n.type, black.syms.file_input)
1083 self.assertEqual(len(n.children), 1)
1084 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
1086 @pytest.mark.incompatible_with_mypyc
1087 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
1088 def test_assertFormatEqual(self) -> None:
1092 def out(msg: str, **kwargs: Any) -> None:
1093 out_lines.append(msg)
1095 def err(msg: str, **kwargs: Any) -> None:
1096 err_lines.append(msg)
1098 with patch("black.output._out", out), patch("black.output._err", err):
1099 with self.assertRaises(AssertionError):
1100 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
1102 out_str = "".join(out_lines)
1103 self.assertIn("Expected tree:", out_str)
1104 self.assertIn("Actual tree:", out_str)
1105 self.assertEqual("".join(err_lines), "")
1108 @patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1109 def test_works_in_mono_process_only_environment(self) -> None:
1110 with cache_dir() as workspace:
1112 (workspace / "one.py").resolve(),
1113 (workspace / "two.py").resolve(),
1115 f.write_text('print("hello")\n')
1116 self.invokeBlack([str(workspace)])
1119 def test_check_diff_use_together(self) -> None:
1121 # Files which will be reformatted.
1122 src1 = get_case_path("miscellaneous", "string_quotes")
1123 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1124 # Files which will not be reformatted.
1125 src2 = get_case_path("simple_cases", "composition")
1126 self.invokeBlack([str(src2), "--diff", "--check"])
1127 # Multi file command.
1128 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1130 def test_no_src_fails(self) -> None:
1132 self.invokeBlack([], exit_code=1)
1134 def test_src_and_code_fails(self) -> None:
1136 self.invokeBlack([".", "-c", "0"], exit_code=1)
1138 def test_broken_symlink(self) -> None:
1139 with cache_dir() as workspace:
1140 symlink = workspace / "broken_link.py"
1142 symlink.symlink_to("nonexistent.py")
1143 except (OSError, NotImplementedError) as e:
1144 self.skipTest(f"Can't create symlinks: {e}")
1145 self.invokeBlack([str(workspace.resolve())])
1147 def test_single_file_force_pyi(self) -> None:
1148 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1149 contents, expected = read_data("miscellaneous", "force_pyi")
1150 with cache_dir() as workspace:
1151 path = (workspace / "file.py").resolve()
1152 with open(path, "w") as fh:
1154 self.invokeBlack([str(path), "--pyi"])
1155 with open(path, "r") as fh:
1157 # verify cache with --pyi is separate
1158 pyi_cache = black.read_cache(pyi_mode)
1159 self.assertIn(str(path), pyi_cache)
1160 normal_cache = black.read_cache(DEFAULT_MODE)
1161 self.assertNotIn(str(path), normal_cache)
1162 self.assertFormatEqual(expected, actual)
1163 black.assert_equivalent(contents, actual)
1164 black.assert_stable(contents, actual, pyi_mode)
1167 def test_multi_file_force_pyi(self) -> None:
1168 reg_mode = DEFAULT_MODE
1169 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1170 contents, expected = read_data("miscellaneous", "force_pyi")
1171 with cache_dir() as workspace:
1173 (workspace / "file1.py").resolve(),
1174 (workspace / "file2.py").resolve(),
1177 with open(path, "w") as fh:
1179 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1181 with open(path, "r") as fh:
1183 self.assertEqual(actual, expected)
1184 # verify cache with --pyi is separate
1185 pyi_cache = black.read_cache(pyi_mode)
1186 normal_cache = black.read_cache(reg_mode)
1188 self.assertIn(str(path), pyi_cache)
1189 self.assertNotIn(str(path), normal_cache)
1191 def test_pipe_force_pyi(self) -> None:
1192 source, expected = read_data("miscellaneous", "force_pyi")
1193 result = CliRunner().invoke(
1194 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1196 self.assertEqual(result.exit_code, 0)
1197 actual = result.output
1198 self.assertFormatEqual(actual, expected)
1200 def test_single_file_force_py36(self) -> None:
1201 reg_mode = DEFAULT_MODE
1202 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1203 source, expected = read_data("miscellaneous", "force_py36")
1204 with cache_dir() as workspace:
1205 path = (workspace / "file.py").resolve()
1206 with open(path, "w") as fh:
1208 self.invokeBlack([str(path), *PY36_ARGS])
1209 with open(path, "r") as fh:
1211 # verify cache with --target-version is separate
1212 py36_cache = black.read_cache(py36_mode)
1213 self.assertIn(str(path), py36_cache)
1214 normal_cache = black.read_cache(reg_mode)
1215 self.assertNotIn(str(path), normal_cache)
1216 self.assertEqual(actual, expected)
1219 def test_multi_file_force_py36(self) -> None:
1220 reg_mode = DEFAULT_MODE
1221 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1222 source, expected = read_data("miscellaneous", "force_py36")
1223 with cache_dir() as workspace:
1225 (workspace / "file1.py").resolve(),
1226 (workspace / "file2.py").resolve(),
1229 with open(path, "w") as fh:
1231 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1233 with open(path, "r") as fh:
1235 self.assertEqual(actual, expected)
1236 # verify cache with --target-version is separate
1237 pyi_cache = black.read_cache(py36_mode)
1238 normal_cache = black.read_cache(reg_mode)
1240 self.assertIn(str(path), pyi_cache)
1241 self.assertNotIn(str(path), normal_cache)
1243 def test_pipe_force_py36(self) -> None:
1244 source, expected = read_data("miscellaneous", "force_py36")
1245 result = CliRunner().invoke(
1247 ["-", "-q", "--target-version=py36"],
1248 input=BytesIO(source.encode("utf8")),
1250 self.assertEqual(result.exit_code, 0)
1251 actual = result.output
1252 self.assertFormatEqual(actual, expected)
1254 @pytest.mark.incompatible_with_mypyc
1255 def test_reformat_one_with_stdin(self) -> None:
1257 "black.format_stdin_to_stdout",
1258 return_value=lambda *args, **kwargs: black.Changed.YES,
1260 report = MagicMock()
1265 write_back=black.WriteBack.YES,
1269 fsts.assert_called_once()
1270 report.done.assert_called_with(path, black.Changed.YES)
1272 @pytest.mark.incompatible_with_mypyc
1273 def test_reformat_one_with_stdin_filename(self) -> None:
1275 "black.format_stdin_to_stdout",
1276 return_value=lambda *args, **kwargs: black.Changed.YES,
1278 report = MagicMock()
1280 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1285 write_back=black.WriteBack.YES,
1289 fsts.assert_called_once_with(
1290 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1292 # __BLACK_STDIN_FILENAME__ should have been stripped
1293 report.done.assert_called_with(expected, black.Changed.YES)
1295 @pytest.mark.incompatible_with_mypyc
1296 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1298 "black.format_stdin_to_stdout",
1299 return_value=lambda *args, **kwargs: black.Changed.YES,
1301 report = MagicMock()
1303 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1308 write_back=black.WriteBack.YES,
1312 fsts.assert_called_once_with(
1314 write_back=black.WriteBack.YES,
1315 mode=replace(DEFAULT_MODE, is_pyi=True),
1317 # __BLACK_STDIN_FILENAME__ should have been stripped
1318 report.done.assert_called_with(expected, black.Changed.YES)
1320 @pytest.mark.incompatible_with_mypyc
1321 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1323 "black.format_stdin_to_stdout",
1324 return_value=lambda *args, **kwargs: black.Changed.YES,
1326 report = MagicMock()
1328 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1333 write_back=black.WriteBack.YES,
1337 fsts.assert_called_once_with(
1339 write_back=black.WriteBack.YES,
1340 mode=replace(DEFAULT_MODE, is_ipynb=True),
1342 # __BLACK_STDIN_FILENAME__ should have been stripped
1343 report.done.assert_called_with(expected, black.Changed.YES)
1345 @pytest.mark.incompatible_with_mypyc
1346 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1348 "black.format_stdin_to_stdout",
1349 return_value=lambda *args, **kwargs: black.Changed.YES,
1351 report = MagicMock()
1352 # Even with an existing file, since we are forcing stdin, black
1353 # should output to stdout and not modify the file inplace
1354 p = THIS_DIR / "data" / "simple_cases" / "collections.py"
1355 # Make sure is_file actually returns True
1356 self.assertTrue(p.is_file())
1357 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1362 write_back=black.WriteBack.YES,
1366 fsts.assert_called_once()
1367 # __BLACK_STDIN_FILENAME__ should have been stripped
1368 report.done.assert_called_with(expected, black.Changed.YES)
1370 def test_reformat_one_with_stdin_empty(self) -> None:
1377 (" \t\r\n\t ", "\r\n"),
1381 output: io.StringIO, io_TextIOWrapper: Type[io.TextIOWrapper]
1382 ) -> Callable[[Any, Any], io.TextIOWrapper]:
1383 def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper:
1384 if args == (sys.stdout.buffer,):
1385 # It's `format_stdin_to_stdout()` calling `io.TextIOWrapper()`,
1386 # return our mock object.
1388 # It's something else (i.e. `decode_bytes()`) calling
1389 # `io.TextIOWrapper()`, pass through to the original implementation.
1390 # See discussion in https://github.com/psf/black/pull/2489
1391 return io_TextIOWrapper(*args, **kwargs)
1395 mode = black.Mode(preview=True)
1396 for content, expected in cases:
1397 output = io.StringIO()
1398 io_TextIOWrapper = io.TextIOWrapper
1400 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1402 black.format_stdin_to_stdout(
1405 write_back=black.WriteBack.YES,
1408 except io.UnsupportedOperation:
1409 pass # StringIO does not support detach
1410 assert output.getvalue() == expected
1412 # An empty string is the only test case for `preview=False`
1413 output = io.StringIO()
1414 io_TextIOWrapper = io.TextIOWrapper
1415 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1417 black.format_stdin_to_stdout(
1420 write_back=black.WriteBack.YES,
1423 except io.UnsupportedOperation:
1424 pass # StringIO does not support detach
1425 assert output.getvalue() == ""
1427 def test_invalid_cli_regex(self) -> None:
1428 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1429 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1431 def test_required_version_matches_version(self) -> None:
1433 ["--required-version", black.__version__, "-c", "0"],
1438 def test_required_version_matches_partial_version(self) -> None:
1440 ["--required-version", black.__version__.split(".")[0], "-c", "0"],
1445 def test_required_version_does_not_match_on_minor_version(self) -> None:
1447 ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
1452 def test_required_version_does_not_match_version(self) -> None:
1453 result = BlackRunner().invoke(
1455 ["--required-version", "20.99b", "-c", "0"],
1457 self.assertEqual(result.exit_code, 1)
1458 self.assertIn("required version", result.stderr)
1460 def test_preserves_line_endings(self) -> None:
1461 with TemporaryDirectory() as workspace:
1462 test_file = Path(workspace) / "test.py"
1463 for nl in ["\n", "\r\n"]:
1464 contents = nl.join(["def f( ):", " pass"])
1465 test_file.write_bytes(contents.encode())
1466 ff(test_file, write_back=black.WriteBack.YES)
1467 updated_contents: bytes = test_file.read_bytes()
1468 self.assertIn(nl.encode(), updated_contents)
1470 self.assertNotIn(b"\r\n", updated_contents)
1472 def test_preserves_line_endings_via_stdin(self) -> None:
1473 for nl in ["\n", "\r\n"]:
1474 contents = nl.join(["def f( ):", " pass"])
1475 runner = BlackRunner()
1476 result = runner.invoke(
1477 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1479 self.assertEqual(result.exit_code, 0)
1480 output = result.stdout_bytes
1481 self.assertIn(nl.encode("utf8"), output)
1483 self.assertNotIn(b"\r\n", output)
1485 def test_normalize_line_endings(self) -> None:
1486 with TemporaryDirectory() as workspace:
1487 test_file = Path(workspace) / "test.py"
1488 for data, expected in (
1489 (b"c\r\nc\n ", b"c\r\nc\r\n"),
1490 (b"l\nl\r\n ", b"l\nl\n"),
1492 test_file.write_bytes(data)
1493 ff(test_file, write_back=black.WriteBack.YES)
1494 self.assertEqual(test_file.read_bytes(), expected)
1496 def test_assert_equivalent_different_asts(self) -> None:
1497 with self.assertRaises(AssertionError):
1498 black.assert_equivalent("{}", "None")
1500 def test_shhh_click(self) -> None:
1502 from click import _unicodefun # type: ignore
1504 self.skipTest("Incompatible Click version")
1506 if not hasattr(_unicodefun, "_verify_python_env"):
1507 self.skipTest("Incompatible Click version")
1509 # First, let's see if Click is crashing with a preferred ASCII charset.
1510 with patch("locale.getpreferredencoding") as gpe:
1511 gpe.return_value = "ASCII"
1512 with self.assertRaises(RuntimeError):
1513 _unicodefun._verify_python_env()
1514 # Now, let's silence Click...
1516 # ...and confirm it's silent.
1517 with patch("locale.getpreferredencoding") as gpe:
1518 gpe.return_value = "ASCII"
1520 _unicodefun._verify_python_env()
1521 except RuntimeError as re:
1522 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1524 def test_root_logger_not_used_directly(self) -> None:
1525 def fail(*args: Any, **kwargs: Any) -> None:
1526 self.fail("Record created with root logger")
1528 with patch.multiple(
1537 ff(THIS_DIR / "util.py")
1539 def test_invalid_config_return_code(self) -> None:
1540 tmp_file = Path(black.dump_to_file())
1542 tmp_config = Path(black.dump_to_file())
1544 args = ["--config", str(tmp_config), str(tmp_file)]
1545 self.invokeBlack(args, exit_code=2, ignore_config=False)
1549 def test_parse_pyproject_toml(self) -> None:
1550 test_toml_file = THIS_DIR / "test.toml"
1551 config = black.parse_pyproject_toml(str(test_toml_file))
1552 self.assertEqual(config["verbose"], 1)
1553 self.assertEqual(config["check"], "no")
1554 self.assertEqual(config["diff"], "y")
1555 self.assertEqual(config["color"], True)
1556 self.assertEqual(config["line_length"], 79)
1557 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1558 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1559 self.assertEqual(config["exclude"], r"\.pyi?$")
1560 self.assertEqual(config["include"], r"\.py?$")
1562 def test_read_pyproject_toml(self) -> None:
1563 test_toml_file = THIS_DIR / "test.toml"
1564 fake_ctx = FakeContext()
1565 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1566 config = fake_ctx.default_map
1567 self.assertEqual(config["verbose"], "1")
1568 self.assertEqual(config["check"], "no")
1569 self.assertEqual(config["diff"], "y")
1570 self.assertEqual(config["color"], "True")
1571 self.assertEqual(config["line_length"], "79")
1572 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1573 self.assertEqual(config["exclude"], r"\.pyi?$")
1574 self.assertEqual(config["include"], r"\.py?$")
1576 @pytest.mark.incompatible_with_mypyc
1577 def test_find_project_root(self) -> None:
1578 with TemporaryDirectory() as workspace:
1579 root = Path(workspace)
1580 test_dir = root / "test"
1583 src_dir = root / "src"
1586 root_pyproject = root / "pyproject.toml"
1587 root_pyproject.touch()
1588 src_pyproject = src_dir / "pyproject.toml"
1589 src_pyproject.touch()
1590 src_python = src_dir / "foo.py"
1594 black.find_project_root((src_dir, test_dir)),
1595 (root.resolve(), "pyproject.toml"),
1598 black.find_project_root((src_dir,)),
1599 (src_dir.resolve(), "pyproject.toml"),
1602 black.find_project_root((src_python,)),
1603 (src_dir.resolve(), "pyproject.toml"),
1606 with change_directory(test_dir):
1608 black.find_project_root(("-",), stdin_filename="../src/a.py"),
1609 (src_dir.resolve(), "pyproject.toml"),
1613 "black.files.find_user_pyproject_toml",
1615 def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
1616 find_user_pyproject_toml.side_effect = RuntimeError()
1618 with redirect_stderr(io.StringIO()) as stderr:
1619 result = black.files.find_pyproject_toml(
1620 path_search_start=(str(Path.cwd().root),)
1623 assert result is None
1624 err = stderr.getvalue()
1625 assert "Ignoring user configuration" in err
1628 "black.files.find_user_pyproject_toml",
1629 black.files.find_user_pyproject_toml.__wrapped__,
1631 def test_find_user_pyproject_toml_linux(self) -> None:
1632 if system() == "Windows":
1635 # Test if XDG_CONFIG_HOME is checked
1636 with TemporaryDirectory() as workspace:
1637 tmp_user_config = Path(workspace) / "black"
1638 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1640 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1643 # Test fallback for XDG_CONFIG_HOME
1644 with patch.dict("os.environ"):
1645 os.environ.pop("XDG_CONFIG_HOME", None)
1646 fallback_user_config = Path("~/.config").expanduser() / "black"
1648 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1651 def test_find_user_pyproject_toml_windows(self) -> None:
1652 if system() != "Windows":
1655 user_config_path = Path.home() / ".black"
1657 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1660 def test_bpo_33660_workaround(self) -> None:
1661 if system() == "Windows":
1664 # https://bugs.python.org/issue33660
1666 with change_directory(root):
1667 path = Path("workspace") / "project"
1668 report = black.Report(verbose=True)
1669 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1670 self.assertEqual(normalized_path, "workspace/project")
1672 def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
1673 if system() != "Windows":
1676 with TemporaryDirectory() as workspace:
1677 root = Path(workspace)
1678 junction_dir = root / "junction"
1679 junction_target_outside_of_root = root / ".."
1680 os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
1682 report = black.Report(verbose=True)
1683 normalized_path = black.normalize_path_maybe_ignore(
1684 junction_dir, root, report
1686 # Manually delete for Python < 3.8
1687 os.system(f"rmdir {junction_dir}")
1689 self.assertEqual(normalized_path, None)
1691 def test_newline_comment_interaction(self) -> None:
1692 source = "class A:\\\r\n# type: ignore\n pass\n"
1693 output = black.format_str(source, mode=DEFAULT_MODE)
1694 black.assert_stable(source, output, mode=DEFAULT_MODE)
1696 def test_bpo_2142_workaround(self) -> None:
1697 # https://bugs.python.org/issue2142
1699 source, _ = read_data("miscellaneous", "missing_final_newline")
1700 # read_data adds a trailing newline
1701 source = source.rstrip()
1702 expected, _ = read_data("miscellaneous", "missing_final_newline.diff")
1703 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1704 diff_header = re.compile(
1705 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1706 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1709 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1710 self.assertEqual(result.exit_code, 0)
1713 actual = result.output
1714 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1715 self.assertEqual(actual, expected)
1718 def compare_results(
1719 result: click.testing.Result, expected_value: str, expected_exit_code: int
1721 """Helper method to test the value and exit code of a click Result."""
1723 result.output == expected_value
1724 ), "The output did not match the expected value."
1725 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1727 def test_code_option(self) -> None:
1728 """Test the code option with no changes."""
1729 code = 'print("Hello world")\n'
1730 args = ["--code", code]
1731 result = CliRunner().invoke(black.main, args)
1733 self.compare_results(result, code, 0)
1735 def test_code_option_changed(self) -> None:
1736 """Test the code option when changes are required."""
1737 code = "print('hello world')"
1738 formatted = black.format_str(code, mode=DEFAULT_MODE)
1740 args = ["--code", code]
1741 result = CliRunner().invoke(black.main, args)
1743 self.compare_results(result, formatted, 0)
1745 def test_code_option_check(self) -> None:
1746 """Test the code option when check is passed."""
1747 args = ["--check", "--code", 'print("Hello world")\n']
1748 result = CliRunner().invoke(black.main, args)
1749 self.compare_results(result, "", 0)
1751 def test_code_option_check_changed(self) -> None:
1752 """Test the code option when changes are required, and check is passed."""
1753 args = ["--check", "--code", "print('hello world')"]
1754 result = CliRunner().invoke(black.main, args)
1755 self.compare_results(result, "", 1)
1757 def test_code_option_diff(self) -> None:
1758 """Test the code option when diff is passed."""
1759 code = "print('hello world')"
1760 formatted = black.format_str(code, mode=DEFAULT_MODE)
1761 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1763 args = ["--diff", "--code", code]
1764 result = CliRunner().invoke(black.main, args)
1766 # Remove time from diff
1767 output = DIFF_TIME.sub("", result.output)
1769 assert output == result_diff, "The output did not match the expected value."
1770 assert result.exit_code == 0, "The exit code is incorrect."
1772 def test_code_option_color_diff(self) -> None:
1773 """Test the code option when color and diff are passed."""
1774 code = "print('hello world')"
1775 formatted = black.format_str(code, mode=DEFAULT_MODE)
1777 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1778 result_diff = color_diff(result_diff)
1780 args = ["--diff", "--color", "--code", code]
1781 result = CliRunner().invoke(black.main, args)
1783 # Remove time from diff
1784 output = DIFF_TIME.sub("", result.output)
1786 assert output == result_diff, "The output did not match the expected value."
1787 assert result.exit_code == 0, "The exit code is incorrect."
1789 @pytest.mark.incompatible_with_mypyc
1790 def test_code_option_safe(self) -> None:
1791 """Test that the code option throws an error when the sanity checks fail."""
1792 # Patch black.assert_equivalent to ensure the sanity checks fail
1793 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1794 code = 'print("Hello world")'
1795 error_msg = f"{code}\nerror: cannot format <string>: \n"
1797 args = ["--safe", "--code", code]
1798 result = CliRunner().invoke(black.main, args)
1800 self.compare_results(result, error_msg, 123)
1802 def test_code_option_fast(self) -> None:
1803 """Test that the code option ignores errors when the sanity checks fail."""
1804 # Patch black.assert_equivalent to ensure the sanity checks fail
1805 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1806 code = 'print("Hello world")'
1807 formatted = black.format_str(code, mode=DEFAULT_MODE)
1809 args = ["--fast", "--code", code]
1810 result = CliRunner().invoke(black.main, args)
1812 self.compare_results(result, formatted, 0)
1814 @pytest.mark.incompatible_with_mypyc
1815 def test_code_option_config(self) -> None:
1817 Test that the code option finds the pyproject.toml in the current directory.
1819 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1820 args = ["--code", "print"]
1821 # This is the only directory known to contain a pyproject.toml
1822 with change_directory(PROJECT_ROOT):
1823 CliRunner().invoke(black.main, args)
1824 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1827 len(parse.mock_calls) >= 1
1828 ), "Expected config parse to be called with the current directory."
1830 _, call_args, _ = parse.mock_calls[0]
1832 call_args[0].lower() == str(pyproject_path).lower()
1833 ), "Incorrect config loaded."
1835 @pytest.mark.incompatible_with_mypyc
1836 def test_code_option_parent_config(self) -> None:
1838 Test that the code option finds the pyproject.toml in the parent directory.
1840 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1841 with change_directory(THIS_DIR):
1842 args = ["--code", "print"]
1843 CliRunner().invoke(black.main, args)
1845 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1847 len(parse.mock_calls) >= 1
1848 ), "Expected config parse to be called with the current directory."
1850 _, call_args, _ = parse.mock_calls[0]
1852 call_args[0].lower() == str(pyproject_path).lower()
1853 ), "Incorrect config loaded."
1855 def test_for_handled_unexpected_eof_error(self) -> None:
1857 Test that an unexpected EOF SyntaxError is nicely presented.
1859 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1860 black.lib2to3_parse("print(", {})
1862 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1864 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1865 with pytest.raises(AssertionError) as err:
1866 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1869 # Unfortunately the SyntaxError message has changed in newer versions so we
1870 # can't match it directly.
1871 err.match("invalid character")
1872 err.match(r"\(<unknown>, line 1\)")
1876 def test_get_cache_dir(
1879 monkeypatch: pytest.MonkeyPatch,
1881 # Create multiple cache directories
1882 workspace1 = tmp_path / "ws1"
1884 workspace2 = tmp_path / "ws2"
1887 # Force user_cache_dir to use the temporary directory for easier assertions
1888 patch_user_cache_dir = patch(
1889 target="black.cache.user_cache_dir",
1891 return_value=str(workspace1),
1894 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1895 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1896 with patch_user_cache_dir:
1897 assert get_cache_dir() == workspace1
1899 # If it is set, use the path provided in the env var.
1900 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1901 assert get_cache_dir() == workspace2
1903 def test_cache_broken_file(self) -> None:
1905 with cache_dir() as workspace:
1906 cache_file = get_cache_file(mode)
1907 cache_file.write_text("this is not a pickle")
1908 assert black.read_cache(mode) == {}
1909 src = (workspace / "test.py").resolve()
1910 src.write_text("print('hello')")
1911 invokeBlack([str(src)])
1912 cache = black.read_cache(mode)
1913 assert str(src) in cache
1915 def test_cache_single_file_already_cached(self) -> None:
1917 with cache_dir() as workspace:
1918 src = (workspace / "test.py").resolve()
1919 src.write_text("print('hello')")
1920 black.write_cache({}, [src], mode)
1921 invokeBlack([str(src)])
1922 assert src.read_text() == "print('hello')"
1925 def test_cache_multiple_files(self) -> None:
1927 with cache_dir() as workspace, patch(
1928 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
1930 one = (workspace / "one.py").resolve()
1931 with one.open("w") as fobj:
1932 fobj.write("print('hello')")
1933 two = (workspace / "two.py").resolve()
1934 with two.open("w") as fobj:
1935 fobj.write("print('hello')")
1936 black.write_cache({}, [one], mode)
1937 invokeBlack([str(workspace)])
1938 with one.open("r") as fobj:
1939 assert fobj.read() == "print('hello')"
1940 with two.open("r") as fobj:
1941 assert fobj.read() == 'print("hello")\n'
1942 cache = black.read_cache(mode)
1943 assert str(one) in cache
1944 assert str(two) in cache
1946 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1947 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1949 with cache_dir() as workspace:
1950 src = (workspace / "test.py").resolve()
1951 with src.open("w") as fobj:
1952 fobj.write("print('hello')")
1953 with patch("black.read_cache") as read_cache, patch(
1956 cmd = [str(src), "--diff"]
1958 cmd.append("--color")
1960 cache_file = get_cache_file(mode)
1961 assert cache_file.exists() is False
1962 write_cache.assert_not_called()
1963 read_cache.assert_not_called()
1965 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1967 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1968 with cache_dir() as workspace:
1969 for tag in range(0, 4):
1970 src = (workspace / f"test{tag}.py").resolve()
1971 with src.open("w") as fobj:
1972 fobj.write("print('hello')")
1974 "black.concurrency.Manager", wraps=multiprocessing.Manager
1976 cmd = ["--diff", str(workspace)]
1978 cmd.append("--color")
1979 invokeBlack(cmd, exit_code=0)
1980 # this isn't quite doing what we want, but if it _isn't_
1981 # called then we cannot be using the lock it provides
1984 def test_no_cache_when_stdin(self) -> None:
1987 result = CliRunner().invoke(
1988 black.main, ["-"], input=BytesIO(b"print('hello')")
1990 assert not result.exit_code
1991 cache_file = get_cache_file(mode)
1992 assert not cache_file.exists()
1994 def test_read_cache_no_cachefile(self) -> None:
1997 assert black.read_cache(mode) == {}
1999 def test_write_cache_read_cache(self) -> None:
2001 with cache_dir() as workspace:
2002 src = (workspace / "test.py").resolve()
2004 black.write_cache({}, [src], mode)
2005 cache = black.read_cache(mode)
2006 assert str(src) in cache
2007 assert cache[str(src)] == black.get_cache_info(src)
2009 def test_filter_cached(self) -> None:
2010 with TemporaryDirectory() as workspace:
2011 path = Path(workspace)
2012 uncached = (path / "uncached").resolve()
2013 cached = (path / "cached").resolve()
2014 cached_but_changed = (path / "changed").resolve()
2017 cached_but_changed.touch()
2019 str(cached): black.get_cache_info(cached),
2020 str(cached_but_changed): (0.0, 0),
2022 todo, done = black.cache.filter_cached(
2023 cache, {uncached, cached, cached_but_changed}
2025 assert todo == {uncached, cached_but_changed}
2026 assert done == {cached}
2028 def test_write_cache_creates_directory_if_needed(self) -> None:
2030 with cache_dir(exists=False) as workspace:
2031 assert not workspace.exists()
2032 black.write_cache({}, [], mode)
2033 assert workspace.exists()
2036 def test_failed_formatting_does_not_get_cached(self) -> None:
2038 with cache_dir() as workspace, patch(
2039 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
2041 failing = (workspace / "failing.py").resolve()
2042 with failing.open("w") as fobj:
2043 fobj.write("not actually python")
2044 clean = (workspace / "clean.py").resolve()
2045 with clean.open("w") as fobj:
2046 fobj.write('print("hello")\n')
2047 invokeBlack([str(workspace)], exit_code=123)
2048 cache = black.read_cache(mode)
2049 assert str(failing) not in cache
2050 assert str(clean) in cache
2052 def test_write_cache_write_fail(self) -> None:
2054 with cache_dir(), patch.object(Path, "open") as mock:
2055 mock.side_effect = OSError
2056 black.write_cache({}, [], mode)
2058 def test_read_cache_line_lengths(self) -> None:
2060 short_mode = replace(DEFAULT_MODE, line_length=1)
2061 with cache_dir() as workspace:
2062 path = (workspace / "file.py").resolve()
2064 black.write_cache({}, [path], mode)
2065 one = black.read_cache(mode)
2066 assert str(path) in one
2067 two = black.read_cache(short_mode)
2068 assert str(path) not in two
2071 def assert_collected_sources(
2072 src: Sequence[Union[str, Path]],
2073 expected: Sequence[Union[str, Path]],
2075 ctx: Optional[FakeContext] = None,
2076 exclude: Optional[str] = None,
2077 include: Optional[str] = None,
2078 extend_exclude: Optional[str] = None,
2079 force_exclude: Optional[str] = None,
2080 stdin_filename: Optional[str] = None,
2082 gs_src = tuple(str(Path(s)) for s in src)
2083 gs_expected = [Path(s) for s in expected]
2084 gs_exclude = None if exclude is None else compile_pattern(exclude)
2085 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
2086 gs_extend_exclude = (
2087 None if extend_exclude is None else compile_pattern(extend_exclude)
2089 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
2090 collected = black.get_sources(
2091 ctx=ctx or FakeContext(),
2097 extend_exclude=gs_extend_exclude,
2098 force_exclude=gs_force_exclude,
2099 report=black.Report(),
2100 stdin_filename=stdin_filename,
2102 assert sorted(collected) == sorted(gs_expected)
2105 class TestFileCollection:
2106 def test_include_exclude(self) -> None:
2107 path = THIS_DIR / "data" / "include_exclude_tests"
2110 Path(path / "b/dont_exclude/a.py"),
2111 Path(path / "b/dont_exclude/a.pyi"),
2113 assert_collected_sources(
2117 exclude=r"/exclude/|/\.definitely_exclude/",
2120 def test_gitignore_used_as_default(self) -> None:
2121 base = Path(DATA_DIR / "include_exclude_tests")
2123 base / "b/.definitely_exclude/a.py",
2124 base / "b/.definitely_exclude/a.pyi",
2128 ctx.obj["root"] = base
2129 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
2131 def test_gitignore_used_on_multiple_sources(self) -> None:
2132 root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
2134 root / "dir1" / "b.py",
2135 root / "dir2" / "b.py",
2138 ctx.obj["root"] = root
2139 src = [root / "dir1", root / "dir2"]
2140 assert_collected_sources(src, expected, ctx=ctx)
2142 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2143 def test_exclude_for_issue_1572(self) -> None:
2144 # Exclude shouldn't touch files that were explicitly given to Black through the
2145 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
2146 # https://github.com/psf/black/issues/1572
2147 path = DATA_DIR / "include_exclude_tests"
2148 src = [path / "b/exclude/a.py"]
2149 expected = [path / "b/exclude/a.py"]
2150 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2152 def test_gitignore_exclude(self) -> None:
2153 path = THIS_DIR / "data" / "include_exclude_tests"
2154 include = re.compile(r"\.pyi?$")
2155 exclude = re.compile(r"")
2156 report = black.Report()
2157 gitignore = PathSpec.from_lines(
2158 "gitwildmatch", ["exclude/", ".definitely_exclude"]
2160 sources: List[Path] = []
2162 Path(path / "b/dont_exclude/a.py"),
2163 Path(path / "b/dont_exclude/a.pyi"),
2165 this_abs = THIS_DIR.resolve()
2167 black.gen_python_files(
2180 assert sorted(expected) == sorted(sources)
2182 def test_nested_gitignore(self) -> None:
2183 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
2184 include = re.compile(r"\.pyi?$")
2185 exclude = re.compile(r"")
2186 root_gitignore = black.files.get_gitignore(path)
2187 report = black.Report()
2188 expected: List[Path] = [
2189 Path(path / "x.py"),
2190 Path(path / "root/b.py"),
2191 Path(path / "root/c.py"),
2192 Path(path / "root/child/c.py"),
2194 this_abs = THIS_DIR.resolve()
2196 black.gen_python_files(
2204 {path: root_gitignore},
2209 assert sorted(expected) == sorted(sources)
2211 def test_nested_gitignore_directly_in_source_directory(self) -> None:
2212 # https://github.com/psf/black/issues/2598
2213 path = Path(DATA_DIR / "nested_gitignore_tests")
2214 src = Path(path / "root" / "child")
2215 expected = [src / "a.py", src / "c.py"]
2216 assert_collected_sources([src], expected)
2218 def test_invalid_gitignore(self) -> None:
2219 path = THIS_DIR / "data" / "invalid_gitignore_tests"
2220 empty_config = path / "pyproject.toml"
2221 result = BlackRunner().invoke(
2222 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2224 assert result.exit_code == 1
2225 assert result.stderr_bytes is not None
2227 gitignore = path / ".gitignore"
2228 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2230 def test_invalid_nested_gitignore(self) -> None:
2231 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
2232 empty_config = path / "pyproject.toml"
2233 result = BlackRunner().invoke(
2234 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2236 assert result.exit_code == 1
2237 assert result.stderr_bytes is not None
2239 gitignore = path / "a" / ".gitignore"
2240 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2242 def test_gitignore_that_ignores_subfolders(self) -> None:
2243 # If gitignore with */* is in root
2244 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir")
2245 expected = [root / "b.py"]
2247 ctx.obj["root"] = root
2248 assert_collected_sources([root], expected, ctx=ctx)
2250 # If .gitignore with */* is nested
2251 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2254 root / "subdir" / "b.py",
2257 ctx.obj["root"] = root
2258 assert_collected_sources([root], expected, ctx=ctx)
2260 # If command is executed from outer dir
2261 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2262 target = root / "subdir"
2263 expected = [target / "b.py"]
2265 ctx.obj["root"] = root
2266 assert_collected_sources([target], expected, ctx=ctx)
2268 def test_empty_include(self) -> None:
2269 path = DATA_DIR / "include_exclude_tests"
2272 Path(path / "b/exclude/a.pie"),
2273 Path(path / "b/exclude/a.py"),
2274 Path(path / "b/exclude/a.pyi"),
2275 Path(path / "b/dont_exclude/a.pie"),
2276 Path(path / "b/dont_exclude/a.py"),
2277 Path(path / "b/dont_exclude/a.pyi"),
2278 Path(path / "b/.definitely_exclude/a.pie"),
2279 Path(path / "b/.definitely_exclude/a.py"),
2280 Path(path / "b/.definitely_exclude/a.pyi"),
2281 Path(path / ".gitignore"),
2282 Path(path / "pyproject.toml"),
2284 # Setting exclude explicitly to an empty string to block .gitignore usage.
2285 assert_collected_sources(src, expected, include="", exclude="")
2287 def test_extend_exclude(self) -> None:
2288 path = DATA_DIR / "include_exclude_tests"
2291 Path(path / "b/exclude/a.py"),
2292 Path(path / "b/dont_exclude/a.py"),
2294 assert_collected_sources(
2295 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
2298 @pytest.mark.incompatible_with_mypyc
2299 def test_symlink_out_of_root_directory(self) -> None:
2301 root = THIS_DIR.resolve()
2303 include = re.compile(black.DEFAULT_INCLUDES)
2304 exclude = re.compile(black.DEFAULT_EXCLUDES)
2305 report = black.Report()
2306 gitignore = PathSpec.from_lines("gitwildmatch", [])
2307 # `child` should behave like a symlink which resolved path is clearly
2308 # outside of the `root` directory.
2309 path.iterdir.return_value = [child]
2310 child.resolve.return_value = Path("/a/b/c")
2311 child.as_posix.return_value = "/a/b/c"
2314 black.gen_python_files(
2327 except ValueError as ve:
2328 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
2329 path.iterdir.assert_called_once()
2330 child.resolve.assert_called_once()
2332 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2333 def test_get_sources_with_stdin(self) -> None:
2336 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2338 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2339 def test_get_sources_with_stdin_filename(self) -> None:
2341 stdin_filename = str(THIS_DIR / "data/collections.py")
2342 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2343 assert_collected_sources(
2346 exclude=r"/exclude/a\.py",
2347 stdin_filename=stdin_filename,
2350 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2351 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2352 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2353 # file being passed directly. This is the same as
2354 # test_exclude_for_issue_1572
2355 path = DATA_DIR / "include_exclude_tests"
2357 stdin_filename = str(path / "b/exclude/a.py")
2358 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2359 assert_collected_sources(
2362 exclude=r"/exclude/|a\.py",
2363 stdin_filename=stdin_filename,
2366 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2367 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2368 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2369 # file being passed directly. This is the same as
2370 # test_exclude_for_issue_1572
2372 path = THIS_DIR / "data" / "include_exclude_tests"
2373 stdin_filename = str(path / "b/exclude/a.py")
2374 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2375 assert_collected_sources(
2378 extend_exclude=r"/exclude/|a\.py",
2379 stdin_filename=stdin_filename,
2382 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2383 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2384 # Force exclude should exclude the file when passing it through
2386 path = THIS_DIR / "data" / "include_exclude_tests"
2387 stdin_filename = str(path / "b/exclude/a.py")
2388 assert_collected_sources(
2391 force_exclude=r"/exclude/|a\.py",
2392 stdin_filename=stdin_filename,
2397 with open(black.__file__, "r", encoding="utf-8") as _bf:
2398 black_source_lines = _bf.readlines()
2399 except UnicodeDecodeError:
2400 if not black.COMPILED:
2405 frame: types.FrameType, event: str, arg: Any
2406 ) -> Callable[[types.FrameType, str, Any], Any]:
2407 """Show function calls `from black/__init__.py` as they happen.
2409 Register this with `sys.settrace()` in a test you're debugging.
2414 stack = len(inspect.stack()) - 19
2416 filename = frame.f_code.co_filename
2417 lineno = frame.f_lineno
2418 func_sig_lineno = lineno - 1
2419 funcname = black_source_lines[func_sig_lineno].strip()
2420 while funcname.startswith("@"):
2421 func_sig_lineno += 1
2422 funcname = black_source_lines[func_sig_lineno].strip()
2423 if "black/__init__.py" in filename:
2424 print(f"{' ' * stack}{lineno}:{funcname}")