All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
12 from concurrent.futures import ThreadPoolExecutor
13 from contextlib import contextmanager
14 from dataclasses import replace
15 from io import BytesIO
16 from pathlib import Path
17 from platform import system
18 from tempfile import TemporaryDirectory
30 from unittest.mock import MagicMock, patch
35 from click import unstyle
36 from click.testing import CliRunner
37 from pathspec import PathSpec
41 from black import Feature, TargetVersion
42 from black import re_compile_maybe_verbose as compile_pattern
43 from black.cache import get_cache_file
44 from black.debug import DebugVisitor
45 from black.output import color_diff, diff
46 from black.report import Report
48 # Import other test classes
49 from tests.util import (
64 THIS_FILE = Path(__file__)
65 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
66 DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
67 DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
71 # Match the time output in a diff, but nothing else
72 DIFF_TIME = re.compile(r"\t[\d-:+\. ]+")
76 def cache_dir(exists: bool = True) -> Iterator[Path]:
77 with TemporaryDirectory() as workspace:
78 cache_dir = Path(workspace)
80 cache_dir = cache_dir / "new"
81 with patch("black.cache.CACHE_DIR", cache_dir):
86 def event_loop() -> Iterator[None]:
87 policy = asyncio.get_event_loop_policy()
88 loop = policy.new_event_loop()
89 asyncio.set_event_loop(loop)
97 class FakeContext(click.Context):
98 """A fake click Context for when calling functions that need it."""
100 def __init__(self) -> None:
101 self.default_map: Dict[str, Any] = {}
104 class FakeParameter(click.Parameter):
105 """A fake click Parameter for when calling functions that need it."""
107 def __init__(self) -> None:
111 class BlackRunner(CliRunner):
112 """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
114 def __init__(self) -> None:
115 super().__init__(mix_stderr=False)
119 args: List[str], exit_code: int = 0, ignore_config: bool = True
121 runner = BlackRunner()
123 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
124 result = runner.invoke(black.main, args)
125 assert result.stdout_bytes is not None
126 assert result.stderr_bytes is not None
128 f"Failed with args: {args}\n"
129 f"stdout: {result.stdout_bytes.decode()!r}\n"
130 f"stderr: {result.stderr_bytes.decode()!r}\n"
131 f"exception: {result.exception}"
133 assert result.exit_code == exit_code, msg
136 class BlackTestCase(BlackBaseTestCase):
137 invokeBlack = staticmethod(invokeBlack)
139 def test_empty_ff(self) -> None:
141 tmp_file = Path(black.dump_to_file())
143 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
144 with open(tmp_file, encoding="utf8") as f:
148 self.assertFormatEqual(expected, actual)
150 def test_piping(self) -> None:
151 source, expected = read_data("src/black/__init__", data=False)
152 result = BlackRunner().invoke(
154 ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
155 input=BytesIO(source.encode("utf8")),
157 self.assertEqual(result.exit_code, 0)
158 self.assertFormatEqual(expected, result.output)
159 if source != result.output:
160 black.assert_equivalent(source, result.output)
161 black.assert_stable(source, result.output, DEFAULT_MODE)
163 def test_piping_diff(self) -> None:
164 diff_header = re.compile(
165 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
168 source, _ = read_data("expression.py")
169 expected, _ = read_data("expression.diff")
170 config = THIS_DIR / "data" / "empty_pyproject.toml"
174 f"--line-length={black.DEFAULT_LINE_LENGTH}",
176 f"--config={config}",
178 result = BlackRunner().invoke(
179 black.main, args, input=BytesIO(source.encode("utf8"))
181 self.assertEqual(result.exit_code, 0)
182 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
183 actual = actual.rstrip() + "\n" # the diff output has a trailing space
184 self.assertEqual(expected, actual)
186 def test_piping_diff_with_color(self) -> None:
187 source, _ = read_data("expression.py")
188 config = THIS_DIR / "data" / "empty_pyproject.toml"
192 f"--line-length={black.DEFAULT_LINE_LENGTH}",
195 f"--config={config}",
197 result = BlackRunner().invoke(
198 black.main, args, input=BytesIO(source.encode("utf8"))
200 actual = result.output
201 # Again, the contents are checked in a different test, so only look for colors.
202 self.assertIn("\033[1;37m", actual)
203 self.assertIn("\033[36m", actual)
204 self.assertIn("\033[32m", actual)
205 self.assertIn("\033[31m", actual)
206 self.assertIn("\033[0m", actual)
208 @patch("black.dump_to_file", dump_to_stderr)
209 def _test_wip(self) -> None:
210 source, expected = read_data("wip")
211 sys.settrace(tracefunc)
214 experimental_string_processing=False,
215 target_versions={black.TargetVersion.PY38},
217 actual = fs(source, mode=mode)
219 self.assertFormatEqual(expected, actual)
220 black.assert_equivalent(source, actual)
221 black.assert_stable(source, actual, black.FileMode())
223 @unittest.expectedFailure
224 @patch("black.dump_to_file", dump_to_stderr)
225 def test_trailing_comma_optional_parens_stability1(self) -> None:
226 source, _expected = read_data("trailing_comma_optional_parens1")
228 black.assert_stable(source, actual, DEFAULT_MODE)
230 @unittest.expectedFailure
231 @patch("black.dump_to_file", dump_to_stderr)
232 def test_trailing_comma_optional_parens_stability2(self) -> None:
233 source, _expected = read_data("trailing_comma_optional_parens2")
235 black.assert_stable(source, actual, DEFAULT_MODE)
237 @unittest.expectedFailure
238 @patch("black.dump_to_file", dump_to_stderr)
239 def test_trailing_comma_optional_parens_stability3(self) -> None:
240 source, _expected = read_data("trailing_comma_optional_parens3")
242 black.assert_stable(source, actual, DEFAULT_MODE)
244 @patch("black.dump_to_file", dump_to_stderr)
245 def test_trailing_comma_optional_parens_stability1_pass2(self) -> None:
246 source, _expected = read_data("trailing_comma_optional_parens1")
247 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
248 black.assert_stable(source, actual, DEFAULT_MODE)
250 @patch("black.dump_to_file", dump_to_stderr)
251 def test_trailing_comma_optional_parens_stability2_pass2(self) -> None:
252 source, _expected = read_data("trailing_comma_optional_parens2")
253 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
254 black.assert_stable(source, actual, DEFAULT_MODE)
256 @patch("black.dump_to_file", dump_to_stderr)
257 def test_trailing_comma_optional_parens_stability3_pass2(self) -> None:
258 source, _expected = read_data("trailing_comma_optional_parens3")
259 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
260 black.assert_stable(source, actual, DEFAULT_MODE)
262 def test_pep_572_version_detection(self) -> None:
263 source, _ = read_data("pep_572")
264 root = black.lib2to3_parse(source)
265 features = black.get_features_used(root)
266 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
267 versions = black.detect_target_versions(root)
268 self.assertIn(black.TargetVersion.PY38, versions)
270 def test_expression_ff(self) -> None:
271 source, expected = read_data("expression")
272 tmp_file = Path(black.dump_to_file(source))
274 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
275 with open(tmp_file, encoding="utf8") as f:
279 self.assertFormatEqual(expected, actual)
280 with patch("black.dump_to_file", dump_to_stderr):
281 black.assert_equivalent(source, actual)
282 black.assert_stable(source, actual, DEFAULT_MODE)
284 def test_expression_diff(self) -> None:
285 source, _ = read_data("expression.py")
286 config = THIS_DIR / "data" / "empty_pyproject.toml"
287 expected, _ = read_data("expression.diff")
288 tmp_file = Path(black.dump_to_file(source))
289 diff_header = re.compile(
290 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
291 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
294 result = BlackRunner().invoke(
295 black.main, ["--diff", str(tmp_file), f"--config={config}"]
297 self.assertEqual(result.exit_code, 0)
300 actual = result.output
301 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
302 if expected != actual:
303 dump = black.dump_to_file(actual)
305 "Expected diff isn't equal to the actual. If you made changes to"
306 " expression.py and this is an anticipated difference, overwrite"
307 f" tests/data/expression.diff with {dump}"
309 self.assertEqual(expected, actual, msg)
311 def test_expression_diff_with_color(self) -> None:
312 source, _ = read_data("expression.py")
313 config = THIS_DIR / "data" / "empty_pyproject.toml"
314 expected, _ = read_data("expression.diff")
315 tmp_file = Path(black.dump_to_file(source))
317 result = BlackRunner().invoke(
318 black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
322 actual = result.output
323 # We check the contents of the diff in `test_expression_diff`. All
324 # we need to check here is that color codes exist in the result.
325 self.assertIn("\033[1;37m", actual)
326 self.assertIn("\033[36m", actual)
327 self.assertIn("\033[32m", actual)
328 self.assertIn("\033[31m", actual)
329 self.assertIn("\033[0m", actual)
331 def test_detect_pos_only_arguments(self) -> None:
332 source, _ = read_data("pep_570")
333 root = black.lib2to3_parse(source)
334 features = black.get_features_used(root)
335 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
336 versions = black.detect_target_versions(root)
337 self.assertIn(black.TargetVersion.PY38, versions)
339 @patch("black.dump_to_file", dump_to_stderr)
340 def test_string_quotes(self) -> None:
341 source, expected = read_data("string_quotes")
342 mode = black.Mode(experimental_string_processing=True)
343 assert_format(source, expected, mode)
344 mode = replace(mode, string_normalization=False)
345 not_normalized = fs(source, mode=mode)
346 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
347 black.assert_equivalent(source, not_normalized)
348 black.assert_stable(source, not_normalized, mode=mode)
350 def test_skip_magic_trailing_comma(self) -> None:
351 source, _ = read_data("expression.py")
352 expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
353 tmp_file = Path(black.dump_to_file(source))
354 diff_header = re.compile(
355 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
356 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
359 result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
360 self.assertEqual(result.exit_code, 0)
363 actual = result.output
364 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
365 actual = actual.rstrip() + "\n" # the diff output has a trailing space
366 if expected != actual:
367 dump = black.dump_to_file(actual)
369 "Expected diff isn't equal to the actual. If you made changes to"
370 " expression.py and this is an anticipated difference, overwrite"
371 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
373 self.assertEqual(expected, actual, msg)
375 @patch("black.dump_to_file", dump_to_stderr)
376 def test_async_as_identifier(self) -> None:
377 source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
378 source, expected = read_data("async_as_identifier")
380 self.assertFormatEqual(expected, actual)
381 major, minor = sys.version_info[:2]
382 if major < 3 or (major <= 3 and minor < 7):
383 black.assert_equivalent(source, actual)
384 black.assert_stable(source, actual, DEFAULT_MODE)
385 # ensure black can parse this when the target is 3.6
386 self.invokeBlack([str(source_path), "--target-version", "py36"])
387 # but not on 3.7, because async/await is no longer an identifier
388 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
390 @patch("black.dump_to_file", dump_to_stderr)
391 def test_python37(self) -> None:
392 source_path = (THIS_DIR / "data" / "python37.py").resolve()
393 source, expected = read_data("python37")
395 self.assertFormatEqual(expected, actual)
396 major, minor = sys.version_info[:2]
397 if major > 3 or (major == 3 and minor >= 7):
398 black.assert_equivalent(source, actual)
399 black.assert_stable(source, actual, DEFAULT_MODE)
400 # ensure black can parse this when the target is 3.7
401 self.invokeBlack([str(source_path), "--target-version", "py37"])
402 # but not on 3.6, because we use async as a reserved keyword
403 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
405 def test_tab_comment_indentation(self) -> None:
406 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
407 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
408 self.assertFormatEqual(contents_spc, fs(contents_spc))
409 self.assertFormatEqual(contents_spc, fs(contents_tab))
411 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
412 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
413 self.assertFormatEqual(contents_spc, fs(contents_spc))
414 self.assertFormatEqual(contents_spc, fs(contents_tab))
416 # mixed tabs and spaces (valid Python 2 code)
417 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
418 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
419 self.assertFormatEqual(contents_spc, fs(contents_spc))
420 self.assertFormatEqual(contents_spc, fs(contents_tab))
422 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
423 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
424 self.assertFormatEqual(contents_spc, fs(contents_spc))
425 self.assertFormatEqual(contents_spc, fs(contents_tab))
427 def test_report_verbose(self) -> None:
428 report = Report(verbose=True)
432 def out(msg: str, **kwargs: Any) -> None:
433 out_lines.append(msg)
435 def err(msg: str, **kwargs: Any) -> None:
436 err_lines.append(msg)
438 with patch("black.output._out", out), patch("black.output._err", err):
439 report.done(Path("f1"), black.Changed.NO)
440 self.assertEqual(len(out_lines), 1)
441 self.assertEqual(len(err_lines), 0)
442 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
443 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
444 self.assertEqual(report.return_code, 0)
445 report.done(Path("f2"), black.Changed.YES)
446 self.assertEqual(len(out_lines), 2)
447 self.assertEqual(len(err_lines), 0)
448 self.assertEqual(out_lines[-1], "reformatted f2")
450 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
452 report.done(Path("f3"), black.Changed.CACHED)
453 self.assertEqual(len(out_lines), 3)
454 self.assertEqual(len(err_lines), 0)
456 out_lines[-1], "f3 wasn't modified on disk since last run."
459 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
461 self.assertEqual(report.return_code, 0)
463 self.assertEqual(report.return_code, 1)
465 report.failed(Path("e1"), "boom")
466 self.assertEqual(len(out_lines), 3)
467 self.assertEqual(len(err_lines), 1)
468 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
470 unstyle(str(report)),
471 "1 file reformatted, 2 files left unchanged, 1 file failed to"
474 self.assertEqual(report.return_code, 123)
475 report.done(Path("f3"), black.Changed.YES)
476 self.assertEqual(len(out_lines), 4)
477 self.assertEqual(len(err_lines), 1)
478 self.assertEqual(out_lines[-1], "reformatted f3")
480 unstyle(str(report)),
481 "2 files reformatted, 2 files left unchanged, 1 file failed to"
484 self.assertEqual(report.return_code, 123)
485 report.failed(Path("e2"), "boom")
486 self.assertEqual(len(out_lines), 4)
487 self.assertEqual(len(err_lines), 2)
488 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
490 unstyle(str(report)),
491 "2 files reformatted, 2 files left unchanged, 2 files failed to"
494 self.assertEqual(report.return_code, 123)
495 report.path_ignored(Path("wat"), "no match")
496 self.assertEqual(len(out_lines), 5)
497 self.assertEqual(len(err_lines), 2)
498 self.assertEqual(out_lines[-1], "wat ignored: no match")
500 unstyle(str(report)),
501 "2 files reformatted, 2 files left unchanged, 2 files failed to"
504 self.assertEqual(report.return_code, 123)
505 report.done(Path("f4"), black.Changed.NO)
506 self.assertEqual(len(out_lines), 6)
507 self.assertEqual(len(err_lines), 2)
508 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
510 unstyle(str(report)),
511 "2 files reformatted, 3 files left unchanged, 2 files failed to"
514 self.assertEqual(report.return_code, 123)
517 unstyle(str(report)),
518 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
519 " would fail to reformat.",
524 unstyle(str(report)),
525 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
526 " would fail to reformat.",
529 def test_report_quiet(self) -> None:
530 report = Report(quiet=True)
534 def out(msg: str, **kwargs: Any) -> None:
535 out_lines.append(msg)
537 def err(msg: str, **kwargs: Any) -> None:
538 err_lines.append(msg)
540 with patch("black.output._out", out), patch("black.output._err", err):
541 report.done(Path("f1"), black.Changed.NO)
542 self.assertEqual(len(out_lines), 0)
543 self.assertEqual(len(err_lines), 0)
544 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
545 self.assertEqual(report.return_code, 0)
546 report.done(Path("f2"), black.Changed.YES)
547 self.assertEqual(len(out_lines), 0)
548 self.assertEqual(len(err_lines), 0)
550 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
552 report.done(Path("f3"), black.Changed.CACHED)
553 self.assertEqual(len(out_lines), 0)
554 self.assertEqual(len(err_lines), 0)
556 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
558 self.assertEqual(report.return_code, 0)
560 self.assertEqual(report.return_code, 1)
562 report.failed(Path("e1"), "boom")
563 self.assertEqual(len(out_lines), 0)
564 self.assertEqual(len(err_lines), 1)
565 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
567 unstyle(str(report)),
568 "1 file reformatted, 2 files left unchanged, 1 file failed to"
571 self.assertEqual(report.return_code, 123)
572 report.done(Path("f3"), black.Changed.YES)
573 self.assertEqual(len(out_lines), 0)
574 self.assertEqual(len(err_lines), 1)
576 unstyle(str(report)),
577 "2 files reformatted, 2 files left unchanged, 1 file failed to"
580 self.assertEqual(report.return_code, 123)
581 report.failed(Path("e2"), "boom")
582 self.assertEqual(len(out_lines), 0)
583 self.assertEqual(len(err_lines), 2)
584 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
586 unstyle(str(report)),
587 "2 files reformatted, 2 files left unchanged, 2 files failed to"
590 self.assertEqual(report.return_code, 123)
591 report.path_ignored(Path("wat"), "no match")
592 self.assertEqual(len(out_lines), 0)
593 self.assertEqual(len(err_lines), 2)
595 unstyle(str(report)),
596 "2 files reformatted, 2 files left unchanged, 2 files failed to"
599 self.assertEqual(report.return_code, 123)
600 report.done(Path("f4"), black.Changed.NO)
601 self.assertEqual(len(out_lines), 0)
602 self.assertEqual(len(err_lines), 2)
604 unstyle(str(report)),
605 "2 files reformatted, 3 files left unchanged, 2 files failed to"
608 self.assertEqual(report.return_code, 123)
611 unstyle(str(report)),
612 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
613 " would fail to reformat.",
618 unstyle(str(report)),
619 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
620 " would fail to reformat.",
623 def test_report_normal(self) -> None:
624 report = black.Report()
628 def out(msg: str, **kwargs: Any) -> None:
629 out_lines.append(msg)
631 def err(msg: str, **kwargs: Any) -> None:
632 err_lines.append(msg)
634 with patch("black.output._out", out), patch("black.output._err", err):
635 report.done(Path("f1"), black.Changed.NO)
636 self.assertEqual(len(out_lines), 0)
637 self.assertEqual(len(err_lines), 0)
638 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
639 self.assertEqual(report.return_code, 0)
640 report.done(Path("f2"), black.Changed.YES)
641 self.assertEqual(len(out_lines), 1)
642 self.assertEqual(len(err_lines), 0)
643 self.assertEqual(out_lines[-1], "reformatted f2")
645 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
647 report.done(Path("f3"), black.Changed.CACHED)
648 self.assertEqual(len(out_lines), 1)
649 self.assertEqual(len(err_lines), 0)
650 self.assertEqual(out_lines[-1], "reformatted f2")
652 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
654 self.assertEqual(report.return_code, 0)
656 self.assertEqual(report.return_code, 1)
658 report.failed(Path("e1"), "boom")
659 self.assertEqual(len(out_lines), 1)
660 self.assertEqual(len(err_lines), 1)
661 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
663 unstyle(str(report)),
664 "1 file reformatted, 2 files left unchanged, 1 file failed to"
667 self.assertEqual(report.return_code, 123)
668 report.done(Path("f3"), black.Changed.YES)
669 self.assertEqual(len(out_lines), 2)
670 self.assertEqual(len(err_lines), 1)
671 self.assertEqual(out_lines[-1], "reformatted f3")
673 unstyle(str(report)),
674 "2 files reformatted, 2 files left unchanged, 1 file failed to"
677 self.assertEqual(report.return_code, 123)
678 report.failed(Path("e2"), "boom")
679 self.assertEqual(len(out_lines), 2)
680 self.assertEqual(len(err_lines), 2)
681 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
683 unstyle(str(report)),
684 "2 files reformatted, 2 files left unchanged, 2 files failed to"
687 self.assertEqual(report.return_code, 123)
688 report.path_ignored(Path("wat"), "no match")
689 self.assertEqual(len(out_lines), 2)
690 self.assertEqual(len(err_lines), 2)
692 unstyle(str(report)),
693 "2 files reformatted, 2 files left unchanged, 2 files failed to"
696 self.assertEqual(report.return_code, 123)
697 report.done(Path("f4"), black.Changed.NO)
698 self.assertEqual(len(out_lines), 2)
699 self.assertEqual(len(err_lines), 2)
701 unstyle(str(report)),
702 "2 files reformatted, 3 files left unchanged, 2 files failed to"
705 self.assertEqual(report.return_code, 123)
708 unstyle(str(report)),
709 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
710 " would fail to reformat.",
715 unstyle(str(report)),
716 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
717 " would fail to reformat.",
720 def test_lib2to3_parse(self) -> None:
721 with self.assertRaises(black.InvalidInput):
722 black.lib2to3_parse("invalid syntax")
725 black.lib2to3_parse(straddling)
726 black.lib2to3_parse(straddling, {TargetVersion.PY27})
727 black.lib2to3_parse(straddling, {TargetVersion.PY36})
728 black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36})
731 black.lib2to3_parse(py2_only)
732 black.lib2to3_parse(py2_only, {TargetVersion.PY27})
733 with self.assertRaises(black.InvalidInput):
734 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
735 with self.assertRaises(black.InvalidInput):
736 black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36})
738 py3_only = "exec(x, end=y)"
739 black.lib2to3_parse(py3_only)
740 with self.assertRaises(black.InvalidInput):
741 black.lib2to3_parse(py3_only, {TargetVersion.PY27})
742 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
743 black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36})
745 def test_get_features_used_decorator(self) -> None:
746 # Test the feature detection of new decorator syntax
747 # since this makes some test cases of test_get_features_used()
748 # fails if it fails, this is tested first so that a useful case
750 simples, relaxed = read_data("decorators")
751 # skip explanation comments at the top of the file
752 for simple_test in simples.split("##")[1:]:
753 node = black.lib2to3_parse(simple_test)
754 decorator = str(node.children[0].children[0]).strip()
756 Feature.RELAXED_DECORATORS,
757 black.get_features_used(node),
759 f"decorator '{decorator}' follows python<=3.8 syntax"
760 "but is detected as 3.9+"
761 # f"The full node is\n{node!r}"
764 # skip the '# output' comment at the top of the output part
765 for relaxed_test in relaxed.split("##")[1:]:
766 node = black.lib2to3_parse(relaxed_test)
767 decorator = str(node.children[0].children[0]).strip()
769 Feature.RELAXED_DECORATORS,
770 black.get_features_used(node),
772 f"decorator '{decorator}' uses python3.9+ syntax"
773 "but is detected as python<=3.8"
774 # f"The full node is\n{node!r}"
778 def test_get_features_used(self) -> None:
779 node = black.lib2to3_parse("def f(*, arg): ...\n")
780 self.assertEqual(black.get_features_used(node), set())
781 node = black.lib2to3_parse("def f(*, arg,): ...\n")
782 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
783 node = black.lib2to3_parse("f(*arg,)\n")
785 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
787 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
788 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
789 node = black.lib2to3_parse("123_456\n")
790 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
791 node = black.lib2to3_parse("123456\n")
792 self.assertEqual(black.get_features_used(node), set())
793 source, expected = read_data("function")
794 node = black.lib2to3_parse(source)
795 expected_features = {
796 Feature.TRAILING_COMMA_IN_CALL,
797 Feature.TRAILING_COMMA_IN_DEF,
800 self.assertEqual(black.get_features_used(node), expected_features)
801 node = black.lib2to3_parse(expected)
802 self.assertEqual(black.get_features_used(node), expected_features)
803 source, expected = read_data("expression")
804 node = black.lib2to3_parse(source)
805 self.assertEqual(black.get_features_used(node), set())
806 node = black.lib2to3_parse(expected)
807 self.assertEqual(black.get_features_used(node), set())
809 def test_get_future_imports(self) -> None:
810 node = black.lib2to3_parse("\n")
811 self.assertEqual(set(), black.get_future_imports(node))
812 node = black.lib2to3_parse("from __future__ import black\n")
813 self.assertEqual({"black"}, black.get_future_imports(node))
814 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
815 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
816 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
817 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
818 node = black.lib2to3_parse(
819 "from __future__ import multiple\nfrom __future__ import imports\n"
821 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
822 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
823 self.assertEqual({"black"}, black.get_future_imports(node))
824 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
825 self.assertEqual({"black"}, black.get_future_imports(node))
826 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
827 self.assertEqual(set(), black.get_future_imports(node))
828 node = black.lib2to3_parse("from some.module import black\n")
829 self.assertEqual(set(), black.get_future_imports(node))
830 node = black.lib2to3_parse(
831 "from __future__ import unicode_literals as _unicode_literals"
833 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
834 node = black.lib2to3_parse(
835 "from __future__ import unicode_literals as _lol, print"
837 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
839 def test_debug_visitor(self) -> None:
840 source, _ = read_data("debug_visitor.py")
841 expected, _ = read_data("debug_visitor.out")
845 def out(msg: str, **kwargs: Any) -> None:
846 out_lines.append(msg)
848 def err(msg: str, **kwargs: Any) -> None:
849 err_lines.append(msg)
851 with patch("black.debug.out", out):
852 DebugVisitor.show(source)
853 actual = "\n".join(out_lines) + "\n"
855 if expected != actual:
856 log_name = black.dump_to_file(*out_lines)
860 f"AST print out is different. Actual version dumped to {log_name}",
863 def test_format_file_contents(self) -> None:
866 with self.assertRaises(black.NothingChanged):
867 black.format_file_contents(empty, mode=mode, fast=False)
869 with self.assertRaises(black.NothingChanged):
870 black.format_file_contents(just_nl, mode=mode, fast=False)
871 same = "j = [1, 2, 3]\n"
872 with self.assertRaises(black.NothingChanged):
873 black.format_file_contents(same, mode=mode, fast=False)
874 different = "j = [1,2,3]"
876 actual = black.format_file_contents(different, mode=mode, fast=False)
877 self.assertEqual(expected, actual)
878 invalid = "return if you can"
879 with self.assertRaises(black.InvalidInput) as e:
880 black.format_file_contents(invalid, mode=mode, fast=False)
881 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
883 def test_endmarker(self) -> None:
884 n = black.lib2to3_parse("\n")
885 self.assertEqual(n.type, black.syms.file_input)
886 self.assertEqual(len(n.children), 1)
887 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
889 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
890 def test_assertFormatEqual(self) -> None:
894 def out(msg: str, **kwargs: Any) -> None:
895 out_lines.append(msg)
897 def err(msg: str, **kwargs: Any) -> None:
898 err_lines.append(msg)
900 with patch("black.output._out", out), patch("black.output._err", err):
901 with self.assertRaises(AssertionError):
902 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
904 out_str = "".join(out_lines)
905 self.assertTrue("Expected tree:" in out_str)
906 self.assertTrue("Actual tree:" in out_str)
907 self.assertEqual("".join(err_lines), "")
910 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
911 def test_works_in_mono_process_only_environment(self) -> None:
912 with cache_dir() as workspace:
914 (workspace / "one.py").resolve(),
915 (workspace / "two.py").resolve(),
917 f.write_text('print("hello")\n')
918 self.invokeBlack([str(workspace)])
921 def test_check_diff_use_together(self) -> None:
923 # Files which will be reformatted.
924 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
925 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
926 # Files which will not be reformatted.
927 src2 = (THIS_DIR / "data" / "composition.py").resolve()
928 self.invokeBlack([str(src2), "--diff", "--check"])
929 # Multi file command.
930 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
932 def test_no_files(self) -> None:
934 # Without an argument, black exits with error code 0.
937 def test_broken_symlink(self) -> None:
938 with cache_dir() as workspace:
939 symlink = workspace / "broken_link.py"
941 symlink.symlink_to("nonexistent.py")
943 self.skipTest(f"Can't create symlinks: {e}")
944 self.invokeBlack([str(workspace.resolve())])
946 def test_single_file_force_pyi(self) -> None:
947 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
948 contents, expected = read_data("force_pyi")
949 with cache_dir() as workspace:
950 path = (workspace / "file.py").resolve()
951 with open(path, "w") as fh:
953 self.invokeBlack([str(path), "--pyi"])
954 with open(path, "r") as fh:
956 # verify cache with --pyi is separate
957 pyi_cache = black.read_cache(pyi_mode)
958 self.assertIn(str(path), pyi_cache)
959 normal_cache = black.read_cache(DEFAULT_MODE)
960 self.assertNotIn(str(path), normal_cache)
961 self.assertFormatEqual(expected, actual)
962 black.assert_equivalent(contents, actual)
963 black.assert_stable(contents, actual, pyi_mode)
966 def test_multi_file_force_pyi(self) -> None:
967 reg_mode = DEFAULT_MODE
968 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
969 contents, expected = read_data("force_pyi")
970 with cache_dir() as workspace:
972 (workspace / "file1.py").resolve(),
973 (workspace / "file2.py").resolve(),
976 with open(path, "w") as fh:
978 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
980 with open(path, "r") as fh:
982 self.assertEqual(actual, expected)
983 # verify cache with --pyi is separate
984 pyi_cache = black.read_cache(pyi_mode)
985 normal_cache = black.read_cache(reg_mode)
987 self.assertIn(str(path), pyi_cache)
988 self.assertNotIn(str(path), normal_cache)
990 def test_pipe_force_pyi(self) -> None:
991 source, expected = read_data("force_pyi")
992 result = CliRunner().invoke(
993 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
995 self.assertEqual(result.exit_code, 0)
996 actual = result.output
997 self.assertFormatEqual(actual, expected)
999 def test_single_file_force_py36(self) -> None:
1000 reg_mode = DEFAULT_MODE
1001 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1002 source, expected = read_data("force_py36")
1003 with cache_dir() as workspace:
1004 path = (workspace / "file.py").resolve()
1005 with open(path, "w") as fh:
1007 self.invokeBlack([str(path), *PY36_ARGS])
1008 with open(path, "r") as fh:
1010 # verify cache with --target-version is separate
1011 py36_cache = black.read_cache(py36_mode)
1012 self.assertIn(str(path), py36_cache)
1013 normal_cache = black.read_cache(reg_mode)
1014 self.assertNotIn(str(path), normal_cache)
1015 self.assertEqual(actual, expected)
1018 def test_multi_file_force_py36(self) -> None:
1019 reg_mode = DEFAULT_MODE
1020 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1021 source, expected = read_data("force_py36")
1022 with cache_dir() as workspace:
1024 (workspace / "file1.py").resolve(),
1025 (workspace / "file2.py").resolve(),
1028 with open(path, "w") as fh:
1030 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1032 with open(path, "r") as fh:
1034 self.assertEqual(actual, expected)
1035 # verify cache with --target-version is separate
1036 pyi_cache = black.read_cache(py36_mode)
1037 normal_cache = black.read_cache(reg_mode)
1039 self.assertIn(str(path), pyi_cache)
1040 self.assertNotIn(str(path), normal_cache)
1042 def test_pipe_force_py36(self) -> None:
1043 source, expected = read_data("force_py36")
1044 result = CliRunner().invoke(
1046 ["-", "-q", "--target-version=py36"],
1047 input=BytesIO(source.encode("utf8")),
1049 self.assertEqual(result.exit_code, 0)
1050 actual = result.output
1051 self.assertFormatEqual(actual, expected)
1053 def test_reformat_one_with_stdin(self) -> None:
1055 "black.format_stdin_to_stdout",
1056 return_value=lambda *args, **kwargs: black.Changed.YES,
1058 report = MagicMock()
1063 write_back=black.WriteBack.YES,
1067 fsts.assert_called_once()
1068 report.done.assert_called_with(path, black.Changed.YES)
1070 def test_reformat_one_with_stdin_filename(self) -> None:
1072 "black.format_stdin_to_stdout",
1073 return_value=lambda *args, **kwargs: black.Changed.YES,
1075 report = MagicMock()
1077 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1082 write_back=black.WriteBack.YES,
1086 fsts.assert_called_once_with(
1087 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1089 # __BLACK_STDIN_FILENAME__ should have been stripped
1090 report.done.assert_called_with(expected, black.Changed.YES)
1092 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1094 "black.format_stdin_to_stdout",
1095 return_value=lambda *args, **kwargs: black.Changed.YES,
1097 report = MagicMock()
1099 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1104 write_back=black.WriteBack.YES,
1108 fsts.assert_called_once_with(
1110 write_back=black.WriteBack.YES,
1111 mode=replace(DEFAULT_MODE, is_pyi=True),
1113 # __BLACK_STDIN_FILENAME__ should have been stripped
1114 report.done.assert_called_with(expected, black.Changed.YES)
1116 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1118 "black.format_stdin_to_stdout",
1119 return_value=lambda *args, **kwargs: black.Changed.YES,
1121 report = MagicMock()
1123 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1128 write_back=black.WriteBack.YES,
1132 fsts.assert_called_once_with(
1134 write_back=black.WriteBack.YES,
1135 mode=replace(DEFAULT_MODE, is_ipynb=True),
1137 # __BLACK_STDIN_FILENAME__ should have been stripped
1138 report.done.assert_called_with(expected, black.Changed.YES)
1140 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1142 "black.format_stdin_to_stdout",
1143 return_value=lambda *args, **kwargs: black.Changed.YES,
1145 report = MagicMock()
1146 # Even with an existing file, since we are forcing stdin, black
1147 # should output to stdout and not modify the file inplace
1148 p = Path(str(THIS_DIR / "data/collections.py"))
1149 # Make sure is_file actually returns True
1150 self.assertTrue(p.is_file())
1151 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1156 write_back=black.WriteBack.YES,
1160 fsts.assert_called_once()
1161 # __BLACK_STDIN_FILENAME__ should have been stripped
1162 report.done.assert_called_with(expected, black.Changed.YES)
1164 def test_reformat_one_with_stdin_empty(self) -> None:
1165 output = io.StringIO()
1166 with patch("io.TextIOWrapper", lambda *args, **kwargs: output):
1168 black.format_stdin_to_stdout(
1171 write_back=black.WriteBack.YES,
1174 except io.UnsupportedOperation:
1175 pass # StringIO does not support detach
1176 assert output.getvalue() == ""
1178 def test_invalid_cli_regex(self) -> None:
1179 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1180 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1182 def test_required_version_matches_version(self) -> None:
1184 ["--required-version", black.__version__], exit_code=0, ignore_config=True
1187 def test_required_version_does_not_match_version(self) -> None:
1189 ["--required-version", "20.99b"], exit_code=1, ignore_config=True
1192 def test_preserves_line_endings(self) -> None:
1193 with TemporaryDirectory() as workspace:
1194 test_file = Path(workspace) / "test.py"
1195 for nl in ["\n", "\r\n"]:
1196 contents = nl.join(["def f( ):", " pass"])
1197 test_file.write_bytes(contents.encode())
1198 ff(test_file, write_back=black.WriteBack.YES)
1199 updated_contents: bytes = test_file.read_bytes()
1200 self.assertIn(nl.encode(), updated_contents)
1202 self.assertNotIn(b"\r\n", updated_contents)
1204 def test_preserves_line_endings_via_stdin(self) -> None:
1205 for nl in ["\n", "\r\n"]:
1206 contents = nl.join(["def f( ):", " pass"])
1207 runner = BlackRunner()
1208 result = runner.invoke(
1209 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1211 self.assertEqual(result.exit_code, 0)
1212 output = result.stdout_bytes
1213 self.assertIn(nl.encode("utf8"), output)
1215 self.assertNotIn(b"\r\n", output)
1217 def test_assert_equivalent_different_asts(self) -> None:
1218 with self.assertRaises(AssertionError):
1219 black.assert_equivalent("{}", "None")
1221 def test_shhh_click(self) -> None:
1223 from click import _unicodefun
1224 except ModuleNotFoundError:
1225 self.skipTest("Incompatible Click version")
1226 if not hasattr(_unicodefun, "_verify_python3_env"):
1227 self.skipTest("Incompatible Click version")
1228 # First, let's see if Click is crashing with a preferred ASCII charset.
1229 with patch("locale.getpreferredencoding") as gpe:
1230 gpe.return_value = "ASCII"
1231 with self.assertRaises(RuntimeError):
1232 _unicodefun._verify_python3_env() # type: ignore
1233 # Now, let's silence Click...
1235 # ...and confirm it's silent.
1236 with patch("locale.getpreferredencoding") as gpe:
1237 gpe.return_value = "ASCII"
1239 _unicodefun._verify_python3_env() # type: ignore
1240 except RuntimeError as re:
1241 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1243 def test_root_logger_not_used_directly(self) -> None:
1244 def fail(*args: Any, **kwargs: Any) -> None:
1245 self.fail("Record created with root logger")
1247 with patch.multiple(
1256 ff(THIS_DIR / "util.py")
1258 def test_invalid_config_return_code(self) -> None:
1259 tmp_file = Path(black.dump_to_file())
1261 tmp_config = Path(black.dump_to_file())
1263 args = ["--config", str(tmp_config), str(tmp_file)]
1264 self.invokeBlack(args, exit_code=2, ignore_config=False)
1268 def test_parse_pyproject_toml(self) -> None:
1269 test_toml_file = THIS_DIR / "test.toml"
1270 config = black.parse_pyproject_toml(str(test_toml_file))
1271 self.assertEqual(config["verbose"], 1)
1272 self.assertEqual(config["check"], "no")
1273 self.assertEqual(config["diff"], "y")
1274 self.assertEqual(config["color"], True)
1275 self.assertEqual(config["line_length"], 79)
1276 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1277 self.assertEqual(config["exclude"], r"\.pyi?$")
1278 self.assertEqual(config["include"], r"\.py?$")
1280 def test_read_pyproject_toml(self) -> None:
1281 test_toml_file = THIS_DIR / "test.toml"
1282 fake_ctx = FakeContext()
1283 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1284 config = fake_ctx.default_map
1285 self.assertEqual(config["verbose"], "1")
1286 self.assertEqual(config["check"], "no")
1287 self.assertEqual(config["diff"], "y")
1288 self.assertEqual(config["color"], "True")
1289 self.assertEqual(config["line_length"], "79")
1290 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1291 self.assertEqual(config["exclude"], r"\.pyi?$")
1292 self.assertEqual(config["include"], r"\.py?$")
1294 def test_find_project_root(self) -> None:
1295 with TemporaryDirectory() as workspace:
1296 root = Path(workspace)
1297 test_dir = root / "test"
1300 src_dir = root / "src"
1303 root_pyproject = root / "pyproject.toml"
1304 root_pyproject.touch()
1305 src_pyproject = src_dir / "pyproject.toml"
1306 src_pyproject.touch()
1307 src_python = src_dir / "foo.py"
1311 black.find_project_root((src_dir, test_dir)), root.resolve()
1313 self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1314 self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1317 "black.files.find_user_pyproject_toml",
1318 black.files.find_user_pyproject_toml.__wrapped__,
1320 def test_find_user_pyproject_toml_linux(self) -> None:
1321 if system() == "Windows":
1324 # Test if XDG_CONFIG_HOME is checked
1325 with TemporaryDirectory() as workspace:
1326 tmp_user_config = Path(workspace) / "black"
1327 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1329 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1332 # Test fallback for XDG_CONFIG_HOME
1333 with patch.dict("os.environ"):
1334 os.environ.pop("XDG_CONFIG_HOME", None)
1335 fallback_user_config = Path("~/.config").expanduser() / "black"
1337 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1340 def test_find_user_pyproject_toml_windows(self) -> None:
1341 if system() != "Windows":
1344 user_config_path = Path.home() / ".black"
1346 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1349 def test_bpo_33660_workaround(self) -> None:
1350 if system() == "Windows":
1353 # https://bugs.python.org/issue33660
1355 with change_directory(root):
1356 path = Path("workspace") / "project"
1357 report = black.Report(verbose=True)
1358 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1359 self.assertEqual(normalized_path, "workspace/project")
1361 def test_newline_comment_interaction(self) -> None:
1362 source = "class A:\\\r\n# type: ignore\n pass\n"
1363 output = black.format_str(source, mode=DEFAULT_MODE)
1364 black.assert_stable(source, output, mode=DEFAULT_MODE)
1366 def test_bpo_2142_workaround(self) -> None:
1368 # https://bugs.python.org/issue2142
1370 source, _ = read_data("missing_final_newline.py")
1371 # read_data adds a trailing newline
1372 source = source.rstrip()
1373 expected, _ = read_data("missing_final_newline.diff")
1374 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1375 diff_header = re.compile(
1376 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1377 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1380 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1381 self.assertEqual(result.exit_code, 0)
1384 actual = result.output
1385 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1386 self.assertEqual(actual, expected)
1388 @pytest.mark.python2
1389 def test_docstring_reformat_for_py27(self) -> None:
1391 Check that stripping trailing whitespace from Python 2 docstrings
1392 doesn't trigger a "not equivalent to source" error
1395 b'def foo():\r\n """Testing\r\n Testing """\r\n print "Foo"\r\n'
1397 expected = 'def foo():\n """Testing\n Testing"""\n print "Foo"\n'
1399 result = CliRunner().invoke(
1401 ["-", "-q", "--target-version=py27"],
1402 input=BytesIO(source),
1405 self.assertEqual(result.exit_code, 0)
1406 actual = result.output
1407 self.assertFormatEqual(actual, expected)
1410 def compare_results(
1411 result: click.testing.Result, expected_value: str, expected_exit_code: int
1413 """Helper method to test the value and exit code of a click Result."""
1415 result.output == expected_value
1416 ), "The output did not match the expected value."
1417 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1419 def test_code_option(self) -> None:
1420 """Test the code option with no changes."""
1421 code = 'print("Hello world")\n'
1422 args = ["--code", code]
1423 result = CliRunner().invoke(black.main, args)
1425 self.compare_results(result, code, 0)
1427 def test_code_option_changed(self) -> None:
1428 """Test the code option when changes are required."""
1429 code = "print('hello world')"
1430 formatted = black.format_str(code, mode=DEFAULT_MODE)
1432 args = ["--code", code]
1433 result = CliRunner().invoke(black.main, args)
1435 self.compare_results(result, formatted, 0)
1437 def test_code_option_check(self) -> None:
1438 """Test the code option when check is passed."""
1439 args = ["--check", "--code", 'print("Hello world")\n']
1440 result = CliRunner().invoke(black.main, args)
1441 self.compare_results(result, "", 0)
1443 def test_code_option_check_changed(self) -> None:
1444 """Test the code option when changes are required, and check is passed."""
1445 args = ["--check", "--code", "print('hello world')"]
1446 result = CliRunner().invoke(black.main, args)
1447 self.compare_results(result, "", 1)
1449 def test_code_option_diff(self) -> None:
1450 """Test the code option when diff is passed."""
1451 code = "print('hello world')"
1452 formatted = black.format_str(code, mode=DEFAULT_MODE)
1453 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1455 args = ["--diff", "--code", code]
1456 result = CliRunner().invoke(black.main, args)
1458 # Remove time from diff
1459 output = DIFF_TIME.sub("", result.output)
1461 assert output == result_diff, "The output did not match the expected value."
1462 assert result.exit_code == 0, "The exit code is incorrect."
1464 def test_code_option_color_diff(self) -> None:
1465 """Test the code option when color and diff are passed."""
1466 code = "print('hello world')"
1467 formatted = black.format_str(code, mode=DEFAULT_MODE)
1469 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1470 result_diff = color_diff(result_diff)
1472 args = ["--diff", "--color", "--code", code]
1473 result = CliRunner().invoke(black.main, args)
1475 # Remove time from diff
1476 output = DIFF_TIME.sub("", result.output)
1478 assert output == result_diff, "The output did not match the expected value."
1479 assert result.exit_code == 0, "The exit code is incorrect."
1481 def test_code_option_safe(self) -> None:
1482 """Test that the code option throws an error when the sanity checks fail."""
1483 # Patch black.assert_equivalent to ensure the sanity checks fail
1484 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1485 code = 'print("Hello world")'
1486 error_msg = f"{code}\nerror: cannot format <string>: \n"
1488 args = ["--safe", "--code", code]
1489 result = CliRunner().invoke(black.main, args)
1491 self.compare_results(result, error_msg, 123)
1493 def test_code_option_fast(self) -> None:
1494 """Test that the code option ignores errors when the sanity checks fail."""
1495 # Patch black.assert_equivalent to ensure the sanity checks fail
1496 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1497 code = 'print("Hello world")'
1498 formatted = black.format_str(code, mode=DEFAULT_MODE)
1500 args = ["--fast", "--code", code]
1501 result = CliRunner().invoke(black.main, args)
1503 self.compare_results(result, formatted, 0)
1505 def test_code_option_config(self) -> None:
1507 Test that the code option finds the pyproject.toml in the current directory.
1509 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1510 args = ["--code", "print"]
1511 CliRunner().invoke(black.main, args)
1513 pyproject_path = Path(Path().cwd(), "pyproject.toml").resolve()
1515 len(parse.mock_calls) >= 1
1516 ), "Expected config parse to be called with the current directory."
1518 _, call_args, _ = parse.mock_calls[0]
1520 call_args[0].lower() == str(pyproject_path).lower()
1521 ), "Incorrect config loaded."
1523 def test_code_option_parent_config(self) -> None:
1525 Test that the code option finds the pyproject.toml in the parent directory.
1527 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1528 with change_directory(Path("tests")):
1529 args = ["--code", "print"]
1530 CliRunner().invoke(black.main, args)
1532 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1534 len(parse.mock_calls) >= 1
1535 ), "Expected config parse to be called with the current directory."
1537 _, call_args, _ = parse.mock_calls[0]
1539 call_args[0].lower() == str(pyproject_path).lower()
1540 ), "Incorrect config loaded."
1544 def test_cache_broken_file(self) -> None:
1546 with cache_dir() as workspace:
1547 cache_file = get_cache_file(mode)
1548 cache_file.write_text("this is not a pickle")
1549 assert black.read_cache(mode) == {}
1550 src = (workspace / "test.py").resolve()
1551 src.write_text("print('hello')")
1552 invokeBlack([str(src)])
1553 cache = black.read_cache(mode)
1554 assert str(src) in cache
1556 def test_cache_single_file_already_cached(self) -> None:
1558 with cache_dir() as workspace:
1559 src = (workspace / "test.py").resolve()
1560 src.write_text("print('hello')")
1561 black.write_cache({}, [src], mode)
1562 invokeBlack([str(src)])
1563 assert src.read_text() == "print('hello')"
1566 def test_cache_multiple_files(self) -> None:
1568 with cache_dir() as workspace, patch(
1569 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1571 one = (workspace / "one.py").resolve()
1572 with one.open("w") as fobj:
1573 fobj.write("print('hello')")
1574 two = (workspace / "two.py").resolve()
1575 with two.open("w") as fobj:
1576 fobj.write("print('hello')")
1577 black.write_cache({}, [one], mode)
1578 invokeBlack([str(workspace)])
1579 with one.open("r") as fobj:
1580 assert fobj.read() == "print('hello')"
1581 with two.open("r") as fobj:
1582 assert fobj.read() == 'print("hello")\n'
1583 cache = black.read_cache(mode)
1584 assert str(one) in cache
1585 assert str(two) in cache
1587 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1588 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1590 with cache_dir() as workspace:
1591 src = (workspace / "test.py").resolve()
1592 with src.open("w") as fobj:
1593 fobj.write("print('hello')")
1594 with patch("black.read_cache") as read_cache, patch(
1597 cmd = [str(src), "--diff"]
1599 cmd.append("--color")
1601 cache_file = get_cache_file(mode)
1602 assert cache_file.exists() is False
1603 write_cache.assert_not_called()
1604 read_cache.assert_not_called()
1606 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1608 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1609 with cache_dir() as workspace:
1610 for tag in range(0, 4):
1611 src = (workspace / f"test{tag}.py").resolve()
1612 with src.open("w") as fobj:
1613 fobj.write("print('hello')")
1614 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1615 cmd = ["--diff", str(workspace)]
1617 cmd.append("--color")
1618 invokeBlack(cmd, exit_code=0)
1619 # this isn't quite doing what we want, but if it _isn't_
1620 # called then we cannot be using the lock it provides
1623 def test_no_cache_when_stdin(self) -> None:
1626 result = CliRunner().invoke(
1627 black.main, ["-"], input=BytesIO(b"print('hello')")
1629 assert not result.exit_code
1630 cache_file = get_cache_file(mode)
1631 assert not cache_file.exists()
1633 def test_read_cache_no_cachefile(self) -> None:
1636 assert black.read_cache(mode) == {}
1638 def test_write_cache_read_cache(self) -> None:
1640 with cache_dir() as workspace:
1641 src = (workspace / "test.py").resolve()
1643 black.write_cache({}, [src], mode)
1644 cache = black.read_cache(mode)
1645 assert str(src) in cache
1646 assert cache[str(src)] == black.get_cache_info(src)
1648 def test_filter_cached(self) -> None:
1649 with TemporaryDirectory() as workspace:
1650 path = Path(workspace)
1651 uncached = (path / "uncached").resolve()
1652 cached = (path / "cached").resolve()
1653 cached_but_changed = (path / "changed").resolve()
1656 cached_but_changed.touch()
1658 str(cached): black.get_cache_info(cached),
1659 str(cached_but_changed): (0.0, 0),
1661 todo, done = black.filter_cached(
1662 cache, {uncached, cached, cached_but_changed}
1664 assert todo == {uncached, cached_but_changed}
1665 assert done == {cached}
1667 def test_write_cache_creates_directory_if_needed(self) -> None:
1669 with cache_dir(exists=False) as workspace:
1670 assert not workspace.exists()
1671 black.write_cache({}, [], mode)
1672 assert workspace.exists()
1675 def test_failed_formatting_does_not_get_cached(self) -> None:
1677 with cache_dir() as workspace, patch(
1678 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1680 failing = (workspace / "failing.py").resolve()
1681 with failing.open("w") as fobj:
1682 fobj.write("not actually python")
1683 clean = (workspace / "clean.py").resolve()
1684 with clean.open("w") as fobj:
1685 fobj.write('print("hello")\n')
1686 invokeBlack([str(workspace)], exit_code=123)
1687 cache = black.read_cache(mode)
1688 assert str(failing) not in cache
1689 assert str(clean) in cache
1691 def test_write_cache_write_fail(self) -> None:
1693 with cache_dir(), patch.object(Path, "open") as mock:
1694 mock.side_effect = OSError
1695 black.write_cache({}, [], mode)
1697 def test_read_cache_line_lengths(self) -> None:
1699 short_mode = replace(DEFAULT_MODE, line_length=1)
1700 with cache_dir() as workspace:
1701 path = (workspace / "file.py").resolve()
1703 black.write_cache({}, [path], mode)
1704 one = black.read_cache(mode)
1705 assert str(path) in one
1706 two = black.read_cache(short_mode)
1707 assert str(path) not in two
1710 def assert_collected_sources(
1711 src: Sequence[Union[str, Path]],
1712 expected: Sequence[Union[str, Path]],
1714 exclude: Optional[str] = None,
1715 include: Optional[str] = None,
1716 extend_exclude: Optional[str] = None,
1717 force_exclude: Optional[str] = None,
1718 stdin_filename: Optional[str] = None,
1720 gs_src = tuple(str(Path(s)) for s in src)
1721 gs_expected = [Path(s) for s in expected]
1722 gs_exclude = None if exclude is None else compile_pattern(exclude)
1723 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
1724 gs_extend_exclude = (
1725 None if extend_exclude is None else compile_pattern(extend_exclude)
1727 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
1728 collected = black.get_sources(
1735 extend_exclude=gs_extend_exclude,
1736 force_exclude=gs_force_exclude,
1737 report=black.Report(),
1738 stdin_filename=stdin_filename,
1740 assert sorted(list(collected)) == sorted(gs_expected)
1743 class TestFileCollection:
1744 def test_include_exclude(self) -> None:
1745 path = THIS_DIR / "data" / "include_exclude_tests"
1748 Path(path / "b/dont_exclude/a.py"),
1749 Path(path / "b/dont_exclude/a.pyi"),
1751 assert_collected_sources(
1755 exclude=r"/exclude/|/\.definitely_exclude/",
1758 def test_gitignore_used_as_default(self) -> None:
1759 base = Path(DATA_DIR / "include_exclude_tests")
1761 base / "b/.definitely_exclude/a.py",
1762 base / "b/.definitely_exclude/a.pyi",
1765 assert_collected_sources(src, expected, extend_exclude=r"/exclude/")
1767 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1768 def test_exclude_for_issue_1572(self) -> None:
1769 # Exclude shouldn't touch files that were explicitly given to Black through the
1770 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1771 # https://github.com/psf/black/issues/1572
1772 path = DATA_DIR / "include_exclude_tests"
1773 src = [path / "b/exclude/a.py"]
1774 expected = [path / "b/exclude/a.py"]
1775 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1777 def test_gitignore_exclude(self) -> None:
1778 path = THIS_DIR / "data" / "include_exclude_tests"
1779 include = re.compile(r"\.pyi?$")
1780 exclude = re.compile(r"")
1781 report = black.Report()
1782 gitignore = PathSpec.from_lines(
1783 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1785 sources: List[Path] = []
1787 Path(path / "b/dont_exclude/a.py"),
1788 Path(path / "b/dont_exclude/a.pyi"),
1790 this_abs = THIS_DIR.resolve()
1792 black.gen_python_files(
1805 assert sorted(expected) == sorted(sources)
1807 def test_nested_gitignore(self) -> None:
1808 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
1809 include = re.compile(r"\.pyi?$")
1810 exclude = re.compile(r"")
1811 root_gitignore = black.files.get_gitignore(path)
1812 report = black.Report()
1813 expected: List[Path] = [
1814 Path(path / "x.py"),
1815 Path(path / "root/b.py"),
1816 Path(path / "root/c.py"),
1817 Path(path / "root/child/c.py"),
1819 this_abs = THIS_DIR.resolve()
1821 black.gen_python_files(
1834 assert sorted(expected) == sorted(sources)
1836 def test_invalid_gitignore(self) -> None:
1837 path = THIS_DIR / "data" / "invalid_gitignore_tests"
1838 empty_config = path / "pyproject.toml"
1839 result = BlackRunner().invoke(
1840 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1842 assert result.exit_code == 1
1843 assert result.stderr_bytes is not None
1845 gitignore = path / ".gitignore"
1846 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1848 def test_invalid_nested_gitignore(self) -> None:
1849 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
1850 empty_config = path / "pyproject.toml"
1851 result = BlackRunner().invoke(
1852 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1854 assert result.exit_code == 1
1855 assert result.stderr_bytes is not None
1857 gitignore = path / "a" / ".gitignore"
1858 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1860 def test_empty_include(self) -> None:
1861 path = DATA_DIR / "include_exclude_tests"
1864 Path(path / "b/exclude/a.pie"),
1865 Path(path / "b/exclude/a.py"),
1866 Path(path / "b/exclude/a.pyi"),
1867 Path(path / "b/dont_exclude/a.pie"),
1868 Path(path / "b/dont_exclude/a.py"),
1869 Path(path / "b/dont_exclude/a.pyi"),
1870 Path(path / "b/.definitely_exclude/a.pie"),
1871 Path(path / "b/.definitely_exclude/a.py"),
1872 Path(path / "b/.definitely_exclude/a.pyi"),
1873 Path(path / ".gitignore"),
1874 Path(path / "pyproject.toml"),
1876 # Setting exclude explicitly to an empty string to block .gitignore usage.
1877 assert_collected_sources(src, expected, include="", exclude="")
1879 def test_extend_exclude(self) -> None:
1880 path = DATA_DIR / "include_exclude_tests"
1883 Path(path / "b/exclude/a.py"),
1884 Path(path / "b/dont_exclude/a.py"),
1886 assert_collected_sources(
1887 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
1890 def test_symlink_out_of_root_directory(self) -> None:
1892 root = THIS_DIR.resolve()
1894 include = re.compile(black.DEFAULT_INCLUDES)
1895 exclude = re.compile(black.DEFAULT_EXCLUDES)
1896 report = black.Report()
1897 gitignore = PathSpec.from_lines("gitwildmatch", [])
1898 # `child` should behave like a symlink which resolved path is clearly
1899 # outside of the `root` directory.
1900 path.iterdir.return_value = [child]
1901 child.resolve.return_value = Path("/a/b/c")
1902 child.as_posix.return_value = "/a/b/c"
1903 child.is_symlink.return_value = True
1906 black.gen_python_files(
1919 except ValueError as ve:
1920 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
1921 path.iterdir.assert_called_once()
1922 child.resolve.assert_called_once()
1923 child.is_symlink.assert_called_once()
1924 # `child` should behave like a strange file which resolved path is clearly
1925 # outside of the `root` directory.
1926 child.is_symlink.return_value = False
1927 with pytest.raises(ValueError):
1929 black.gen_python_files(
1942 path.iterdir.assert_called()
1943 assert path.iterdir.call_count == 2
1944 child.resolve.assert_called()
1945 assert child.resolve.call_count == 2
1946 child.is_symlink.assert_called()
1947 assert child.is_symlink.call_count == 2
1949 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1950 def test_get_sources_with_stdin(self) -> None:
1953 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1955 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1956 def test_get_sources_with_stdin_filename(self) -> None:
1958 stdin_filename = str(THIS_DIR / "data/collections.py")
1959 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
1960 assert_collected_sources(
1963 exclude=r"/exclude/a\.py",
1964 stdin_filename=stdin_filename,
1967 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1968 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
1969 # Exclude shouldn't exclude stdin_filename since it is mimicking the
1970 # file being passed directly. This is the same as
1971 # test_exclude_for_issue_1572
1972 path = DATA_DIR / "include_exclude_tests"
1974 stdin_filename = str(path / "b/exclude/a.py")
1975 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
1976 assert_collected_sources(
1979 exclude=r"/exclude/|a\.py",
1980 stdin_filename=stdin_filename,
1983 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1984 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
1985 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
1986 # file being passed directly. This is the same as
1987 # test_exclude_for_issue_1572
1989 path = THIS_DIR / "data" / "include_exclude_tests"
1990 stdin_filename = str(path / "b/exclude/a.py")
1991 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
1992 assert_collected_sources(
1995 extend_exclude=r"/exclude/|a\.py",
1996 stdin_filename=stdin_filename,
1999 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
2000 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2001 # Force exclude should exclude the file when passing it through
2003 path = THIS_DIR / "data" / "include_exclude_tests"
2004 stdin_filename = str(path / "b/exclude/a.py")
2005 assert_collected_sources(
2008 force_exclude=r"/exclude/|a\.py",
2009 stdin_filename=stdin_filename,
2013 with open(black.__file__, "r", encoding="utf-8") as _bf:
2014 black_source_lines = _bf.readlines()
2017 def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
2018 """Show function calls `from black/__init__.py` as they happen.
2020 Register this with `sys.settrace()` in a test you're debugging.
2025 stack = len(inspect.stack()) - 19
2027 filename = frame.f_code.co_filename
2028 lineno = frame.f_lineno
2029 func_sig_lineno = lineno - 1
2030 funcname = black_source_lines[func_sig_lineno].strip()
2031 while funcname.startswith("@"):
2032 func_sig_lineno += 1
2033 funcname = black_source_lines[func_sig_lineno].strip()
2034 if "black/__init__.py" in filename:
2035 print(f"{' ' * stack}{lineno}:{funcname}")