All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
13 from concurrent.futures import ThreadPoolExecutor
14 from contextlib import contextmanager, redirect_stderr
15 from dataclasses import replace
16 from io import BytesIO
17 from pathlib import Path
18 from platform import system
19 from tempfile import TemporaryDirectory
32 from unittest.mock import MagicMock, patch
36 from click import unstyle
37 from click.testing import CliRunner
38 from pathspec import PathSpec
42 from black import Feature, TargetVersion
43 from black import re_compile_maybe_verbose as compile_pattern
44 from black.cache import get_cache_dir, get_cache_file
45 from black.debug import DebugVisitor
46 from black.output import color_diff, diff
47 from black.report import Report
49 # Import other test classes
50 from tests.util import (
68 THIS_FILE = Path(__file__)
69 EMPTY_CONFIG = THIS_DIR / "data" / "empty_pyproject.toml"
70 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
71 DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
72 DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
76 # Match the time output in a diff, but nothing else
77 DIFF_TIME = re.compile(r"\t[\d\-:+\. ]+")
81 def cache_dir(exists: bool = True) -> Iterator[Path]:
82 with TemporaryDirectory() as workspace:
83 cache_dir = Path(workspace)
85 cache_dir = cache_dir / "new"
86 with patch("black.cache.CACHE_DIR", cache_dir):
91 def event_loop() -> Iterator[None]:
92 policy = asyncio.get_event_loop_policy()
93 loop = policy.new_event_loop()
94 asyncio.set_event_loop(loop)
102 class FakeContext(click.Context):
103 """A fake click Context for when calling functions that need it."""
105 def __init__(self) -> None:
106 self.default_map: Dict[str, Any] = {}
107 self.params: Dict[str, Any] = {}
108 # Dummy root, since most of the tests don't care about it
109 self.obj: Dict[str, Any] = {"root": PROJECT_ROOT}
112 class FakeParameter(click.Parameter):
113 """A fake click Parameter for when calling functions that need it."""
115 def __init__(self) -> None:
119 class BlackRunner(CliRunner):
120 """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
122 def __init__(self) -> None:
123 super().__init__(mix_stderr=False)
127 args: List[str], exit_code: int = 0, ignore_config: bool = True
129 runner = BlackRunner()
131 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
132 result = runner.invoke(black.main, args, catch_exceptions=False)
133 assert result.stdout_bytes is not None
134 assert result.stderr_bytes is not None
136 f"Failed with args: {args}\n"
137 f"stdout: {result.stdout_bytes.decode()!r}\n"
138 f"stderr: {result.stderr_bytes.decode()!r}\n"
139 f"exception: {result.exception}"
141 assert result.exit_code == exit_code, msg
144 class BlackTestCase(BlackBaseTestCase):
145 invokeBlack = staticmethod(invokeBlack)
147 def test_empty_ff(self) -> None:
149 tmp_file = Path(black.dump_to_file())
151 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
152 with open(tmp_file, encoding="utf8") as f:
156 self.assertFormatEqual(expected, actual)
158 @patch("black.dump_to_file", dump_to_stderr)
159 def test_one_empty_line(self) -> None:
160 mode = black.Mode(preview=True)
161 for nl in ["\n", "\r\n"]:
162 source = expected = nl
163 assert_format(source, expected, mode=mode)
165 def test_one_empty_line_ff(self) -> None:
166 mode = black.Mode(preview=True)
167 for nl in ["\n", "\r\n"]:
169 tmp_file = Path(black.dump_to_file(nl))
170 if system() == "Windows":
171 # Writing files in text mode automatically uses the system newline,
172 # but in this case we don't want this for testing reasons. See:
173 # https://github.com/psf/black/pull/3348
174 with open(tmp_file, "wb") as f:
175 f.write(nl.encode("utf-8"))
178 ff(tmp_file, mode=mode, write_back=black.WriteBack.YES)
180 with open(tmp_file, "rb") as f:
181 actual = f.read().decode("utf8")
184 self.assertFormatEqual(expected, actual)
186 def test_experimental_string_processing_warns(self) -> None:
188 black.mode.Deprecated, black.Mode, experimental_string_processing=True
191 def test_piping(self) -> None:
192 source, expected = read_data_from_file(PROJECT_ROOT / "src/black/__init__.py")
193 result = BlackRunner().invoke(
198 f"--line-length={black.DEFAULT_LINE_LENGTH}",
199 f"--config={EMPTY_CONFIG}",
201 input=BytesIO(source.encode("utf8")),
203 self.assertEqual(result.exit_code, 0)
204 self.assertFormatEqual(expected, result.output)
205 if source != result.output:
206 black.assert_equivalent(source, result.output)
207 black.assert_stable(source, result.output, DEFAULT_MODE)
209 def test_piping_diff(self) -> None:
210 diff_header = re.compile(
211 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d"
214 source, _ = read_data("simple_cases", "expression.py")
215 expected, _ = read_data("simple_cases", "expression.diff")
219 f"--line-length={black.DEFAULT_LINE_LENGTH}",
221 f"--config={EMPTY_CONFIG}",
223 result = BlackRunner().invoke(
224 black.main, args, input=BytesIO(source.encode("utf8"))
226 self.assertEqual(result.exit_code, 0)
227 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
228 actual = actual.rstrip() + "\n" # the diff output has a trailing space
229 self.assertEqual(expected, actual)
231 def test_piping_diff_with_color(self) -> None:
232 source, _ = read_data("simple_cases", "expression.py")
236 f"--line-length={black.DEFAULT_LINE_LENGTH}",
239 f"--config={EMPTY_CONFIG}",
241 result = BlackRunner().invoke(
242 black.main, args, input=BytesIO(source.encode("utf8"))
244 actual = result.output
245 # Again, the contents are checked in a different test, so only look for colors.
246 self.assertIn("\033[1m", actual)
247 self.assertIn("\033[36m", actual)
248 self.assertIn("\033[32m", actual)
249 self.assertIn("\033[31m", actual)
250 self.assertIn("\033[0m", actual)
252 @patch("black.dump_to_file", dump_to_stderr)
253 def _test_wip(self) -> None:
254 source, expected = read_data("miscellaneous", "wip")
255 sys.settrace(tracefunc)
258 experimental_string_processing=False,
259 target_versions={black.TargetVersion.PY38},
261 actual = fs(source, mode=mode)
263 self.assertFormatEqual(expected, actual)
264 black.assert_equivalent(source, actual)
265 black.assert_stable(source, actual, black.FileMode())
267 def test_pep_572_version_detection(self) -> None:
268 source, _ = read_data("py_38", "pep_572")
269 root = black.lib2to3_parse(source)
270 features = black.get_features_used(root)
271 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
272 versions = black.detect_target_versions(root)
273 self.assertIn(black.TargetVersion.PY38, versions)
275 def test_pep_695_version_detection(self) -> None:
276 for file in ("type_aliases", "type_params"):
277 source, _ = read_data("py_312", file)
278 root = black.lib2to3_parse(source)
279 features = black.get_features_used(root)
280 self.assertIn(black.Feature.TYPE_PARAMS, features)
281 versions = black.detect_target_versions(root)
282 self.assertIn(black.TargetVersion.PY312, versions)
284 def test_expression_ff(self) -> None:
285 source, expected = read_data("simple_cases", "expression.py")
286 tmp_file = Path(black.dump_to_file(source))
288 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
289 with open(tmp_file, encoding="utf8") as f:
293 self.assertFormatEqual(expected, actual)
294 with patch("black.dump_to_file", dump_to_stderr):
295 black.assert_equivalent(source, actual)
296 black.assert_stable(source, actual, DEFAULT_MODE)
298 def test_expression_diff(self) -> None:
299 source, _ = read_data("simple_cases", "expression.py")
300 expected, _ = read_data("simple_cases", "expression.diff")
301 tmp_file = Path(black.dump_to_file(source))
302 diff_header = re.compile(
303 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
304 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
307 result = BlackRunner().invoke(
308 black.main, ["--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
310 self.assertEqual(result.exit_code, 0)
313 actual = result.output
314 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
315 if expected != actual:
316 dump = black.dump_to_file(actual)
318 "Expected diff isn't equal to the actual. If you made changes to"
319 " expression.py and this is an anticipated difference, overwrite"
320 f" tests/data/expression.diff with {dump}"
322 self.assertEqual(expected, actual, msg)
324 def test_expression_diff_with_color(self) -> None:
325 source, _ = read_data("simple_cases", "expression.py")
326 expected, _ = read_data("simple_cases", "expression.diff")
327 tmp_file = Path(black.dump_to_file(source))
329 result = BlackRunner().invoke(
331 ["--diff", "--color", str(tmp_file), f"--config={EMPTY_CONFIG}"],
335 actual = result.output
336 # We check the contents of the diff in `test_expression_diff`. All
337 # we need to check here is that color codes exist in the result.
338 self.assertIn("\033[1m", actual)
339 self.assertIn("\033[36m", actual)
340 self.assertIn("\033[32m", actual)
341 self.assertIn("\033[31m", actual)
342 self.assertIn("\033[0m", actual)
344 def test_detect_pos_only_arguments(self) -> None:
345 source, _ = read_data("py_38", "pep_570")
346 root = black.lib2to3_parse(source)
347 features = black.get_features_used(root)
348 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
349 versions = black.detect_target_versions(root)
350 self.assertIn(black.TargetVersion.PY38, versions)
352 def test_detect_debug_f_strings(self) -> None:
353 root = black.lib2to3_parse("""f"{x=}" """)
354 features = black.get_features_used(root)
355 self.assertIn(black.Feature.DEBUG_F_STRINGS, features)
356 versions = black.detect_target_versions(root)
357 self.assertIn(black.TargetVersion.PY38, versions)
359 root = black.lib2to3_parse(
360 """f"{x}"\nf'{"="}'\nf'{(x:=5)}'\nf'{f(a="3=")}'\nf'{x:=10}'\n"""
362 features = black.get_features_used(root)
363 self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
365 # We don't yet support feature version detection in nested f-strings
366 root = black.lib2to3_parse(
367 """f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """
369 features = black.get_features_used(root)
370 self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
372 @patch("black.dump_to_file", dump_to_stderr)
373 def test_string_quotes(self) -> None:
374 source, expected = read_data("miscellaneous", "string_quotes")
375 mode = black.Mode(preview=True)
376 assert_format(source, expected, mode)
377 mode = replace(mode, string_normalization=False)
378 not_normalized = fs(source, mode=mode)
379 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
380 black.assert_equivalent(source, not_normalized)
381 black.assert_stable(source, not_normalized, mode=mode)
383 def test_skip_source_first_line(self) -> None:
384 source, _ = read_data("miscellaneous", "invalid_header")
385 tmp_file = Path(black.dump_to_file(source))
386 # Full source should fail (invalid syntax at header)
387 self.invokeBlack([str(tmp_file), "--diff", "--check"], exit_code=123)
388 # So, skipping the first line should work
389 result = BlackRunner().invoke(
390 black.main, [str(tmp_file), "-x", f"--config={EMPTY_CONFIG}"]
392 self.assertEqual(result.exit_code, 0)
393 with open(tmp_file, encoding="utf8") as f:
395 self.assertFormatEqual(source, actual)
397 def test_skip_source_first_line_when_mixing_newlines(self) -> None:
398 code_mixing_newlines = b"Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n"
399 expected = b"Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n"
400 with TemporaryDirectory() as workspace:
401 test_file = Path(workspace) / "skip_header.py"
402 test_file.write_bytes(code_mixing_newlines)
403 mode = replace(DEFAULT_MODE, skip_source_first_line=True)
404 ff(test_file, mode=mode, write_back=black.WriteBack.YES)
405 self.assertEqual(test_file.read_bytes(), expected)
407 def test_skip_magic_trailing_comma(self) -> None:
408 source, _ = read_data("simple_cases", "expression")
409 expected, _ = read_data(
410 "miscellaneous", "expression_skip_magic_trailing_comma.diff"
412 tmp_file = Path(black.dump_to_file(source))
413 diff_header = re.compile(
414 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
415 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
418 result = BlackRunner().invoke(
419 black.main, ["-C", "--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
421 self.assertEqual(result.exit_code, 0)
424 actual = result.output
425 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
426 actual = actual.rstrip() + "\n" # the diff output has a trailing space
427 if expected != actual:
428 dump = black.dump_to_file(actual)
430 "Expected diff isn't equal to the actual. If you made changes to"
431 " expression.py and this is an anticipated difference, overwrite"
432 " tests/data/miscellaneous/expression_skip_magic_trailing_comma.diff"
435 self.assertEqual(expected, actual, msg)
437 @patch("black.dump_to_file", dump_to_stderr)
438 def test_async_as_identifier(self) -> None:
439 source_path = get_case_path("miscellaneous", "async_as_identifier")
440 source, expected = read_data_from_file(source_path)
442 self.assertFormatEqual(expected, actual)
443 major, minor = sys.version_info[:2]
444 if major < 3 or (major <= 3 and minor < 7):
445 black.assert_equivalent(source, actual)
446 black.assert_stable(source, actual, DEFAULT_MODE)
447 # ensure black can parse this when the target is 3.6
448 self.invokeBlack([str(source_path), "--target-version", "py36"])
449 # but not on 3.7, because async/await is no longer an identifier
450 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
452 @patch("black.dump_to_file", dump_to_stderr)
453 def test_python37(self) -> None:
454 source_path = get_case_path("py_37", "python37")
455 source, expected = read_data_from_file(source_path)
457 self.assertFormatEqual(expected, actual)
458 major, minor = sys.version_info[:2]
459 if major > 3 or (major == 3 and minor >= 7):
460 black.assert_equivalent(source, actual)
461 black.assert_stable(source, actual, DEFAULT_MODE)
462 # ensure black can parse this when the target is 3.7
463 self.invokeBlack([str(source_path), "--target-version", "py37"])
464 # but not on 3.6, because we use async as a reserved keyword
465 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
467 def test_tab_comment_indentation(self) -> None:
468 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\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\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\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 # mixed tabs and spaces (valid Python 2 code)
479 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
480 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
481 self.assertFormatEqual(contents_spc, fs(contents_spc))
482 self.assertFormatEqual(contents_spc, fs(contents_tab))
484 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
485 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
486 self.assertFormatEqual(contents_spc, fs(contents_spc))
487 self.assertFormatEqual(contents_spc, fs(contents_tab))
489 def test_false_positive_symlink_output_issue_3384(self) -> None:
490 # Emulate the behavior when using the CLI (`black ./child --verbose`), which
491 # involves patching some `pathlib.Path` methods. In particular, `is_dir` is
492 # patched only on its first call: when checking if "./child" is a directory it
493 # should return True. The "./child" folder exists relative to the cwd when
494 # running from CLI, but fails when running the tests because cwd is different
495 project_root = Path(THIS_DIR / "data" / "nested_gitignore_tests")
496 working_directory = project_root / "root"
497 target_abspath = working_directory / "child"
499 src.relative_to(working_directory) for src in target_abspath.iterdir()
502 def mock_n_calls(responses: List[bool]) -> Callable[[], bool]:
503 def _mocked_calls() -> bool:
505 return responses.pop(0)
510 with patch("pathlib.Path.iterdir", return_value=target_contents), patch(
511 "pathlib.Path.cwd", return_value=working_directory
512 ), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])):
514 ctx.obj["root"] = project_root
515 report = MagicMock(verbose=True)
521 include=DEFAULT_INCLUDE,
529 mock_args[1].startswith("is a symbolic link that points outside")
530 for _, mock_args, _ in report.path_ignored.mock_calls
531 ), "A symbolic link was reported."
532 report.path_ignored.assert_called_once_with(
533 Path("child", "b.py"), "matches a .gitignore file content"
536 def test_report_verbose(self) -> None:
537 report = Report(verbose=True)
541 def out(msg: str, **kwargs: Any) -> None:
542 out_lines.append(msg)
544 def err(msg: str, **kwargs: Any) -> None:
545 err_lines.append(msg)
547 with patch("black.output._out", out), patch("black.output._err", err):
548 report.done(Path("f1"), black.Changed.NO)
549 self.assertEqual(len(out_lines), 1)
550 self.assertEqual(len(err_lines), 0)
551 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
552 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
553 self.assertEqual(report.return_code, 0)
554 report.done(Path("f2"), black.Changed.YES)
555 self.assertEqual(len(out_lines), 2)
556 self.assertEqual(len(err_lines), 0)
557 self.assertEqual(out_lines[-1], "reformatted f2")
559 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
561 report.done(Path("f3"), black.Changed.CACHED)
562 self.assertEqual(len(out_lines), 3)
563 self.assertEqual(len(err_lines), 0)
565 out_lines[-1], "f3 wasn't modified on disk since last run."
568 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
570 self.assertEqual(report.return_code, 0)
572 self.assertEqual(report.return_code, 1)
574 report.failed(Path("e1"), "boom")
575 self.assertEqual(len(out_lines), 3)
576 self.assertEqual(len(err_lines), 1)
577 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
579 unstyle(str(report)),
580 "1 file reformatted, 2 files left unchanged, 1 file failed to"
583 self.assertEqual(report.return_code, 123)
584 report.done(Path("f3"), black.Changed.YES)
585 self.assertEqual(len(out_lines), 4)
586 self.assertEqual(len(err_lines), 1)
587 self.assertEqual(out_lines[-1], "reformatted f3")
589 unstyle(str(report)),
590 "2 files reformatted, 2 files left unchanged, 1 file failed to"
593 self.assertEqual(report.return_code, 123)
594 report.failed(Path("e2"), "boom")
595 self.assertEqual(len(out_lines), 4)
596 self.assertEqual(len(err_lines), 2)
597 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
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.path_ignored(Path("wat"), "no match")
605 self.assertEqual(len(out_lines), 5)
606 self.assertEqual(len(err_lines), 2)
607 self.assertEqual(out_lines[-1], "wat ignored: no match")
609 unstyle(str(report)),
610 "2 files reformatted, 2 files left unchanged, 2 files failed to"
613 self.assertEqual(report.return_code, 123)
614 report.done(Path("f4"), black.Changed.NO)
615 self.assertEqual(len(out_lines), 6)
616 self.assertEqual(len(err_lines), 2)
617 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
619 unstyle(str(report)),
620 "2 files reformatted, 3 files left unchanged, 2 files failed to"
623 self.assertEqual(report.return_code, 123)
626 unstyle(str(report)),
627 "2 files would be reformatted, 3 files would be left unchanged, 2"
628 " files would fail to reformat.",
633 unstyle(str(report)),
634 "2 files would be reformatted, 3 files would be left unchanged, 2"
635 " files would fail to reformat.",
638 def test_report_quiet(self) -> None:
639 report = Report(quiet=True)
643 def out(msg: str, **kwargs: Any) -> None:
644 out_lines.append(msg)
646 def err(msg: str, **kwargs: Any) -> None:
647 err_lines.append(msg)
649 with patch("black.output._out", out), patch("black.output._err", err):
650 report.done(Path("f1"), black.Changed.NO)
651 self.assertEqual(len(out_lines), 0)
652 self.assertEqual(len(err_lines), 0)
653 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
654 self.assertEqual(report.return_code, 0)
655 report.done(Path("f2"), black.Changed.YES)
656 self.assertEqual(len(out_lines), 0)
657 self.assertEqual(len(err_lines), 0)
659 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
661 report.done(Path("f3"), black.Changed.CACHED)
662 self.assertEqual(len(out_lines), 0)
663 self.assertEqual(len(err_lines), 0)
665 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
667 self.assertEqual(report.return_code, 0)
669 self.assertEqual(report.return_code, 1)
671 report.failed(Path("e1"), "boom")
672 self.assertEqual(len(out_lines), 0)
673 self.assertEqual(len(err_lines), 1)
674 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
676 unstyle(str(report)),
677 "1 file reformatted, 2 files left unchanged, 1 file failed to"
680 self.assertEqual(report.return_code, 123)
681 report.done(Path("f3"), black.Changed.YES)
682 self.assertEqual(len(out_lines), 0)
683 self.assertEqual(len(err_lines), 1)
685 unstyle(str(report)),
686 "2 files reformatted, 2 files left unchanged, 1 file failed to"
689 self.assertEqual(report.return_code, 123)
690 report.failed(Path("e2"), "boom")
691 self.assertEqual(len(out_lines), 0)
692 self.assertEqual(len(err_lines), 2)
693 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
695 unstyle(str(report)),
696 "2 files reformatted, 2 files left unchanged, 2 files failed to"
699 self.assertEqual(report.return_code, 123)
700 report.path_ignored(Path("wat"), "no match")
701 self.assertEqual(len(out_lines), 0)
702 self.assertEqual(len(err_lines), 2)
704 unstyle(str(report)),
705 "2 files reformatted, 2 files left unchanged, 2 files failed to"
708 self.assertEqual(report.return_code, 123)
709 report.done(Path("f4"), black.Changed.NO)
710 self.assertEqual(len(out_lines), 0)
711 self.assertEqual(len(err_lines), 2)
713 unstyle(str(report)),
714 "2 files reformatted, 3 files left unchanged, 2 files failed to"
717 self.assertEqual(report.return_code, 123)
720 unstyle(str(report)),
721 "2 files would be reformatted, 3 files would be left unchanged, 2"
722 " files would fail to reformat.",
727 unstyle(str(report)),
728 "2 files would be reformatted, 3 files would be left unchanged, 2"
729 " files would fail to reformat.",
732 def test_report_normal(self) -> None:
733 report = black.Report()
737 def out(msg: str, **kwargs: Any) -> None:
738 out_lines.append(msg)
740 def err(msg: str, **kwargs: Any) -> None:
741 err_lines.append(msg)
743 with patch("black.output._out", out), patch("black.output._err", err):
744 report.done(Path("f1"), black.Changed.NO)
745 self.assertEqual(len(out_lines), 0)
746 self.assertEqual(len(err_lines), 0)
747 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
748 self.assertEqual(report.return_code, 0)
749 report.done(Path("f2"), black.Changed.YES)
750 self.assertEqual(len(out_lines), 1)
751 self.assertEqual(len(err_lines), 0)
752 self.assertEqual(out_lines[-1], "reformatted f2")
754 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
756 report.done(Path("f3"), black.Changed.CACHED)
757 self.assertEqual(len(out_lines), 1)
758 self.assertEqual(len(err_lines), 0)
759 self.assertEqual(out_lines[-1], "reformatted f2")
761 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
763 self.assertEqual(report.return_code, 0)
765 self.assertEqual(report.return_code, 1)
767 report.failed(Path("e1"), "boom")
768 self.assertEqual(len(out_lines), 1)
769 self.assertEqual(len(err_lines), 1)
770 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
772 unstyle(str(report)),
773 "1 file reformatted, 2 files left unchanged, 1 file failed to"
776 self.assertEqual(report.return_code, 123)
777 report.done(Path("f3"), black.Changed.YES)
778 self.assertEqual(len(out_lines), 2)
779 self.assertEqual(len(err_lines), 1)
780 self.assertEqual(out_lines[-1], "reformatted f3")
782 unstyle(str(report)),
783 "2 files reformatted, 2 files left unchanged, 1 file failed to"
786 self.assertEqual(report.return_code, 123)
787 report.failed(Path("e2"), "boom")
788 self.assertEqual(len(out_lines), 2)
789 self.assertEqual(len(err_lines), 2)
790 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
792 unstyle(str(report)),
793 "2 files reformatted, 2 files left unchanged, 2 files failed to"
796 self.assertEqual(report.return_code, 123)
797 report.path_ignored(Path("wat"), "no match")
798 self.assertEqual(len(out_lines), 2)
799 self.assertEqual(len(err_lines), 2)
801 unstyle(str(report)),
802 "2 files reformatted, 2 files left unchanged, 2 files failed to"
805 self.assertEqual(report.return_code, 123)
806 report.done(Path("f4"), black.Changed.NO)
807 self.assertEqual(len(out_lines), 2)
808 self.assertEqual(len(err_lines), 2)
810 unstyle(str(report)),
811 "2 files reformatted, 3 files left unchanged, 2 files failed to"
814 self.assertEqual(report.return_code, 123)
817 unstyle(str(report)),
818 "2 files would be reformatted, 3 files would be left unchanged, 2"
819 " files would fail to reformat.",
824 unstyle(str(report)),
825 "2 files would be reformatted, 3 files would be left unchanged, 2"
826 " files would fail to reformat.",
829 def test_lib2to3_parse(self) -> None:
830 with self.assertRaises(black.InvalidInput):
831 black.lib2to3_parse("invalid syntax")
834 black.lib2to3_parse(straddling)
835 black.lib2to3_parse(straddling, {TargetVersion.PY36})
838 with self.assertRaises(black.InvalidInput):
839 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
841 py3_only = "exec(x, end=y)"
842 black.lib2to3_parse(py3_only)
843 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
845 def test_get_features_used_decorator(self) -> None:
846 # Test the feature detection of new decorator syntax
847 # since this makes some test cases of test_get_features_used()
848 # fails if it fails, this is tested first so that a useful case
850 simples, relaxed = read_data("miscellaneous", "decorators")
851 # skip explanation comments at the top of the file
852 for simple_test in simples.split("##")[1:]:
853 node = black.lib2to3_parse(simple_test)
854 decorator = str(node.children[0].children[0]).strip()
856 Feature.RELAXED_DECORATORS,
857 black.get_features_used(node),
859 f"decorator '{decorator}' follows python<=3.8 syntax"
860 "but is detected as 3.9+"
861 # f"The full node is\n{node!r}"
864 # skip the '# output' comment at the top of the output part
865 for relaxed_test in relaxed.split("##")[1:]:
866 node = black.lib2to3_parse(relaxed_test)
867 decorator = str(node.children[0].children[0]).strip()
869 Feature.RELAXED_DECORATORS,
870 black.get_features_used(node),
872 f"decorator '{decorator}' uses python3.9+ syntax"
873 "but is detected as python<=3.8"
874 # f"The full node is\n{node!r}"
878 def test_get_features_used(self) -> None:
879 node = black.lib2to3_parse("def f(*, arg): ...\n")
880 self.assertEqual(black.get_features_used(node), set())
881 node = black.lib2to3_parse("def f(*, arg,): ...\n")
882 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
883 node = black.lib2to3_parse("f(*arg,)\n")
885 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
887 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
888 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
889 node = black.lib2to3_parse("123_456\n")
890 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
891 node = black.lib2to3_parse("123456\n")
892 self.assertEqual(black.get_features_used(node), set())
893 source, expected = read_data("simple_cases", "function")
894 node = black.lib2to3_parse(source)
895 expected_features = {
896 Feature.TRAILING_COMMA_IN_CALL,
897 Feature.TRAILING_COMMA_IN_DEF,
900 self.assertEqual(black.get_features_used(node), expected_features)
901 node = black.lib2to3_parse(expected)
902 self.assertEqual(black.get_features_used(node), expected_features)
903 source, expected = read_data("simple_cases", "expression")
904 node = black.lib2to3_parse(source)
905 self.assertEqual(black.get_features_used(node), set())
906 node = black.lib2to3_parse(expected)
907 self.assertEqual(black.get_features_used(node), set())
908 node = black.lib2to3_parse("lambda a, /, b: ...")
909 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
910 node = black.lib2to3_parse("def fn(a, /, b): ...")
911 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
912 node = black.lib2to3_parse("def fn(): yield a, b")
913 self.assertEqual(black.get_features_used(node), set())
914 node = black.lib2to3_parse("def fn(): return a, b")
915 self.assertEqual(black.get_features_used(node), set())
916 node = black.lib2to3_parse("def fn(): yield *b, c")
917 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
918 node = black.lib2to3_parse("def fn(): return a, *b, c")
919 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
920 node = black.lib2to3_parse("x = a, *b, c")
921 self.assertEqual(black.get_features_used(node), set())
922 node = black.lib2to3_parse("x: Any = regular")
923 self.assertEqual(black.get_features_used(node), set())
924 node = black.lib2to3_parse("x: Any = (regular, regular)")
925 self.assertEqual(black.get_features_used(node), set())
926 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
927 self.assertEqual(black.get_features_used(node), set())
928 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
930 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
932 node = black.lib2to3_parse("try: pass\nexcept Something: pass")
933 self.assertEqual(black.get_features_used(node), set())
934 node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass")
935 self.assertEqual(black.get_features_used(node), set())
936 node = black.lib2to3_parse("try: pass\nexcept *Group: pass")
937 self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR})
938 node = black.lib2to3_parse("a[*b]")
939 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
940 node = black.lib2to3_parse("a[x, *y(), z] = t")
941 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
942 node = black.lib2to3_parse("def fn(*args: *T): pass")
943 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
945 def test_get_features_used_for_future_flags(self) -> None:
946 for src, features in [
947 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
949 "from __future__ import (other, annotations)",
950 {Feature.FUTURE_ANNOTATIONS},
952 ("a = 1 + 2\nfrom something import annotations", set()),
953 ("from __future__ import x, y", set()),
955 with self.subTest(src=src, features=features):
956 node = black.lib2to3_parse(src)
957 future_imports = black.get_future_imports(node)
959 black.get_features_used(node, future_imports=future_imports),
963 def test_get_future_imports(self) -> None:
964 node = black.lib2to3_parse("\n")
965 self.assertEqual(set(), black.get_future_imports(node))
966 node = black.lib2to3_parse("from __future__ import black\n")
967 self.assertEqual({"black"}, black.get_future_imports(node))
968 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
969 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
970 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
971 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
972 node = black.lib2to3_parse(
973 "from __future__ import multiple\nfrom __future__ import imports\n"
975 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
976 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
977 self.assertEqual({"black"}, black.get_future_imports(node))
978 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
979 self.assertEqual({"black"}, black.get_future_imports(node))
980 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
981 self.assertEqual(set(), black.get_future_imports(node))
982 node = black.lib2to3_parse("from some.module import black\n")
983 self.assertEqual(set(), black.get_future_imports(node))
984 node = black.lib2to3_parse(
985 "from __future__ import unicode_literals as _unicode_literals"
987 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
988 node = black.lib2to3_parse(
989 "from __future__ import unicode_literals as _lol, print"
991 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
993 @pytest.mark.incompatible_with_mypyc
994 def test_debug_visitor(self) -> None:
995 source, _ = read_data("miscellaneous", "debug_visitor")
996 expected, _ = read_data("miscellaneous", "debug_visitor.out")
1000 def out(msg: str, **kwargs: Any) -> None:
1001 out_lines.append(msg)
1003 def err(msg: str, **kwargs: Any) -> None:
1004 err_lines.append(msg)
1006 with patch("black.debug.out", out):
1007 DebugVisitor.show(source)
1008 actual = "\n".join(out_lines) + "\n"
1010 if expected != actual:
1011 log_name = black.dump_to_file(*out_lines)
1015 f"AST print out is different. Actual version dumped to {log_name}",
1018 def test_format_file_contents(self) -> None:
1021 with self.assertRaises(black.NothingChanged):
1022 black.format_file_contents(empty, mode=mode, fast=False)
1024 with self.assertRaises(black.NothingChanged):
1025 black.format_file_contents(just_nl, mode=mode, fast=False)
1026 same = "j = [1, 2, 3]\n"
1027 with self.assertRaises(black.NothingChanged):
1028 black.format_file_contents(same, mode=mode, fast=False)
1029 different = "j = [1,2,3]"
1031 actual = black.format_file_contents(different, mode=mode, fast=False)
1032 self.assertEqual(expected, actual)
1033 invalid = "return if you can"
1034 with self.assertRaises(black.InvalidInput) as e:
1035 black.format_file_contents(invalid, mode=mode, fast=False)
1036 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
1038 mode = black.Mode(preview=True)
1040 with self.assertRaises(black.NothingChanged):
1041 black.format_file_contents(just_crlf, mode=mode, fast=False)
1042 just_whitespace_nl = "\n\t\n \n\t \n \t\n\n"
1043 actual = black.format_file_contents(just_whitespace_nl, mode=mode, fast=False)
1044 self.assertEqual("\n", actual)
1045 just_whitespace_crlf = "\r\n\t\r\n \r\n\t \r\n \t\r\n\r\n"
1046 actual = black.format_file_contents(just_whitespace_crlf, mode=mode, fast=False)
1047 self.assertEqual("\r\n", actual)
1049 def test_endmarker(self) -> None:
1050 n = black.lib2to3_parse("\n")
1051 self.assertEqual(n.type, black.syms.file_input)
1052 self.assertEqual(len(n.children), 1)
1053 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
1055 @pytest.mark.incompatible_with_mypyc
1056 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
1057 def test_assertFormatEqual(self) -> None:
1061 def out(msg: str, **kwargs: Any) -> None:
1062 out_lines.append(msg)
1064 def err(msg: str, **kwargs: Any) -> None:
1065 err_lines.append(msg)
1067 with patch("black.output._out", out), patch("black.output._err", err):
1068 with self.assertRaises(AssertionError):
1069 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
1071 out_str = "".join(out_lines)
1072 self.assertIn("Expected tree:", out_str)
1073 self.assertIn("Actual tree:", out_str)
1074 self.assertEqual("".join(err_lines), "")
1077 @patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1078 def test_works_in_mono_process_only_environment(self) -> None:
1079 with cache_dir() as workspace:
1081 (workspace / "one.py").resolve(),
1082 (workspace / "two.py").resolve(),
1084 f.write_text('print("hello")\n')
1085 self.invokeBlack([str(workspace)])
1088 def test_check_diff_use_together(self) -> None:
1090 # Files which will be reformatted.
1091 src1 = get_case_path("miscellaneous", "string_quotes")
1092 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1093 # Files which will not be reformatted.
1094 src2 = get_case_path("simple_cases", "composition")
1095 self.invokeBlack([str(src2), "--diff", "--check"])
1096 # Multi file command.
1097 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1099 def test_no_src_fails(self) -> None:
1101 self.invokeBlack([], exit_code=1)
1103 def test_src_and_code_fails(self) -> None:
1105 self.invokeBlack([".", "-c", "0"], exit_code=1)
1107 def test_broken_symlink(self) -> None:
1108 with cache_dir() as workspace:
1109 symlink = workspace / "broken_link.py"
1111 symlink.symlink_to("nonexistent.py")
1112 except (OSError, NotImplementedError) as e:
1113 self.skipTest(f"Can't create symlinks: {e}")
1114 self.invokeBlack([str(workspace.resolve())])
1116 def test_single_file_force_pyi(self) -> None:
1117 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1118 contents, expected = read_data("miscellaneous", "force_pyi")
1119 with cache_dir() as workspace:
1120 path = (workspace / "file.py").resolve()
1121 with open(path, "w") as fh:
1123 self.invokeBlack([str(path), "--pyi"])
1124 with open(path, "r") as fh:
1126 # verify cache with --pyi is separate
1127 pyi_cache = black.read_cache(pyi_mode)
1128 self.assertIn(str(path), pyi_cache)
1129 normal_cache = black.read_cache(DEFAULT_MODE)
1130 self.assertNotIn(str(path), normal_cache)
1131 self.assertFormatEqual(expected, actual)
1132 black.assert_equivalent(contents, actual)
1133 black.assert_stable(contents, actual, pyi_mode)
1136 def test_multi_file_force_pyi(self) -> None:
1137 reg_mode = DEFAULT_MODE
1138 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1139 contents, expected = read_data("miscellaneous", "force_pyi")
1140 with cache_dir() as workspace:
1142 (workspace / "file1.py").resolve(),
1143 (workspace / "file2.py").resolve(),
1146 with open(path, "w") as fh:
1148 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1150 with open(path, "r") as fh:
1152 self.assertEqual(actual, expected)
1153 # verify cache with --pyi is separate
1154 pyi_cache = black.read_cache(pyi_mode)
1155 normal_cache = black.read_cache(reg_mode)
1157 self.assertIn(str(path), pyi_cache)
1158 self.assertNotIn(str(path), normal_cache)
1160 def test_pipe_force_pyi(self) -> None:
1161 source, expected = read_data("miscellaneous", "force_pyi")
1162 result = CliRunner().invoke(
1163 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1165 self.assertEqual(result.exit_code, 0)
1166 actual = result.output
1167 self.assertFormatEqual(actual, expected)
1169 def test_single_file_force_py36(self) -> None:
1170 reg_mode = DEFAULT_MODE
1171 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1172 source, expected = read_data("miscellaneous", "force_py36")
1173 with cache_dir() as workspace:
1174 path = (workspace / "file.py").resolve()
1175 with open(path, "w") as fh:
1177 self.invokeBlack([str(path), *PY36_ARGS])
1178 with open(path, "r") as fh:
1180 # verify cache with --target-version is separate
1181 py36_cache = black.read_cache(py36_mode)
1182 self.assertIn(str(path), py36_cache)
1183 normal_cache = black.read_cache(reg_mode)
1184 self.assertNotIn(str(path), normal_cache)
1185 self.assertEqual(actual, expected)
1188 def test_multi_file_force_py36(self) -> None:
1189 reg_mode = DEFAULT_MODE
1190 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1191 source, expected = read_data("miscellaneous", "force_py36")
1192 with cache_dir() as workspace:
1194 (workspace / "file1.py").resolve(),
1195 (workspace / "file2.py").resolve(),
1198 with open(path, "w") as fh:
1200 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1202 with open(path, "r") as fh:
1204 self.assertEqual(actual, expected)
1205 # verify cache with --target-version is separate
1206 pyi_cache = black.read_cache(py36_mode)
1207 normal_cache = black.read_cache(reg_mode)
1209 self.assertIn(str(path), pyi_cache)
1210 self.assertNotIn(str(path), normal_cache)
1212 def test_pipe_force_py36(self) -> None:
1213 source, expected = read_data("miscellaneous", "force_py36")
1214 result = CliRunner().invoke(
1216 ["-", "-q", "--target-version=py36"],
1217 input=BytesIO(source.encode("utf8")),
1219 self.assertEqual(result.exit_code, 0)
1220 actual = result.output
1221 self.assertFormatEqual(actual, expected)
1223 @pytest.mark.incompatible_with_mypyc
1224 def test_reformat_one_with_stdin(self) -> None:
1226 "black.format_stdin_to_stdout",
1227 return_value=lambda *args, **kwargs: black.Changed.YES,
1229 report = MagicMock()
1234 write_back=black.WriteBack.YES,
1238 fsts.assert_called_once()
1239 report.done.assert_called_with(path, black.Changed.YES)
1241 @pytest.mark.incompatible_with_mypyc
1242 def test_reformat_one_with_stdin_filename(self) -> None:
1244 "black.format_stdin_to_stdout",
1245 return_value=lambda *args, **kwargs: black.Changed.YES,
1247 report = MagicMock()
1249 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1254 write_back=black.WriteBack.YES,
1258 fsts.assert_called_once_with(
1259 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1261 # __BLACK_STDIN_FILENAME__ should have been stripped
1262 report.done.assert_called_with(expected, black.Changed.YES)
1264 @pytest.mark.incompatible_with_mypyc
1265 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1267 "black.format_stdin_to_stdout",
1268 return_value=lambda *args, **kwargs: black.Changed.YES,
1270 report = MagicMock()
1272 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1277 write_back=black.WriteBack.YES,
1281 fsts.assert_called_once_with(
1283 write_back=black.WriteBack.YES,
1284 mode=replace(DEFAULT_MODE, is_pyi=True),
1286 # __BLACK_STDIN_FILENAME__ should have been stripped
1287 report.done.assert_called_with(expected, black.Changed.YES)
1289 @pytest.mark.incompatible_with_mypyc
1290 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1292 "black.format_stdin_to_stdout",
1293 return_value=lambda *args, **kwargs: black.Changed.YES,
1295 report = MagicMock()
1297 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1302 write_back=black.WriteBack.YES,
1306 fsts.assert_called_once_with(
1308 write_back=black.WriteBack.YES,
1309 mode=replace(DEFAULT_MODE, is_ipynb=True),
1311 # __BLACK_STDIN_FILENAME__ should have been stripped
1312 report.done.assert_called_with(expected, black.Changed.YES)
1314 @pytest.mark.incompatible_with_mypyc
1315 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1317 "black.format_stdin_to_stdout",
1318 return_value=lambda *args, **kwargs: black.Changed.YES,
1320 report = MagicMock()
1321 # Even with an existing file, since we are forcing stdin, black
1322 # should output to stdout and not modify the file inplace
1323 p = THIS_DIR / "data" / "simple_cases" / "collections.py"
1324 # Make sure is_file actually returns True
1325 self.assertTrue(p.is_file())
1326 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1331 write_back=black.WriteBack.YES,
1335 fsts.assert_called_once()
1336 # __BLACK_STDIN_FILENAME__ should have been stripped
1337 report.done.assert_called_with(expected, black.Changed.YES)
1339 def test_reformat_one_with_stdin_empty(self) -> None:
1346 (" \t\r\n\t ", "\r\n"),
1350 output: io.StringIO, io_TextIOWrapper: Type[io.TextIOWrapper]
1351 ) -> Callable[[Any, Any], io.TextIOWrapper]:
1352 def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper:
1353 if args == (sys.stdout.buffer,):
1354 # It's `format_stdin_to_stdout()` calling `io.TextIOWrapper()`,
1355 # return our mock object.
1357 # It's something else (i.e. `decode_bytes()`) calling
1358 # `io.TextIOWrapper()`, pass through to the original implementation.
1359 # See discussion in https://github.com/psf/black/pull/2489
1360 return io_TextIOWrapper(*args, **kwargs)
1364 mode = black.Mode(preview=True)
1365 for content, expected in cases:
1366 output = io.StringIO()
1367 io_TextIOWrapper = io.TextIOWrapper
1369 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1371 black.format_stdin_to_stdout(
1374 write_back=black.WriteBack.YES,
1377 except io.UnsupportedOperation:
1378 pass # StringIO does not support detach
1379 assert output.getvalue() == expected
1381 # An empty string is the only test case for `preview=False`
1382 output = io.StringIO()
1383 io_TextIOWrapper = io.TextIOWrapper
1384 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1386 black.format_stdin_to_stdout(
1389 write_back=black.WriteBack.YES,
1392 except io.UnsupportedOperation:
1393 pass # StringIO does not support detach
1394 assert output.getvalue() == ""
1396 def test_invalid_cli_regex(self) -> None:
1397 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1398 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1400 def test_required_version_matches_version(self) -> None:
1402 ["--required-version", black.__version__, "-c", "0"],
1407 def test_required_version_matches_partial_version(self) -> None:
1409 ["--required-version", black.__version__.split(".")[0], "-c", "0"],
1414 def test_required_version_does_not_match_on_minor_version(self) -> None:
1416 ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
1421 def test_required_version_does_not_match_version(self) -> None:
1422 result = BlackRunner().invoke(
1424 ["--required-version", "20.99b", "-c", "0"],
1426 self.assertEqual(result.exit_code, 1)
1427 self.assertIn("required version", result.stderr)
1429 def test_preserves_line_endings(self) -> None:
1430 with TemporaryDirectory() as workspace:
1431 test_file = Path(workspace) / "test.py"
1432 for nl in ["\n", "\r\n"]:
1433 contents = nl.join(["def f( ):", " pass"])
1434 test_file.write_bytes(contents.encode())
1435 ff(test_file, write_back=black.WriteBack.YES)
1436 updated_contents: bytes = test_file.read_bytes()
1437 self.assertIn(nl.encode(), updated_contents)
1439 self.assertNotIn(b"\r\n", updated_contents)
1441 def test_preserves_line_endings_via_stdin(self) -> None:
1442 for nl in ["\n", "\r\n"]:
1443 contents = nl.join(["def f( ):", " pass"])
1444 runner = BlackRunner()
1445 result = runner.invoke(
1446 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1448 self.assertEqual(result.exit_code, 0)
1449 output = result.stdout_bytes
1450 self.assertIn(nl.encode("utf8"), output)
1452 self.assertNotIn(b"\r\n", output)
1454 def test_normalize_line_endings(self) -> None:
1455 with TemporaryDirectory() as workspace:
1456 test_file = Path(workspace) / "test.py"
1457 for data, expected in (
1458 (b"c\r\nc\n ", b"c\r\nc\r\n"),
1459 (b"l\nl\r\n ", b"l\nl\n"),
1461 test_file.write_bytes(data)
1462 ff(test_file, write_back=black.WriteBack.YES)
1463 self.assertEqual(test_file.read_bytes(), expected)
1465 def test_assert_equivalent_different_asts(self) -> None:
1466 with self.assertRaises(AssertionError):
1467 black.assert_equivalent("{}", "None")
1469 def test_shhh_click(self) -> None:
1471 from click import _unicodefun # type: ignore
1473 self.skipTest("Incompatible Click version")
1475 if not hasattr(_unicodefun, "_verify_python_env"):
1476 self.skipTest("Incompatible Click version")
1478 # First, let's see if Click is crashing with a preferred ASCII charset.
1479 with patch("locale.getpreferredencoding") as gpe:
1480 gpe.return_value = "ASCII"
1481 with self.assertRaises(RuntimeError):
1482 _unicodefun._verify_python_env()
1483 # Now, let's silence Click...
1485 # ...and confirm it's silent.
1486 with patch("locale.getpreferredencoding") as gpe:
1487 gpe.return_value = "ASCII"
1489 _unicodefun._verify_python_env()
1490 except RuntimeError as re:
1491 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1493 def test_root_logger_not_used_directly(self) -> None:
1494 def fail(*args: Any, **kwargs: Any) -> None:
1495 self.fail("Record created with root logger")
1497 with patch.multiple(
1506 ff(THIS_DIR / "util.py")
1508 def test_invalid_config_return_code(self) -> None:
1509 tmp_file = Path(black.dump_to_file())
1511 tmp_config = Path(black.dump_to_file())
1513 args = ["--config", str(tmp_config), str(tmp_file)]
1514 self.invokeBlack(args, exit_code=2, ignore_config=False)
1518 def test_parse_pyproject_toml(self) -> None:
1519 test_toml_file = THIS_DIR / "test.toml"
1520 config = black.parse_pyproject_toml(str(test_toml_file))
1521 self.assertEqual(config["verbose"], 1)
1522 self.assertEqual(config["check"], "no")
1523 self.assertEqual(config["diff"], "y")
1524 self.assertEqual(config["color"], True)
1525 self.assertEqual(config["line_length"], 79)
1526 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1527 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1528 self.assertEqual(config["exclude"], r"\.pyi?$")
1529 self.assertEqual(config["include"], r"\.py?$")
1531 def test_parse_pyproject_toml_project_metadata(self) -> None:
1532 for test_toml, expected in [
1533 ("only_black_pyproject.toml", ["py310"]),
1534 ("only_metadata_pyproject.toml", ["py37", "py38", "py39", "py310"]),
1535 ("neither_pyproject.toml", None),
1536 ("both_pyproject.toml", ["py310"]),
1538 test_toml_file = THIS_DIR / "data" / "project_metadata" / test_toml
1539 config = black.parse_pyproject_toml(str(test_toml_file))
1540 self.assertEqual(config.get("target_version"), expected)
1542 def test_infer_target_version(self) -> None:
1543 for version, expected in [
1544 ("3.6", [TargetVersion.PY36]),
1545 ("3.11.0rc1", [TargetVersion.PY311]),
1546 (">=3.10", [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312]),
1549 [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
1551 ("<3.6", [TargetVersion.PY33, TargetVersion.PY34, TargetVersion.PY35]),
1552 (">3.7,<3.10", [TargetVersion.PY38, TargetVersion.PY39]),
1555 [TargetVersion.PY310, TargetVersion.PY311, TargetVersion.PY312],
1558 "> 3.9.4, != 3.10.3",
1561 TargetVersion.PY310,
1562 TargetVersion.PY311,
1563 TargetVersion.PY312,
1574 TargetVersion.PY310,
1575 TargetVersion.PY311,
1576 TargetVersion.PY312,
1589 TargetVersion.PY310,
1590 TargetVersion.PY311,
1591 TargetVersion.PY312,
1594 ("==3.8.*", [TargetVersion.PY38]),
1598 ("==invalid", None),
1599 (">3.9,!=invalid", None),
1604 (">3.10,<3.11", None),
1606 test_toml = {"project": {"requires-python": version}}
1607 result = black.files.infer_target_version(test_toml)
1608 self.assertEqual(result, expected)
1610 def test_read_pyproject_toml(self) -> None:
1611 test_toml_file = THIS_DIR / "test.toml"
1612 fake_ctx = FakeContext()
1613 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1614 config = fake_ctx.default_map
1615 self.assertEqual(config["verbose"], "1")
1616 self.assertEqual(config["check"], "no")
1617 self.assertEqual(config["diff"], "y")
1618 self.assertEqual(config["color"], "True")
1619 self.assertEqual(config["line_length"], "79")
1620 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1621 self.assertEqual(config["exclude"], r"\.pyi?$")
1622 self.assertEqual(config["include"], r"\.py?$")
1624 def test_read_pyproject_toml_from_stdin(self) -> None:
1625 with TemporaryDirectory() as workspace:
1626 root = Path(workspace)
1628 src_dir = root / "src"
1631 src_pyproject = src_dir / "pyproject.toml"
1632 src_pyproject.touch()
1634 test_toml_file = THIS_DIR / "test.toml"
1635 src_pyproject.write_text(test_toml_file.read_text())
1637 src_python = src_dir / "foo.py"
1640 fake_ctx = FakeContext()
1641 fake_ctx.params["src"] = ("-",)
1642 fake_ctx.params["stdin_filename"] = str(src_python)
1644 with change_directory(root):
1645 black.read_pyproject_toml(fake_ctx, FakeParameter(), None)
1647 config = fake_ctx.default_map
1648 self.assertEqual(config["verbose"], "1")
1649 self.assertEqual(config["check"], "no")
1650 self.assertEqual(config["diff"], "y")
1651 self.assertEqual(config["color"], "True")
1652 self.assertEqual(config["line_length"], "79")
1653 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1654 self.assertEqual(config["exclude"], r"\.pyi?$")
1655 self.assertEqual(config["include"], r"\.py?$")
1657 @pytest.mark.incompatible_with_mypyc
1658 def test_find_project_root(self) -> None:
1659 with TemporaryDirectory() as workspace:
1660 root = Path(workspace)
1661 test_dir = root / "test"
1664 src_dir = root / "src"
1667 root_pyproject = root / "pyproject.toml"
1668 root_pyproject.touch()
1669 src_pyproject = src_dir / "pyproject.toml"
1670 src_pyproject.touch()
1671 src_python = src_dir / "foo.py"
1675 black.find_project_root((src_dir, test_dir)),
1676 (root.resolve(), "pyproject.toml"),
1679 black.find_project_root((src_dir,)),
1680 (src_dir.resolve(), "pyproject.toml"),
1683 black.find_project_root((src_python,)),
1684 (src_dir.resolve(), "pyproject.toml"),
1687 with change_directory(test_dir):
1689 black.find_project_root(("-",), stdin_filename="../src/a.py"),
1690 (src_dir.resolve(), "pyproject.toml"),
1694 "black.files.find_user_pyproject_toml",
1696 def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
1697 find_user_pyproject_toml.side_effect = RuntimeError()
1699 with redirect_stderr(io.StringIO()) as stderr:
1700 result = black.files.find_pyproject_toml(
1701 path_search_start=(str(Path.cwd().root),)
1704 assert result is None
1705 err = stderr.getvalue()
1706 assert "Ignoring user configuration" in err
1709 "black.files.find_user_pyproject_toml",
1710 black.files.find_user_pyproject_toml.__wrapped__,
1712 def test_find_user_pyproject_toml_linux(self) -> None:
1713 if system() == "Windows":
1716 # Test if XDG_CONFIG_HOME is checked
1717 with TemporaryDirectory() as workspace:
1718 tmp_user_config = Path(workspace) / "black"
1719 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1721 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1724 # Test fallback for XDG_CONFIG_HOME
1725 with patch.dict("os.environ"):
1726 os.environ.pop("XDG_CONFIG_HOME", None)
1727 fallback_user_config = Path("~/.config").expanduser() / "black"
1729 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1732 def test_find_user_pyproject_toml_windows(self) -> None:
1733 if system() != "Windows":
1736 user_config_path = Path.home() / ".black"
1738 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1741 def test_bpo_33660_workaround(self) -> None:
1742 if system() == "Windows":
1745 # https://bugs.python.org/issue33660
1747 with change_directory(root):
1748 path = Path("workspace") / "project"
1749 report = black.Report(verbose=True)
1750 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1751 self.assertEqual(normalized_path, "workspace/project")
1753 def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
1754 if system() != "Windows":
1757 with TemporaryDirectory() as workspace:
1758 root = Path(workspace)
1759 junction_dir = root / "junction"
1760 junction_target_outside_of_root = root / ".."
1761 os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
1763 report = black.Report(verbose=True)
1764 normalized_path = black.normalize_path_maybe_ignore(
1765 junction_dir, root, report
1767 # Manually delete for Python < 3.8
1768 os.system(f"rmdir {junction_dir}")
1770 self.assertEqual(normalized_path, None)
1772 def test_newline_comment_interaction(self) -> None:
1773 source = "class A:\\\r\n# type: ignore\n pass\n"
1774 output = black.format_str(source, mode=DEFAULT_MODE)
1775 black.assert_stable(source, output, mode=DEFAULT_MODE)
1777 def test_bpo_2142_workaround(self) -> None:
1778 # https://bugs.python.org/issue2142
1780 source, _ = read_data("miscellaneous", "missing_final_newline")
1781 # read_data adds a trailing newline
1782 source = source.rstrip()
1783 expected, _ = read_data("miscellaneous", "missing_final_newline.diff")
1784 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1785 diff_header = re.compile(
1786 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1787 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
1790 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1791 self.assertEqual(result.exit_code, 0)
1794 actual = result.output
1795 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1796 self.assertEqual(actual, expected)
1799 def compare_results(
1800 result: click.testing.Result, expected_value: str, expected_exit_code: int
1802 """Helper method to test the value and exit code of a click Result."""
1804 result.output == expected_value
1805 ), "The output did not match the expected value."
1806 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1808 def test_code_option(self) -> None:
1809 """Test the code option with no changes."""
1810 code = 'print("Hello world")\n'
1811 args = ["--code", code]
1812 result = CliRunner().invoke(black.main, args)
1814 self.compare_results(result, code, 0)
1816 def test_code_option_changed(self) -> None:
1817 """Test the code option when changes are required."""
1818 code = "print('hello world')"
1819 formatted = black.format_str(code, mode=DEFAULT_MODE)
1821 args = ["--code", code]
1822 result = CliRunner().invoke(black.main, args)
1824 self.compare_results(result, formatted, 0)
1826 def test_code_option_check(self) -> None:
1827 """Test the code option when check is passed."""
1828 args = ["--check", "--code", 'print("Hello world")\n']
1829 result = CliRunner().invoke(black.main, args)
1830 self.compare_results(result, "", 0)
1832 def test_code_option_check_changed(self) -> None:
1833 """Test the code option when changes are required, and check is passed."""
1834 args = ["--check", "--code", "print('hello world')"]
1835 result = CliRunner().invoke(black.main, args)
1836 self.compare_results(result, "", 1)
1838 def test_code_option_diff(self) -> None:
1839 """Test the code option when diff is passed."""
1840 code = "print('hello world')"
1841 formatted = black.format_str(code, mode=DEFAULT_MODE)
1842 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1844 args = ["--diff", "--code", code]
1845 result = CliRunner().invoke(black.main, args)
1847 # Remove time from diff
1848 output = DIFF_TIME.sub("", result.output)
1850 assert output == result_diff, "The output did not match the expected value."
1851 assert result.exit_code == 0, "The exit code is incorrect."
1853 def test_code_option_color_diff(self) -> None:
1854 """Test the code option when color and diff are passed."""
1855 code = "print('hello world')"
1856 formatted = black.format_str(code, mode=DEFAULT_MODE)
1858 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1859 result_diff = color_diff(result_diff)
1861 args = ["--diff", "--color", "--code", code]
1862 result = CliRunner().invoke(black.main, args)
1864 # Remove time from diff
1865 output = DIFF_TIME.sub("", result.output)
1867 assert output == result_diff, "The output did not match the expected value."
1868 assert result.exit_code == 0, "The exit code is incorrect."
1870 @pytest.mark.incompatible_with_mypyc
1871 def test_code_option_safe(self) -> None:
1872 """Test that the code option throws an error when the sanity checks fail."""
1873 # Patch black.assert_equivalent to ensure the sanity checks fail
1874 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1875 code = 'print("Hello world")'
1876 error_msg = f"{code}\nerror: cannot format <string>: \n"
1878 args = ["--safe", "--code", code]
1879 result = CliRunner().invoke(black.main, args)
1881 self.compare_results(result, error_msg, 123)
1883 def test_code_option_fast(self) -> None:
1884 """Test that the code option ignores errors when the sanity checks fail."""
1885 # Patch black.assert_equivalent to ensure the sanity checks fail
1886 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1887 code = 'print("Hello world")'
1888 formatted = black.format_str(code, mode=DEFAULT_MODE)
1890 args = ["--fast", "--code", code]
1891 result = CliRunner().invoke(black.main, args)
1893 self.compare_results(result, formatted, 0)
1895 @pytest.mark.incompatible_with_mypyc
1896 def test_code_option_config(self) -> None:
1898 Test that the code option finds the pyproject.toml in the current directory.
1900 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1901 args = ["--code", "print"]
1902 # This is the only directory known to contain a pyproject.toml
1903 with change_directory(PROJECT_ROOT):
1904 CliRunner().invoke(black.main, args)
1905 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1908 len(parse.mock_calls) >= 1
1909 ), "Expected config parse to be called with the current directory."
1911 _, call_args, _ = parse.mock_calls[0]
1913 call_args[0].lower() == str(pyproject_path).lower()
1914 ), "Incorrect config loaded."
1916 @pytest.mark.incompatible_with_mypyc
1917 def test_code_option_parent_config(self) -> None:
1919 Test that the code option finds the pyproject.toml in the parent directory.
1921 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1922 with change_directory(THIS_DIR):
1923 args = ["--code", "print"]
1924 CliRunner().invoke(black.main, args)
1926 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1928 len(parse.mock_calls) >= 1
1929 ), "Expected config parse to be called with the current directory."
1931 _, call_args, _ = parse.mock_calls[0]
1933 call_args[0].lower() == str(pyproject_path).lower()
1934 ), "Incorrect config loaded."
1936 def test_for_handled_unexpected_eof_error(self) -> None:
1938 Test that an unexpected EOF SyntaxError is nicely presented.
1940 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1941 black.lib2to3_parse("print(", {})
1943 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1945 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1946 with pytest.raises(AssertionError) as err:
1947 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1950 # Unfortunately the SyntaxError message has changed in newer versions so we
1951 # can't match it directly.
1952 err.match("invalid character")
1953 err.match(r"\(<unknown>, line 1\)")
1957 def test_get_cache_dir(
1960 monkeypatch: pytest.MonkeyPatch,
1962 # Create multiple cache directories
1963 workspace1 = tmp_path / "ws1"
1965 workspace2 = tmp_path / "ws2"
1968 # Force user_cache_dir to use the temporary directory for easier assertions
1969 patch_user_cache_dir = patch(
1970 target="black.cache.user_cache_dir",
1972 return_value=str(workspace1),
1975 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1976 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1977 with patch_user_cache_dir:
1978 assert get_cache_dir() == workspace1
1980 # If it is set, use the path provided in the env var.
1981 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1982 assert get_cache_dir() == workspace2
1984 def test_cache_broken_file(self) -> None:
1986 with cache_dir() as workspace:
1987 cache_file = get_cache_file(mode)
1988 cache_file.write_text("this is not a pickle")
1989 assert black.read_cache(mode) == {}
1990 src = (workspace / "test.py").resolve()
1991 src.write_text("print('hello')")
1992 invokeBlack([str(src)])
1993 cache = black.read_cache(mode)
1994 assert str(src) in cache
1996 def test_cache_single_file_already_cached(self) -> None:
1998 with cache_dir() as workspace:
1999 src = (workspace / "test.py").resolve()
2000 src.write_text("print('hello')")
2001 black.write_cache({}, [src], mode)
2002 invokeBlack([str(src)])
2003 assert src.read_text() == "print('hello')"
2006 def test_cache_multiple_files(self) -> None:
2008 with cache_dir() as workspace, patch(
2009 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
2011 one = (workspace / "one.py").resolve()
2012 with one.open("w") as fobj:
2013 fobj.write("print('hello')")
2014 two = (workspace / "two.py").resolve()
2015 with two.open("w") as fobj:
2016 fobj.write("print('hello')")
2017 black.write_cache({}, [one], mode)
2018 invokeBlack([str(workspace)])
2019 with one.open("r") as fobj:
2020 assert fobj.read() == "print('hello')"
2021 with two.open("r") as fobj:
2022 assert fobj.read() == 'print("hello")\n'
2023 cache = black.read_cache(mode)
2024 assert str(one) in cache
2025 assert str(two) in cache
2027 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
2028 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
2030 with cache_dir() as workspace:
2031 src = (workspace / "test.py").resolve()
2032 with src.open("w") as fobj:
2033 fobj.write("print('hello')")
2034 with patch("black.read_cache") as read_cache, patch(
2037 cmd = [str(src), "--diff"]
2039 cmd.append("--color")
2041 cache_file = get_cache_file(mode)
2042 assert cache_file.exists() is False
2043 write_cache.assert_not_called()
2044 read_cache.assert_not_called()
2046 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
2048 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
2049 with cache_dir() as workspace:
2050 for tag in range(0, 4):
2051 src = (workspace / f"test{tag}.py").resolve()
2052 with src.open("w") as fobj:
2053 fobj.write("print('hello')")
2055 "black.concurrency.Manager", wraps=multiprocessing.Manager
2057 cmd = ["--diff", str(workspace)]
2059 cmd.append("--color")
2060 invokeBlack(cmd, exit_code=0)
2061 # this isn't quite doing what we want, but if it _isn't_
2062 # called then we cannot be using the lock it provides
2065 def test_no_cache_when_stdin(self) -> None:
2068 result = CliRunner().invoke(
2069 black.main, ["-"], input=BytesIO(b"print('hello')")
2071 assert not result.exit_code
2072 cache_file = get_cache_file(mode)
2073 assert not cache_file.exists()
2075 def test_read_cache_no_cachefile(self) -> None:
2078 assert black.read_cache(mode) == {}
2080 def test_write_cache_read_cache(self) -> None:
2082 with cache_dir() as workspace:
2083 src = (workspace / "test.py").resolve()
2085 black.write_cache({}, [src], mode)
2086 cache = black.read_cache(mode)
2087 assert str(src) in cache
2088 assert cache[str(src)] == black.get_cache_info(src)
2090 def test_filter_cached(self) -> None:
2091 with TemporaryDirectory() as workspace:
2092 path = Path(workspace)
2093 uncached = (path / "uncached").resolve()
2094 cached = (path / "cached").resolve()
2095 cached_but_changed = (path / "changed").resolve()
2098 cached_but_changed.touch()
2100 str(cached): black.get_cache_info(cached),
2101 str(cached_but_changed): (0.0, 0),
2103 todo, done = black.cache.filter_cached(
2104 cache, {uncached, cached, cached_but_changed}
2106 assert todo == {uncached, cached_but_changed}
2107 assert done == {cached}
2109 def test_write_cache_creates_directory_if_needed(self) -> None:
2111 with cache_dir(exists=False) as workspace:
2112 assert not workspace.exists()
2113 black.write_cache({}, [], mode)
2114 assert workspace.exists()
2117 def test_failed_formatting_does_not_get_cached(self) -> None:
2119 with cache_dir() as workspace, patch(
2120 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
2122 failing = (workspace / "failing.py").resolve()
2123 with failing.open("w") as fobj:
2124 fobj.write("not actually python")
2125 clean = (workspace / "clean.py").resolve()
2126 with clean.open("w") as fobj:
2127 fobj.write('print("hello")\n')
2128 invokeBlack([str(workspace)], exit_code=123)
2129 cache = black.read_cache(mode)
2130 assert str(failing) not in cache
2131 assert str(clean) in cache
2133 def test_write_cache_write_fail(self) -> None:
2135 with cache_dir(), patch.object(Path, "open") as mock:
2136 mock.side_effect = OSError
2137 black.write_cache({}, [], mode)
2139 def test_read_cache_line_lengths(self) -> None:
2141 short_mode = replace(DEFAULT_MODE, line_length=1)
2142 with cache_dir() as workspace:
2143 path = (workspace / "file.py").resolve()
2145 black.write_cache({}, [path], mode)
2146 one = black.read_cache(mode)
2147 assert str(path) in one
2148 two = black.read_cache(short_mode)
2149 assert str(path) not in two
2152 def assert_collected_sources(
2153 src: Sequence[Union[str, Path]],
2154 expected: Sequence[Union[str, Path]],
2156 ctx: Optional[FakeContext] = None,
2157 exclude: Optional[str] = None,
2158 include: Optional[str] = None,
2159 extend_exclude: Optional[str] = None,
2160 force_exclude: Optional[str] = None,
2161 stdin_filename: Optional[str] = None,
2163 gs_src = tuple(str(Path(s)) for s in src)
2164 gs_expected = [Path(s) for s in expected]
2165 gs_exclude = None if exclude is None else compile_pattern(exclude)
2166 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
2167 gs_extend_exclude = (
2168 None if extend_exclude is None else compile_pattern(extend_exclude)
2170 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
2171 collected = black.get_sources(
2172 ctx=ctx or FakeContext(),
2178 extend_exclude=gs_extend_exclude,
2179 force_exclude=gs_force_exclude,
2180 report=black.Report(),
2181 stdin_filename=stdin_filename,
2183 assert sorted(collected) == sorted(gs_expected)
2186 class TestFileCollection:
2187 def test_include_exclude(self) -> None:
2188 path = THIS_DIR / "data" / "include_exclude_tests"
2191 Path(path / "b/dont_exclude/a.py"),
2192 Path(path / "b/dont_exclude/a.pyi"),
2194 assert_collected_sources(
2198 exclude=r"/exclude/|/\.definitely_exclude/",
2201 def test_gitignore_used_as_default(self) -> None:
2202 base = Path(DATA_DIR / "include_exclude_tests")
2204 base / "b/.definitely_exclude/a.py",
2205 base / "b/.definitely_exclude/a.pyi",
2209 ctx.obj["root"] = base
2210 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
2212 def test_gitignore_used_on_multiple_sources(self) -> None:
2213 root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
2215 root / "dir1" / "b.py",
2216 root / "dir2" / "b.py",
2219 ctx.obj["root"] = root
2220 src = [root / "dir1", root / "dir2"]
2221 assert_collected_sources(src, expected, ctx=ctx)
2223 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2224 def test_exclude_for_issue_1572(self) -> None:
2225 # Exclude shouldn't touch files that were explicitly given to Black through the
2226 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
2227 # https://github.com/psf/black/issues/1572
2228 path = DATA_DIR / "include_exclude_tests"
2229 src = [path / "b/exclude/a.py"]
2230 expected = [path / "b/exclude/a.py"]
2231 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2233 def test_gitignore_exclude(self) -> None:
2234 path = THIS_DIR / "data" / "include_exclude_tests"
2235 include = re.compile(r"\.pyi?$")
2236 exclude = re.compile(r"")
2237 report = black.Report()
2238 gitignore = PathSpec.from_lines(
2239 "gitwildmatch", ["exclude/", ".definitely_exclude"]
2241 sources: List[Path] = []
2243 Path(path / "b/dont_exclude/a.py"),
2244 Path(path / "b/dont_exclude/a.pyi"),
2246 this_abs = THIS_DIR.resolve()
2248 black.gen_python_files(
2261 assert sorted(expected) == sorted(sources)
2263 def test_nested_gitignore(self) -> None:
2264 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
2265 include = re.compile(r"\.pyi?$")
2266 exclude = re.compile(r"")
2267 root_gitignore = black.files.get_gitignore(path)
2268 report = black.Report()
2269 expected: List[Path] = [
2270 Path(path / "x.py"),
2271 Path(path / "root/b.py"),
2272 Path(path / "root/c.py"),
2273 Path(path / "root/child/c.py"),
2275 this_abs = THIS_DIR.resolve()
2277 black.gen_python_files(
2285 {path: root_gitignore},
2290 assert sorted(expected) == sorted(sources)
2292 def test_nested_gitignore_directly_in_source_directory(self) -> None:
2293 # https://github.com/psf/black/issues/2598
2294 path = Path(DATA_DIR / "nested_gitignore_tests")
2295 src = Path(path / "root" / "child")
2296 expected = [src / "a.py", src / "c.py"]
2297 assert_collected_sources([src], expected)
2299 def test_invalid_gitignore(self) -> None:
2300 path = THIS_DIR / "data" / "invalid_gitignore_tests"
2301 empty_config = path / "pyproject.toml"
2302 result = BlackRunner().invoke(
2303 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2305 assert result.exit_code == 1
2306 assert result.stderr_bytes is not None
2308 gitignore = path / ".gitignore"
2309 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2311 def test_invalid_nested_gitignore(self) -> None:
2312 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
2313 empty_config = path / "pyproject.toml"
2314 result = BlackRunner().invoke(
2315 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2317 assert result.exit_code == 1
2318 assert result.stderr_bytes is not None
2320 gitignore = path / "a" / ".gitignore"
2321 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2323 def test_gitignore_that_ignores_subfolders(self) -> None:
2324 # If gitignore with */* is in root
2325 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir")
2326 expected = [root / "b.py"]
2328 ctx.obj["root"] = root
2329 assert_collected_sources([root], expected, ctx=ctx)
2331 # If .gitignore with */* is nested
2332 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2335 root / "subdir" / "b.py",
2338 ctx.obj["root"] = root
2339 assert_collected_sources([root], expected, ctx=ctx)
2341 # If command is executed from outer dir
2342 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2343 target = root / "subdir"
2344 expected = [target / "b.py"]
2346 ctx.obj["root"] = root
2347 assert_collected_sources([target], expected, ctx=ctx)
2349 def test_empty_include(self) -> None:
2350 path = DATA_DIR / "include_exclude_tests"
2353 Path(path / "b/exclude/a.pie"),
2354 Path(path / "b/exclude/a.py"),
2355 Path(path / "b/exclude/a.pyi"),
2356 Path(path / "b/dont_exclude/a.pie"),
2357 Path(path / "b/dont_exclude/a.py"),
2358 Path(path / "b/dont_exclude/a.pyi"),
2359 Path(path / "b/.definitely_exclude/a.pie"),
2360 Path(path / "b/.definitely_exclude/a.py"),
2361 Path(path / "b/.definitely_exclude/a.pyi"),
2362 Path(path / ".gitignore"),
2363 Path(path / "pyproject.toml"),
2365 # Setting exclude explicitly to an empty string to block .gitignore usage.
2366 assert_collected_sources(src, expected, include="", exclude="")
2368 def test_extend_exclude(self) -> None:
2369 path = DATA_DIR / "include_exclude_tests"
2372 Path(path / "b/exclude/a.py"),
2373 Path(path / "b/dont_exclude/a.py"),
2375 assert_collected_sources(
2376 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
2379 @pytest.mark.incompatible_with_mypyc
2380 def test_symlink_out_of_root_directory(self) -> None:
2382 root = THIS_DIR.resolve()
2384 include = re.compile(black.DEFAULT_INCLUDES)
2385 exclude = re.compile(black.DEFAULT_EXCLUDES)
2386 report = black.Report()
2387 gitignore = PathSpec.from_lines("gitwildmatch", [])
2388 # `child` should behave like a symlink which resolved path is clearly
2389 # outside of the `root` directory.
2390 path.iterdir.return_value = [child]
2391 child.resolve.return_value = Path("/a/b/c")
2392 child.as_posix.return_value = "/a/b/c"
2395 black.gen_python_files(
2408 except ValueError as ve:
2409 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
2410 path.iterdir.assert_called_once()
2411 child.resolve.assert_called_once()
2413 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2414 def test_get_sources_with_stdin(self) -> None:
2417 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2419 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2420 def test_get_sources_with_stdin_filename(self) -> None:
2422 stdin_filename = str(THIS_DIR / "data/collections.py")
2423 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2424 assert_collected_sources(
2427 exclude=r"/exclude/a\.py",
2428 stdin_filename=stdin_filename,
2431 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2432 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2433 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2434 # file being passed directly. This is the same as
2435 # test_exclude_for_issue_1572
2436 path = DATA_DIR / "include_exclude_tests"
2438 stdin_filename = str(path / "b/exclude/a.py")
2439 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2440 assert_collected_sources(
2443 exclude=r"/exclude/|a\.py",
2444 stdin_filename=stdin_filename,
2447 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2448 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2449 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2450 # file being passed directly. This is the same as
2451 # test_exclude_for_issue_1572
2453 path = THIS_DIR / "data" / "include_exclude_tests"
2454 stdin_filename = str(path / "b/exclude/a.py")
2455 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2456 assert_collected_sources(
2459 extend_exclude=r"/exclude/|a\.py",
2460 stdin_filename=stdin_filename,
2463 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2464 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2465 # Force exclude should exclude the file when passing it through
2467 path = THIS_DIR / "data" / "include_exclude_tests"
2468 stdin_filename = str(path / "b/exclude/a.py")
2469 assert_collected_sources(
2472 force_exclude=r"/exclude/|a\.py",
2473 stdin_filename=stdin_filename,
2478 with open(black.__file__, "r", encoding="utf-8") as _bf:
2479 black_source_lines = _bf.readlines()
2480 except UnicodeDecodeError:
2481 if not black.COMPILED:
2486 frame: types.FrameType, event: str, arg: Any
2487 ) -> Callable[[types.FrameType, str, Any], Any]:
2488 """Show function calls `from black/__init__.py` as they happen.
2490 Register this with `sys.settrace()` in a test you're debugging.
2495 stack = len(inspect.stack()) - 19
2497 filename = frame.f_code.co_filename
2498 lineno = frame.f_lineno
2499 func_sig_lineno = lineno - 1
2500 funcname = black_source_lines[func_sig_lineno].strip()
2501 while funcname.startswith("@"):
2502 func_sig_lineno += 1
2503 funcname = black_source_lines[func_sig_lineno].strip()
2504 if "black/__init__.py" in filename:
2505 print(f"{' ' * stack}{lineno}:{funcname}")