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())
808 node = black.lib2to3_parse("lambda a, /, b: ...")
809 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
810 node = black.lib2to3_parse("def fn(a, /, b): ...")
811 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
813 def test_get_future_imports(self) -> None:
814 node = black.lib2to3_parse("\n")
815 self.assertEqual(set(), black.get_future_imports(node))
816 node = black.lib2to3_parse("from __future__ import black\n")
817 self.assertEqual({"black"}, black.get_future_imports(node))
818 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
819 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
820 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
821 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
822 node = black.lib2to3_parse(
823 "from __future__ import multiple\nfrom __future__ import imports\n"
825 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
826 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
827 self.assertEqual({"black"}, black.get_future_imports(node))
828 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
829 self.assertEqual({"black"}, black.get_future_imports(node))
830 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
831 self.assertEqual(set(), black.get_future_imports(node))
832 node = black.lib2to3_parse("from some.module import black\n")
833 self.assertEqual(set(), black.get_future_imports(node))
834 node = black.lib2to3_parse(
835 "from __future__ import unicode_literals as _unicode_literals"
837 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
838 node = black.lib2to3_parse(
839 "from __future__ import unicode_literals as _lol, print"
841 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
843 def test_debug_visitor(self) -> None:
844 source, _ = read_data("debug_visitor.py")
845 expected, _ = read_data("debug_visitor.out")
849 def out(msg: str, **kwargs: Any) -> None:
850 out_lines.append(msg)
852 def err(msg: str, **kwargs: Any) -> None:
853 err_lines.append(msg)
855 with patch("black.debug.out", out):
856 DebugVisitor.show(source)
857 actual = "\n".join(out_lines) + "\n"
859 if expected != actual:
860 log_name = black.dump_to_file(*out_lines)
864 f"AST print out is different. Actual version dumped to {log_name}",
867 def test_format_file_contents(self) -> None:
870 with self.assertRaises(black.NothingChanged):
871 black.format_file_contents(empty, mode=mode, fast=False)
873 with self.assertRaises(black.NothingChanged):
874 black.format_file_contents(just_nl, mode=mode, fast=False)
875 same = "j = [1, 2, 3]\n"
876 with self.assertRaises(black.NothingChanged):
877 black.format_file_contents(same, mode=mode, fast=False)
878 different = "j = [1,2,3]"
880 actual = black.format_file_contents(different, mode=mode, fast=False)
881 self.assertEqual(expected, actual)
882 invalid = "return if you can"
883 with self.assertRaises(black.InvalidInput) as e:
884 black.format_file_contents(invalid, mode=mode, fast=False)
885 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
887 def test_endmarker(self) -> None:
888 n = black.lib2to3_parse("\n")
889 self.assertEqual(n.type, black.syms.file_input)
890 self.assertEqual(len(n.children), 1)
891 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
893 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
894 def test_assertFormatEqual(self) -> None:
898 def out(msg: str, **kwargs: Any) -> None:
899 out_lines.append(msg)
901 def err(msg: str, **kwargs: Any) -> None:
902 err_lines.append(msg)
904 with patch("black.output._out", out), patch("black.output._err", err):
905 with self.assertRaises(AssertionError):
906 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
908 out_str = "".join(out_lines)
909 self.assertTrue("Expected tree:" in out_str)
910 self.assertTrue("Actual tree:" in out_str)
911 self.assertEqual("".join(err_lines), "")
914 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
915 def test_works_in_mono_process_only_environment(self) -> None:
916 with cache_dir() as workspace:
918 (workspace / "one.py").resolve(),
919 (workspace / "two.py").resolve(),
921 f.write_text('print("hello")\n')
922 self.invokeBlack([str(workspace)])
925 def test_check_diff_use_together(self) -> None:
927 # Files which will be reformatted.
928 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
929 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
930 # Files which will not be reformatted.
931 src2 = (THIS_DIR / "data" / "composition.py").resolve()
932 self.invokeBlack([str(src2), "--diff", "--check"])
933 # Multi file command.
934 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
936 def test_no_files(self) -> None:
938 # Without an argument, black exits with error code 0.
941 def test_broken_symlink(self) -> None:
942 with cache_dir() as workspace:
943 symlink = workspace / "broken_link.py"
945 symlink.symlink_to("nonexistent.py")
947 self.skipTest(f"Can't create symlinks: {e}")
948 self.invokeBlack([str(workspace.resolve())])
950 def test_single_file_force_pyi(self) -> None:
951 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
952 contents, expected = read_data("force_pyi")
953 with cache_dir() as workspace:
954 path = (workspace / "file.py").resolve()
955 with open(path, "w") as fh:
957 self.invokeBlack([str(path), "--pyi"])
958 with open(path, "r") as fh:
960 # verify cache with --pyi is separate
961 pyi_cache = black.read_cache(pyi_mode)
962 self.assertIn(str(path), pyi_cache)
963 normal_cache = black.read_cache(DEFAULT_MODE)
964 self.assertNotIn(str(path), normal_cache)
965 self.assertFormatEqual(expected, actual)
966 black.assert_equivalent(contents, actual)
967 black.assert_stable(contents, actual, pyi_mode)
970 def test_multi_file_force_pyi(self) -> None:
971 reg_mode = DEFAULT_MODE
972 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
973 contents, expected = read_data("force_pyi")
974 with cache_dir() as workspace:
976 (workspace / "file1.py").resolve(),
977 (workspace / "file2.py").resolve(),
980 with open(path, "w") as fh:
982 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
984 with open(path, "r") as fh:
986 self.assertEqual(actual, expected)
987 # verify cache with --pyi is separate
988 pyi_cache = black.read_cache(pyi_mode)
989 normal_cache = black.read_cache(reg_mode)
991 self.assertIn(str(path), pyi_cache)
992 self.assertNotIn(str(path), normal_cache)
994 def test_pipe_force_pyi(self) -> None:
995 source, expected = read_data("force_pyi")
996 result = CliRunner().invoke(
997 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
999 self.assertEqual(result.exit_code, 0)
1000 actual = result.output
1001 self.assertFormatEqual(actual, expected)
1003 def test_single_file_force_py36(self) -> None:
1004 reg_mode = DEFAULT_MODE
1005 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1006 source, expected = read_data("force_py36")
1007 with cache_dir() as workspace:
1008 path = (workspace / "file.py").resolve()
1009 with open(path, "w") as fh:
1011 self.invokeBlack([str(path), *PY36_ARGS])
1012 with open(path, "r") as fh:
1014 # verify cache with --target-version is separate
1015 py36_cache = black.read_cache(py36_mode)
1016 self.assertIn(str(path), py36_cache)
1017 normal_cache = black.read_cache(reg_mode)
1018 self.assertNotIn(str(path), normal_cache)
1019 self.assertEqual(actual, expected)
1022 def test_multi_file_force_py36(self) -> None:
1023 reg_mode = DEFAULT_MODE
1024 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1025 source, expected = read_data("force_py36")
1026 with cache_dir() as workspace:
1028 (workspace / "file1.py").resolve(),
1029 (workspace / "file2.py").resolve(),
1032 with open(path, "w") as fh:
1034 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1036 with open(path, "r") as fh:
1038 self.assertEqual(actual, expected)
1039 # verify cache with --target-version is separate
1040 pyi_cache = black.read_cache(py36_mode)
1041 normal_cache = black.read_cache(reg_mode)
1043 self.assertIn(str(path), pyi_cache)
1044 self.assertNotIn(str(path), normal_cache)
1046 def test_pipe_force_py36(self) -> None:
1047 source, expected = read_data("force_py36")
1048 result = CliRunner().invoke(
1050 ["-", "-q", "--target-version=py36"],
1051 input=BytesIO(source.encode("utf8")),
1053 self.assertEqual(result.exit_code, 0)
1054 actual = result.output
1055 self.assertFormatEqual(actual, expected)
1057 def test_reformat_one_with_stdin(self) -> None:
1059 "black.format_stdin_to_stdout",
1060 return_value=lambda *args, **kwargs: black.Changed.YES,
1062 report = MagicMock()
1067 write_back=black.WriteBack.YES,
1071 fsts.assert_called_once()
1072 report.done.assert_called_with(path, black.Changed.YES)
1074 def test_reformat_one_with_stdin_filename(self) -> None:
1076 "black.format_stdin_to_stdout",
1077 return_value=lambda *args, **kwargs: black.Changed.YES,
1079 report = MagicMock()
1081 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1086 write_back=black.WriteBack.YES,
1090 fsts.assert_called_once_with(
1091 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1093 # __BLACK_STDIN_FILENAME__ should have been stripped
1094 report.done.assert_called_with(expected, black.Changed.YES)
1096 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1098 "black.format_stdin_to_stdout",
1099 return_value=lambda *args, **kwargs: black.Changed.YES,
1101 report = MagicMock()
1103 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1108 write_back=black.WriteBack.YES,
1112 fsts.assert_called_once_with(
1114 write_back=black.WriteBack.YES,
1115 mode=replace(DEFAULT_MODE, is_pyi=True),
1117 # __BLACK_STDIN_FILENAME__ should have been stripped
1118 report.done.assert_called_with(expected, black.Changed.YES)
1120 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1122 "black.format_stdin_to_stdout",
1123 return_value=lambda *args, **kwargs: black.Changed.YES,
1125 report = MagicMock()
1127 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1132 write_back=black.WriteBack.YES,
1136 fsts.assert_called_once_with(
1138 write_back=black.WriteBack.YES,
1139 mode=replace(DEFAULT_MODE, is_ipynb=True),
1141 # __BLACK_STDIN_FILENAME__ should have been stripped
1142 report.done.assert_called_with(expected, black.Changed.YES)
1144 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1146 "black.format_stdin_to_stdout",
1147 return_value=lambda *args, **kwargs: black.Changed.YES,
1149 report = MagicMock()
1150 # Even with an existing file, since we are forcing stdin, black
1151 # should output to stdout and not modify the file inplace
1152 p = Path(str(THIS_DIR / "data/collections.py"))
1153 # Make sure is_file actually returns True
1154 self.assertTrue(p.is_file())
1155 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1160 write_back=black.WriteBack.YES,
1164 fsts.assert_called_once()
1165 # __BLACK_STDIN_FILENAME__ should have been stripped
1166 report.done.assert_called_with(expected, black.Changed.YES)
1168 def test_reformat_one_with_stdin_empty(self) -> None:
1169 output = io.StringIO()
1170 with patch("io.TextIOWrapper", lambda *args, **kwargs: output):
1172 black.format_stdin_to_stdout(
1175 write_back=black.WriteBack.YES,
1178 except io.UnsupportedOperation:
1179 pass # StringIO does not support detach
1180 assert output.getvalue() == ""
1182 def test_invalid_cli_regex(self) -> None:
1183 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1184 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1186 def test_required_version_matches_version(self) -> None:
1188 ["--required-version", black.__version__], exit_code=0, ignore_config=True
1191 def test_required_version_does_not_match_version(self) -> None:
1193 ["--required-version", "20.99b"], exit_code=1, ignore_config=True
1196 def test_preserves_line_endings(self) -> None:
1197 with TemporaryDirectory() as workspace:
1198 test_file = Path(workspace) / "test.py"
1199 for nl in ["\n", "\r\n"]:
1200 contents = nl.join(["def f( ):", " pass"])
1201 test_file.write_bytes(contents.encode())
1202 ff(test_file, write_back=black.WriteBack.YES)
1203 updated_contents: bytes = test_file.read_bytes()
1204 self.assertIn(nl.encode(), updated_contents)
1206 self.assertNotIn(b"\r\n", updated_contents)
1208 def test_preserves_line_endings_via_stdin(self) -> None:
1209 for nl in ["\n", "\r\n"]:
1210 contents = nl.join(["def f( ):", " pass"])
1211 runner = BlackRunner()
1212 result = runner.invoke(
1213 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1215 self.assertEqual(result.exit_code, 0)
1216 output = result.stdout_bytes
1217 self.assertIn(nl.encode("utf8"), output)
1219 self.assertNotIn(b"\r\n", output)
1221 def test_assert_equivalent_different_asts(self) -> None:
1222 with self.assertRaises(AssertionError):
1223 black.assert_equivalent("{}", "None")
1225 def test_shhh_click(self) -> None:
1227 from click import _unicodefun
1228 except ModuleNotFoundError:
1229 self.skipTest("Incompatible Click version")
1230 if not hasattr(_unicodefun, "_verify_python3_env"):
1231 self.skipTest("Incompatible Click version")
1232 # First, let's see if Click is crashing with a preferred ASCII charset.
1233 with patch("locale.getpreferredencoding") as gpe:
1234 gpe.return_value = "ASCII"
1235 with self.assertRaises(RuntimeError):
1236 _unicodefun._verify_python3_env() # type: ignore
1237 # Now, let's silence Click...
1239 # ...and confirm it's silent.
1240 with patch("locale.getpreferredencoding") as gpe:
1241 gpe.return_value = "ASCII"
1243 _unicodefun._verify_python3_env() # type: ignore
1244 except RuntimeError as re:
1245 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1247 def test_root_logger_not_used_directly(self) -> None:
1248 def fail(*args: Any, **kwargs: Any) -> None:
1249 self.fail("Record created with root logger")
1251 with patch.multiple(
1260 ff(THIS_DIR / "util.py")
1262 def test_invalid_config_return_code(self) -> None:
1263 tmp_file = Path(black.dump_to_file())
1265 tmp_config = Path(black.dump_to_file())
1267 args = ["--config", str(tmp_config), str(tmp_file)]
1268 self.invokeBlack(args, exit_code=2, ignore_config=False)
1272 def test_parse_pyproject_toml(self) -> None:
1273 test_toml_file = THIS_DIR / "test.toml"
1274 config = black.parse_pyproject_toml(str(test_toml_file))
1275 self.assertEqual(config["verbose"], 1)
1276 self.assertEqual(config["check"], "no")
1277 self.assertEqual(config["diff"], "y")
1278 self.assertEqual(config["color"], True)
1279 self.assertEqual(config["line_length"], 79)
1280 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1281 self.assertEqual(config["exclude"], r"\.pyi?$")
1282 self.assertEqual(config["include"], r"\.py?$")
1284 def test_read_pyproject_toml(self) -> None:
1285 test_toml_file = THIS_DIR / "test.toml"
1286 fake_ctx = FakeContext()
1287 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1288 config = fake_ctx.default_map
1289 self.assertEqual(config["verbose"], "1")
1290 self.assertEqual(config["check"], "no")
1291 self.assertEqual(config["diff"], "y")
1292 self.assertEqual(config["color"], "True")
1293 self.assertEqual(config["line_length"], "79")
1294 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1295 self.assertEqual(config["exclude"], r"\.pyi?$")
1296 self.assertEqual(config["include"], r"\.py?$")
1298 def test_find_project_root(self) -> None:
1299 with TemporaryDirectory() as workspace:
1300 root = Path(workspace)
1301 test_dir = root / "test"
1304 src_dir = root / "src"
1307 root_pyproject = root / "pyproject.toml"
1308 root_pyproject.touch()
1309 src_pyproject = src_dir / "pyproject.toml"
1310 src_pyproject.touch()
1311 src_python = src_dir / "foo.py"
1315 black.find_project_root((src_dir, test_dir)), root.resolve()
1317 self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1318 self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1321 "black.files.find_user_pyproject_toml",
1322 black.files.find_user_pyproject_toml.__wrapped__,
1324 def test_find_user_pyproject_toml_linux(self) -> None:
1325 if system() == "Windows":
1328 # Test if XDG_CONFIG_HOME is checked
1329 with TemporaryDirectory() as workspace:
1330 tmp_user_config = Path(workspace) / "black"
1331 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1333 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1336 # Test fallback for XDG_CONFIG_HOME
1337 with patch.dict("os.environ"):
1338 os.environ.pop("XDG_CONFIG_HOME", None)
1339 fallback_user_config = Path("~/.config").expanduser() / "black"
1341 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1344 def test_find_user_pyproject_toml_windows(self) -> None:
1345 if system() != "Windows":
1348 user_config_path = Path.home() / ".black"
1350 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1353 def test_bpo_33660_workaround(self) -> None:
1354 if system() == "Windows":
1357 # https://bugs.python.org/issue33660
1359 with change_directory(root):
1360 path = Path("workspace") / "project"
1361 report = black.Report(verbose=True)
1362 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1363 self.assertEqual(normalized_path, "workspace/project")
1365 def test_newline_comment_interaction(self) -> None:
1366 source = "class A:\\\r\n# type: ignore\n pass\n"
1367 output = black.format_str(source, mode=DEFAULT_MODE)
1368 black.assert_stable(source, output, mode=DEFAULT_MODE)
1370 def test_bpo_2142_workaround(self) -> None:
1372 # https://bugs.python.org/issue2142
1374 source, _ = read_data("missing_final_newline.py")
1375 # read_data adds a trailing newline
1376 source = source.rstrip()
1377 expected, _ = read_data("missing_final_newline.diff")
1378 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1379 diff_header = re.compile(
1380 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1381 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1384 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1385 self.assertEqual(result.exit_code, 0)
1388 actual = result.output
1389 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1390 self.assertEqual(actual, expected)
1392 @pytest.mark.python2
1393 def test_docstring_reformat_for_py27(self) -> None:
1395 Check that stripping trailing whitespace from Python 2 docstrings
1396 doesn't trigger a "not equivalent to source" error
1399 b'def foo():\r\n """Testing\r\n Testing """\r\n print "Foo"\r\n'
1401 expected = 'def foo():\n """Testing\n Testing"""\n print "Foo"\n'
1403 result = CliRunner().invoke(
1405 ["-", "-q", "--target-version=py27"],
1406 input=BytesIO(source),
1409 self.assertEqual(result.exit_code, 0)
1410 actual = result.output
1411 self.assertFormatEqual(actual, expected)
1414 def compare_results(
1415 result: click.testing.Result, expected_value: str, expected_exit_code: int
1417 """Helper method to test the value and exit code of a click Result."""
1419 result.output == expected_value
1420 ), "The output did not match the expected value."
1421 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1423 def test_code_option(self) -> None:
1424 """Test the code option with no changes."""
1425 code = 'print("Hello world")\n'
1426 args = ["--code", code]
1427 result = CliRunner().invoke(black.main, args)
1429 self.compare_results(result, code, 0)
1431 def test_code_option_changed(self) -> None:
1432 """Test the code option when changes are required."""
1433 code = "print('hello world')"
1434 formatted = black.format_str(code, mode=DEFAULT_MODE)
1436 args = ["--code", code]
1437 result = CliRunner().invoke(black.main, args)
1439 self.compare_results(result, formatted, 0)
1441 def test_code_option_check(self) -> None:
1442 """Test the code option when check is passed."""
1443 args = ["--check", "--code", 'print("Hello world")\n']
1444 result = CliRunner().invoke(black.main, args)
1445 self.compare_results(result, "", 0)
1447 def test_code_option_check_changed(self) -> None:
1448 """Test the code option when changes are required, and check is passed."""
1449 args = ["--check", "--code", "print('hello world')"]
1450 result = CliRunner().invoke(black.main, args)
1451 self.compare_results(result, "", 1)
1453 def test_code_option_diff(self) -> None:
1454 """Test the code option when diff is passed."""
1455 code = "print('hello world')"
1456 formatted = black.format_str(code, mode=DEFAULT_MODE)
1457 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1459 args = ["--diff", "--code", code]
1460 result = CliRunner().invoke(black.main, args)
1462 # Remove time from diff
1463 output = DIFF_TIME.sub("", result.output)
1465 assert output == result_diff, "The output did not match the expected value."
1466 assert result.exit_code == 0, "The exit code is incorrect."
1468 def test_code_option_color_diff(self) -> None:
1469 """Test the code option when color and diff are passed."""
1470 code = "print('hello world')"
1471 formatted = black.format_str(code, mode=DEFAULT_MODE)
1473 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1474 result_diff = color_diff(result_diff)
1476 args = ["--diff", "--color", "--code", code]
1477 result = CliRunner().invoke(black.main, args)
1479 # Remove time from diff
1480 output = DIFF_TIME.sub("", result.output)
1482 assert output == result_diff, "The output did not match the expected value."
1483 assert result.exit_code == 0, "The exit code is incorrect."
1485 def test_code_option_safe(self) -> None:
1486 """Test that the code option throws an error when the sanity checks fail."""
1487 # Patch black.assert_equivalent to ensure the sanity checks fail
1488 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1489 code = 'print("Hello world")'
1490 error_msg = f"{code}\nerror: cannot format <string>: \n"
1492 args = ["--safe", "--code", code]
1493 result = CliRunner().invoke(black.main, args)
1495 self.compare_results(result, error_msg, 123)
1497 def test_code_option_fast(self) -> None:
1498 """Test that the code option ignores errors when the sanity checks fail."""
1499 # Patch black.assert_equivalent to ensure the sanity checks fail
1500 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1501 code = 'print("Hello world")'
1502 formatted = black.format_str(code, mode=DEFAULT_MODE)
1504 args = ["--fast", "--code", code]
1505 result = CliRunner().invoke(black.main, args)
1507 self.compare_results(result, formatted, 0)
1509 def test_code_option_config(self) -> None:
1511 Test that the code option finds the pyproject.toml in the current directory.
1513 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1514 args = ["--code", "print"]
1515 CliRunner().invoke(black.main, args)
1517 pyproject_path = Path(Path().cwd(), "pyproject.toml").resolve()
1519 len(parse.mock_calls) >= 1
1520 ), "Expected config parse to be called with the current directory."
1522 _, call_args, _ = parse.mock_calls[0]
1524 call_args[0].lower() == str(pyproject_path).lower()
1525 ), "Incorrect config loaded."
1527 def test_code_option_parent_config(self) -> None:
1529 Test that the code option finds the pyproject.toml in the parent directory.
1531 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1532 with change_directory(Path("tests")):
1533 args = ["--code", "print"]
1534 CliRunner().invoke(black.main, args)
1536 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1538 len(parse.mock_calls) >= 1
1539 ), "Expected config parse to be called with the current directory."
1541 _, call_args, _ = parse.mock_calls[0]
1543 call_args[0].lower() == str(pyproject_path).lower()
1544 ), "Incorrect config loaded."
1548 def test_cache_broken_file(self) -> None:
1550 with cache_dir() as workspace:
1551 cache_file = get_cache_file(mode)
1552 cache_file.write_text("this is not a pickle")
1553 assert black.read_cache(mode) == {}
1554 src = (workspace / "test.py").resolve()
1555 src.write_text("print('hello')")
1556 invokeBlack([str(src)])
1557 cache = black.read_cache(mode)
1558 assert str(src) in cache
1560 def test_cache_single_file_already_cached(self) -> None:
1562 with cache_dir() as workspace:
1563 src = (workspace / "test.py").resolve()
1564 src.write_text("print('hello')")
1565 black.write_cache({}, [src], mode)
1566 invokeBlack([str(src)])
1567 assert src.read_text() == "print('hello')"
1570 def test_cache_multiple_files(self) -> None:
1572 with cache_dir() as workspace, patch(
1573 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1575 one = (workspace / "one.py").resolve()
1576 with one.open("w") as fobj:
1577 fobj.write("print('hello')")
1578 two = (workspace / "two.py").resolve()
1579 with two.open("w") as fobj:
1580 fobj.write("print('hello')")
1581 black.write_cache({}, [one], mode)
1582 invokeBlack([str(workspace)])
1583 with one.open("r") as fobj:
1584 assert fobj.read() == "print('hello')"
1585 with two.open("r") as fobj:
1586 assert fobj.read() == 'print("hello")\n'
1587 cache = black.read_cache(mode)
1588 assert str(one) in cache
1589 assert str(two) in cache
1591 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1592 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1594 with cache_dir() as workspace:
1595 src = (workspace / "test.py").resolve()
1596 with src.open("w") as fobj:
1597 fobj.write("print('hello')")
1598 with patch("black.read_cache") as read_cache, patch(
1601 cmd = [str(src), "--diff"]
1603 cmd.append("--color")
1605 cache_file = get_cache_file(mode)
1606 assert cache_file.exists() is False
1607 write_cache.assert_not_called()
1608 read_cache.assert_not_called()
1610 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1612 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1613 with cache_dir() as workspace:
1614 for tag in range(0, 4):
1615 src = (workspace / f"test{tag}.py").resolve()
1616 with src.open("w") as fobj:
1617 fobj.write("print('hello')")
1618 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1619 cmd = ["--diff", str(workspace)]
1621 cmd.append("--color")
1622 invokeBlack(cmd, exit_code=0)
1623 # this isn't quite doing what we want, but if it _isn't_
1624 # called then we cannot be using the lock it provides
1627 def test_no_cache_when_stdin(self) -> None:
1630 result = CliRunner().invoke(
1631 black.main, ["-"], input=BytesIO(b"print('hello')")
1633 assert not result.exit_code
1634 cache_file = get_cache_file(mode)
1635 assert not cache_file.exists()
1637 def test_read_cache_no_cachefile(self) -> None:
1640 assert black.read_cache(mode) == {}
1642 def test_write_cache_read_cache(self) -> None:
1644 with cache_dir() as workspace:
1645 src = (workspace / "test.py").resolve()
1647 black.write_cache({}, [src], mode)
1648 cache = black.read_cache(mode)
1649 assert str(src) in cache
1650 assert cache[str(src)] == black.get_cache_info(src)
1652 def test_filter_cached(self) -> None:
1653 with TemporaryDirectory() as workspace:
1654 path = Path(workspace)
1655 uncached = (path / "uncached").resolve()
1656 cached = (path / "cached").resolve()
1657 cached_but_changed = (path / "changed").resolve()
1660 cached_but_changed.touch()
1662 str(cached): black.get_cache_info(cached),
1663 str(cached_but_changed): (0.0, 0),
1665 todo, done = black.filter_cached(
1666 cache, {uncached, cached, cached_but_changed}
1668 assert todo == {uncached, cached_but_changed}
1669 assert done == {cached}
1671 def test_write_cache_creates_directory_if_needed(self) -> None:
1673 with cache_dir(exists=False) as workspace:
1674 assert not workspace.exists()
1675 black.write_cache({}, [], mode)
1676 assert workspace.exists()
1679 def test_failed_formatting_does_not_get_cached(self) -> None:
1681 with cache_dir() as workspace, patch(
1682 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1684 failing = (workspace / "failing.py").resolve()
1685 with failing.open("w") as fobj:
1686 fobj.write("not actually python")
1687 clean = (workspace / "clean.py").resolve()
1688 with clean.open("w") as fobj:
1689 fobj.write('print("hello")\n')
1690 invokeBlack([str(workspace)], exit_code=123)
1691 cache = black.read_cache(mode)
1692 assert str(failing) not in cache
1693 assert str(clean) in cache
1695 def test_write_cache_write_fail(self) -> None:
1697 with cache_dir(), patch.object(Path, "open") as mock:
1698 mock.side_effect = OSError
1699 black.write_cache({}, [], mode)
1701 def test_read_cache_line_lengths(self) -> None:
1703 short_mode = replace(DEFAULT_MODE, line_length=1)
1704 with cache_dir() as workspace:
1705 path = (workspace / "file.py").resolve()
1707 black.write_cache({}, [path], mode)
1708 one = black.read_cache(mode)
1709 assert str(path) in one
1710 two = black.read_cache(short_mode)
1711 assert str(path) not in two
1714 def assert_collected_sources(
1715 src: Sequence[Union[str, Path]],
1716 expected: Sequence[Union[str, Path]],
1718 exclude: Optional[str] = None,
1719 include: Optional[str] = None,
1720 extend_exclude: Optional[str] = None,
1721 force_exclude: Optional[str] = None,
1722 stdin_filename: Optional[str] = None,
1724 gs_src = tuple(str(Path(s)) for s in src)
1725 gs_expected = [Path(s) for s in expected]
1726 gs_exclude = None if exclude is None else compile_pattern(exclude)
1727 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
1728 gs_extend_exclude = (
1729 None if extend_exclude is None else compile_pattern(extend_exclude)
1731 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
1732 collected = black.get_sources(
1739 extend_exclude=gs_extend_exclude,
1740 force_exclude=gs_force_exclude,
1741 report=black.Report(),
1742 stdin_filename=stdin_filename,
1744 assert sorted(list(collected)) == sorted(gs_expected)
1747 class TestFileCollection:
1748 def test_include_exclude(self) -> None:
1749 path = THIS_DIR / "data" / "include_exclude_tests"
1752 Path(path / "b/dont_exclude/a.py"),
1753 Path(path / "b/dont_exclude/a.pyi"),
1755 assert_collected_sources(
1759 exclude=r"/exclude/|/\.definitely_exclude/",
1762 def test_gitignore_used_as_default(self) -> None:
1763 base = Path(DATA_DIR / "include_exclude_tests")
1765 base / "b/.definitely_exclude/a.py",
1766 base / "b/.definitely_exclude/a.pyi",
1769 assert_collected_sources(src, expected, extend_exclude=r"/exclude/")
1771 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1772 def test_exclude_for_issue_1572(self) -> None:
1773 # Exclude shouldn't touch files that were explicitly given to Black through the
1774 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1775 # https://github.com/psf/black/issues/1572
1776 path = DATA_DIR / "include_exclude_tests"
1777 src = [path / "b/exclude/a.py"]
1778 expected = [path / "b/exclude/a.py"]
1779 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1781 def test_gitignore_exclude(self) -> None:
1782 path = THIS_DIR / "data" / "include_exclude_tests"
1783 include = re.compile(r"\.pyi?$")
1784 exclude = re.compile(r"")
1785 report = black.Report()
1786 gitignore = PathSpec.from_lines(
1787 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1789 sources: List[Path] = []
1791 Path(path / "b/dont_exclude/a.py"),
1792 Path(path / "b/dont_exclude/a.pyi"),
1794 this_abs = THIS_DIR.resolve()
1796 black.gen_python_files(
1809 assert sorted(expected) == sorted(sources)
1811 def test_nested_gitignore(self) -> None:
1812 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
1813 include = re.compile(r"\.pyi?$")
1814 exclude = re.compile(r"")
1815 root_gitignore = black.files.get_gitignore(path)
1816 report = black.Report()
1817 expected: List[Path] = [
1818 Path(path / "x.py"),
1819 Path(path / "root/b.py"),
1820 Path(path / "root/c.py"),
1821 Path(path / "root/child/c.py"),
1823 this_abs = THIS_DIR.resolve()
1825 black.gen_python_files(
1838 assert sorted(expected) == sorted(sources)
1840 def test_invalid_gitignore(self) -> None:
1841 path = THIS_DIR / "data" / "invalid_gitignore_tests"
1842 empty_config = path / "pyproject.toml"
1843 result = BlackRunner().invoke(
1844 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1846 assert result.exit_code == 1
1847 assert result.stderr_bytes is not None
1849 gitignore = path / ".gitignore"
1850 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1852 def test_invalid_nested_gitignore(self) -> None:
1853 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
1854 empty_config = path / "pyproject.toml"
1855 result = BlackRunner().invoke(
1856 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1858 assert result.exit_code == 1
1859 assert result.stderr_bytes is not None
1861 gitignore = path / "a" / ".gitignore"
1862 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1864 def test_empty_include(self) -> None:
1865 path = DATA_DIR / "include_exclude_tests"
1868 Path(path / "b/exclude/a.pie"),
1869 Path(path / "b/exclude/a.py"),
1870 Path(path / "b/exclude/a.pyi"),
1871 Path(path / "b/dont_exclude/a.pie"),
1872 Path(path / "b/dont_exclude/a.py"),
1873 Path(path / "b/dont_exclude/a.pyi"),
1874 Path(path / "b/.definitely_exclude/a.pie"),
1875 Path(path / "b/.definitely_exclude/a.py"),
1876 Path(path / "b/.definitely_exclude/a.pyi"),
1877 Path(path / ".gitignore"),
1878 Path(path / "pyproject.toml"),
1880 # Setting exclude explicitly to an empty string to block .gitignore usage.
1881 assert_collected_sources(src, expected, include="", exclude="")
1883 def test_extend_exclude(self) -> None:
1884 path = DATA_DIR / "include_exclude_tests"
1887 Path(path / "b/exclude/a.py"),
1888 Path(path / "b/dont_exclude/a.py"),
1890 assert_collected_sources(
1891 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
1894 def test_symlink_out_of_root_directory(self) -> None:
1896 root = THIS_DIR.resolve()
1898 include = re.compile(black.DEFAULT_INCLUDES)
1899 exclude = re.compile(black.DEFAULT_EXCLUDES)
1900 report = black.Report()
1901 gitignore = PathSpec.from_lines("gitwildmatch", [])
1902 # `child` should behave like a symlink which resolved path is clearly
1903 # outside of the `root` directory.
1904 path.iterdir.return_value = [child]
1905 child.resolve.return_value = Path("/a/b/c")
1906 child.as_posix.return_value = "/a/b/c"
1907 child.is_symlink.return_value = True
1910 black.gen_python_files(
1923 except ValueError as ve:
1924 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
1925 path.iterdir.assert_called_once()
1926 child.resolve.assert_called_once()
1927 child.is_symlink.assert_called_once()
1928 # `child` should behave like a strange file which resolved path is clearly
1929 # outside of the `root` directory.
1930 child.is_symlink.return_value = False
1931 with pytest.raises(ValueError):
1933 black.gen_python_files(
1946 path.iterdir.assert_called()
1947 assert path.iterdir.call_count == 2
1948 child.resolve.assert_called()
1949 assert child.resolve.call_count == 2
1950 child.is_symlink.assert_called()
1951 assert child.is_symlink.call_count == 2
1953 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1954 def test_get_sources_with_stdin(self) -> None:
1957 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1959 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1960 def test_get_sources_with_stdin_filename(self) -> None:
1962 stdin_filename = str(THIS_DIR / "data/collections.py")
1963 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
1964 assert_collected_sources(
1967 exclude=r"/exclude/a\.py",
1968 stdin_filename=stdin_filename,
1971 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1972 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
1973 # Exclude shouldn't exclude stdin_filename since it is mimicking the
1974 # file being passed directly. This is the same as
1975 # test_exclude_for_issue_1572
1976 path = DATA_DIR / "include_exclude_tests"
1978 stdin_filename = str(path / "b/exclude/a.py")
1979 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
1980 assert_collected_sources(
1983 exclude=r"/exclude/|a\.py",
1984 stdin_filename=stdin_filename,
1987 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1988 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
1989 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
1990 # file being passed directly. This is the same as
1991 # test_exclude_for_issue_1572
1993 path = THIS_DIR / "data" / "include_exclude_tests"
1994 stdin_filename = str(path / "b/exclude/a.py")
1995 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
1996 assert_collected_sources(
1999 extend_exclude=r"/exclude/|a\.py",
2000 stdin_filename=stdin_filename,
2003 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
2004 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2005 # Force exclude should exclude the file when passing it through
2007 path = THIS_DIR / "data" / "include_exclude_tests"
2008 stdin_filename = str(path / "b/exclude/a.py")
2009 assert_collected_sources(
2012 force_exclude=r"/exclude/|a\.py",
2013 stdin_filename=stdin_filename,
2017 with open(black.__file__, "r", encoding="utf-8") as _bf:
2018 black_source_lines = _bf.readlines()
2021 def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
2022 """Show function calls `from black/__init__.py` as they happen.
2024 Register this with `sys.settrace()` in a test you're debugging.
2029 stack = len(inspect.stack()) - 19
2031 filename = frame.f_code.co_filename
2032 lineno = frame.f_lineno
2033 func_sig_lineno = lineno - 1
2034 funcname = black_source_lines[func_sig_lineno].strip()
2035 while funcname.startswith("@"):
2036 func_sig_lineno += 1
2037 funcname = black_source_lines[func_sig_lineno].strip()
2038 if "black/__init__.py" in filename:
2039 print(f"{' ' * stack}{lineno}:{funcname}")