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.
5 from concurrent.futures import ThreadPoolExecutor
6 from contextlib import contextmanager
7 from dataclasses import replace
11 from pathlib import Path
12 from platform import system
15 from tempfile import TemporaryDirectory
27 from unittest.mock import patch, MagicMock
30 from click import unstyle
31 from click.testing import CliRunner
34 from black import Feature, TargetVersion
35 from black.cache import get_cache_file
36 from black.debug import DebugVisitor
37 from black.report import Report
40 from pathspec import PathSpec
42 # Import other test classes
43 from tests.util import (
55 THIS_FILE = Path(__file__)
62 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
68 def cache_dir(exists: bool = True) -> Iterator[Path]:
69 with TemporaryDirectory() as workspace:
70 cache_dir = Path(workspace)
72 cache_dir = cache_dir / "new"
73 with patch("black.cache.CACHE_DIR", cache_dir):
78 def event_loop() -> Iterator[None]:
79 policy = asyncio.get_event_loop_policy()
80 loop = policy.new_event_loop()
81 asyncio.set_event_loop(loop)
89 class FakeContext(click.Context):
90 """A fake click Context for when calling functions that need it."""
92 def __init__(self) -> None:
93 self.default_map: Dict[str, Any] = {}
96 class FakeParameter(click.Parameter):
97 """A fake click Parameter for when calling functions that need it."""
99 def __init__(self) -> None:
103 class BlackRunner(CliRunner):
104 """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
106 def __init__(self) -> None:
107 super().__init__(mix_stderr=False)
110 class BlackTestCase(BlackBaseTestCase):
112 self, args: List[str], exit_code: int = 0, ignore_config: bool = True
114 runner = BlackRunner()
116 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
117 result = runner.invoke(black.main, args)
122 f"Failed with args: {args}\n"
123 f"stdout: {result.stdout_bytes.decode()!r}\n"
124 f"stderr: {result.stderr_bytes.decode()!r}\n"
125 f"exception: {result.exception}"
129 @patch("black.dump_to_file", dump_to_stderr)
130 def test_empty(self) -> None:
131 source = expected = ""
133 self.assertFormatEqual(expected, actual)
134 black.assert_equivalent(source, actual)
135 black.assert_stable(source, actual, DEFAULT_MODE)
137 def test_empty_ff(self) -> None:
139 tmp_file = Path(black.dump_to_file())
141 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
142 with open(tmp_file, encoding="utf8") as f:
146 self.assertFormatEqual(expected, actual)
148 def test_piping(self) -> None:
149 source, expected = read_data("src/black/__init__", data=False)
150 result = BlackRunner().invoke(
152 ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
153 input=BytesIO(source.encode("utf8")),
155 self.assertEqual(result.exit_code, 0)
156 self.assertFormatEqual(expected, result.output)
157 if source != result.output:
158 black.assert_equivalent(source, result.output)
159 black.assert_stable(source, result.output, DEFAULT_MODE)
161 def test_piping_diff(self) -> None:
162 diff_header = re.compile(
163 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
166 source, _ = read_data("expression.py")
167 expected, _ = read_data("expression.diff")
168 config = THIS_DIR / "data" / "empty_pyproject.toml"
172 f"--line-length={black.DEFAULT_LINE_LENGTH}",
174 f"--config={config}",
176 result = BlackRunner().invoke(
177 black.main, args, input=BytesIO(source.encode("utf8"))
179 self.assertEqual(result.exit_code, 0)
180 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
181 actual = actual.rstrip() + "\n" # the diff output has a trailing space
182 self.assertEqual(expected, actual)
184 def test_piping_diff_with_color(self) -> None:
185 source, _ = read_data("expression.py")
186 config = THIS_DIR / "data" / "empty_pyproject.toml"
190 f"--line-length={black.DEFAULT_LINE_LENGTH}",
193 f"--config={config}",
195 result = BlackRunner().invoke(
196 black.main, args, input=BytesIO(source.encode("utf8"))
198 actual = result.output
199 # Again, the contents are checked in a different test, so only look for colors.
200 self.assertIn("\033[1;37m", actual)
201 self.assertIn("\033[36m", actual)
202 self.assertIn("\033[32m", actual)
203 self.assertIn("\033[31m", actual)
204 self.assertIn("\033[0m", actual)
206 @patch("black.dump_to_file", dump_to_stderr)
207 def _test_wip(self) -> None:
208 source, expected = read_data("wip")
209 sys.settrace(tracefunc)
212 experimental_string_processing=False,
213 target_versions={black.TargetVersion.PY38},
215 actual = fs(source, mode=mode)
217 self.assertFormatEqual(expected, actual)
218 black.assert_equivalent(source, actual)
219 black.assert_stable(source, actual, black.FileMode())
221 @unittest.expectedFailure
222 @patch("black.dump_to_file", dump_to_stderr)
223 def test_trailing_comma_optional_parens_stability1(self) -> None:
224 source, _expected = read_data("trailing_comma_optional_parens1")
226 black.assert_stable(source, actual, DEFAULT_MODE)
228 @unittest.expectedFailure
229 @patch("black.dump_to_file", dump_to_stderr)
230 def test_trailing_comma_optional_parens_stability2(self) -> None:
231 source, _expected = read_data("trailing_comma_optional_parens2")
233 black.assert_stable(source, actual, DEFAULT_MODE)
235 @unittest.expectedFailure
236 @patch("black.dump_to_file", dump_to_stderr)
237 def test_trailing_comma_optional_parens_stability3(self) -> None:
238 source, _expected = read_data("trailing_comma_optional_parens3")
240 black.assert_stable(source, actual, DEFAULT_MODE)
242 @patch("black.dump_to_file", dump_to_stderr)
243 def test_trailing_comma_optional_parens_stability1_pass2(self) -> None:
244 source, _expected = read_data("trailing_comma_optional_parens1")
245 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
246 black.assert_stable(source, actual, DEFAULT_MODE)
248 @patch("black.dump_to_file", dump_to_stderr)
249 def test_trailing_comma_optional_parens_stability2_pass2(self) -> None:
250 source, _expected = read_data("trailing_comma_optional_parens2")
251 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
252 black.assert_stable(source, actual, DEFAULT_MODE)
254 @patch("black.dump_to_file", dump_to_stderr)
255 def test_trailing_comma_optional_parens_stability3_pass2(self) -> None:
256 source, _expected = read_data("trailing_comma_optional_parens3")
257 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
258 black.assert_stable(source, actual, DEFAULT_MODE)
260 @patch("black.dump_to_file", dump_to_stderr)
261 def test_pep_572(self) -> None:
262 source, expected = read_data("pep_572")
264 self.assertFormatEqual(expected, actual)
265 black.assert_stable(source, actual, DEFAULT_MODE)
266 if sys.version_info >= (3, 8):
267 black.assert_equivalent(source, actual)
269 @patch("black.dump_to_file", dump_to_stderr)
270 def test_pep_572_remove_parens(self) -> None:
271 source, expected = read_data("pep_572_remove_parens")
273 self.assertFormatEqual(expected, actual)
274 black.assert_stable(source, actual, DEFAULT_MODE)
275 if sys.version_info >= (3, 8):
276 black.assert_equivalent(source, actual)
278 @patch("black.dump_to_file", dump_to_stderr)
279 def test_pep_572_do_not_remove_parens(self) -> None:
280 source, expected = read_data("pep_572_do_not_remove_parens")
281 # the AST safety checks will fail, but that's expected, just make sure no
282 # parentheses are touched
283 actual = black.format_str(source, mode=DEFAULT_MODE)
284 self.assertFormatEqual(expected, actual)
286 def test_pep_572_version_detection(self) -> None:
287 source, _ = read_data("pep_572")
288 root = black.lib2to3_parse(source)
289 features = black.get_features_used(root)
290 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
291 versions = black.detect_target_versions(root)
292 self.assertIn(black.TargetVersion.PY38, versions)
294 def test_expression_ff(self) -> None:
295 source, expected = read_data("expression")
296 tmp_file = Path(black.dump_to_file(source))
298 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
299 with open(tmp_file, encoding="utf8") as f:
303 self.assertFormatEqual(expected, actual)
304 with patch("black.dump_to_file", dump_to_stderr):
305 black.assert_equivalent(source, actual)
306 black.assert_stable(source, actual, DEFAULT_MODE)
308 def test_expression_diff(self) -> None:
309 source, _ = read_data("expression.py")
310 config = THIS_DIR / "data" / "empty_pyproject.toml"
311 expected, _ = read_data("expression.diff")
312 tmp_file = Path(black.dump_to_file(source))
313 diff_header = re.compile(
314 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
315 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
318 result = BlackRunner().invoke(
319 black.main, ["--diff", str(tmp_file), f"--config={config}"]
321 self.assertEqual(result.exit_code, 0)
324 actual = result.output
325 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
326 if expected != actual:
327 dump = black.dump_to_file(actual)
329 "Expected diff isn't equal to the actual. If you made changes to"
330 " expression.py and this is an anticipated difference, overwrite"
331 f" tests/data/expression.diff with {dump}"
333 self.assertEqual(expected, actual, msg)
335 def test_expression_diff_with_color(self) -> None:
336 source, _ = read_data("expression.py")
337 config = THIS_DIR / "data" / "empty_pyproject.toml"
338 expected, _ = read_data("expression.diff")
339 tmp_file = Path(black.dump_to_file(source))
341 result = BlackRunner().invoke(
342 black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
346 actual = result.output
347 # We check the contents of the diff in `test_expression_diff`. All
348 # we need to check here is that color codes exist in the result.
349 self.assertIn("\033[1;37m", actual)
350 self.assertIn("\033[36m", actual)
351 self.assertIn("\033[32m", actual)
352 self.assertIn("\033[31m", actual)
353 self.assertIn("\033[0m", actual)
355 @patch("black.dump_to_file", dump_to_stderr)
356 def test_pep_570(self) -> None:
357 source, expected = read_data("pep_570")
359 self.assertFormatEqual(expected, actual)
360 black.assert_stable(source, actual, DEFAULT_MODE)
361 if sys.version_info >= (3, 8):
362 black.assert_equivalent(source, actual)
364 def test_detect_pos_only_arguments(self) -> None:
365 source, _ = read_data("pep_570")
366 root = black.lib2to3_parse(source)
367 features = black.get_features_used(root)
368 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
369 versions = black.detect_target_versions(root)
370 self.assertIn(black.TargetVersion.PY38, versions)
372 @patch("black.dump_to_file", dump_to_stderr)
373 def test_string_quotes(self) -> None:
374 source, expected = read_data("string_quotes")
375 mode = black.Mode(experimental_string_processing=True)
376 actual = fs(source, mode=mode)
377 self.assertFormatEqual(expected, actual)
378 black.assert_equivalent(source, actual)
379 black.assert_stable(source, actual, mode)
380 mode = replace(mode, string_normalization=False)
381 not_normalized = fs(source, mode=mode)
382 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
383 black.assert_equivalent(source, not_normalized)
384 black.assert_stable(source, not_normalized, mode=mode)
386 @patch("black.dump_to_file", dump_to_stderr)
387 def test_docstring_no_string_normalization(self) -> None:
388 """Like test_docstring but with string normalization off."""
389 source, expected = read_data("docstring_no_string_normalization")
390 mode = replace(DEFAULT_MODE, string_normalization=False)
391 actual = fs(source, mode=mode)
392 self.assertFormatEqual(expected, actual)
393 black.assert_equivalent(source, actual)
394 black.assert_stable(source, actual, mode)
396 def test_long_strings_flag_disabled(self) -> None:
397 """Tests for turning off the string processing logic."""
398 source, expected = read_data("long_strings_flag_disabled")
399 mode = replace(DEFAULT_MODE, experimental_string_processing=False)
400 actual = fs(source, mode=mode)
401 self.assertFormatEqual(expected, actual)
402 black.assert_stable(expected, actual, mode)
404 @patch("black.dump_to_file", dump_to_stderr)
405 def test_numeric_literals(self) -> None:
406 source, expected = read_data("numeric_literals")
407 mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
408 actual = fs(source, mode=mode)
409 self.assertFormatEqual(expected, actual)
410 black.assert_equivalent(source, actual)
411 black.assert_stable(source, actual, mode)
413 @patch("black.dump_to_file", dump_to_stderr)
414 def test_numeric_literals_ignoring_underscores(self) -> None:
415 source, expected = read_data("numeric_literals_skip_underscores")
416 mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
417 actual = fs(source, mode=mode)
418 self.assertFormatEqual(expected, actual)
419 black.assert_equivalent(source, actual)
420 black.assert_stable(source, actual, mode)
422 def test_skip_magic_trailing_comma(self) -> None:
423 source, _ = read_data("expression.py")
424 expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
425 tmp_file = Path(black.dump_to_file(source))
426 diff_header = re.compile(
427 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
428 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
431 result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
432 self.assertEqual(result.exit_code, 0)
435 actual = result.output
436 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
437 actual = actual.rstrip() + "\n" # the diff output has a trailing space
438 if expected != actual:
439 dump = black.dump_to_file(actual)
441 "Expected diff isn't equal to the actual. If you made changes to"
442 " expression.py and this is an anticipated difference, overwrite"
443 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
445 self.assertEqual(expected, actual, msg)
447 @pytest.mark.no_python2
448 def test_python2_should_fail_without_optional_install(self) -> None:
449 if sys.version_info < (3, 8):
451 "Python 3.6 and 3.7 will install typed-ast to work and as such will be"
452 " able to parse Python 2 syntax without explicitly specifying the"
457 tmp_file = Path(black.dump_to_file(source))
459 runner = BlackRunner()
460 result = runner.invoke(black.main, [str(tmp_file)])
461 self.assertEqual(result.exit_code, 123)
465 result.stderr_bytes.decode()
472 "The requested source code has invalid Python 3 syntax."
473 "If you are trying to format Python 2 files please reinstall Black"
474 " with the 'python2' extra: `python3 -m pip install black[python2]`."
476 self.assertIn(msg, actual)
479 @patch("black.dump_to_file", dump_to_stderr)
480 def test_python2_print_function(self) -> None:
481 source, expected = read_data("python2_print_function")
482 mode = replace(DEFAULT_MODE, target_versions={TargetVersion.PY27})
483 actual = fs(source, mode=mode)
484 self.assertFormatEqual(expected, actual)
485 black.assert_equivalent(source, actual)
486 black.assert_stable(source, actual, mode)
488 @patch("black.dump_to_file", dump_to_stderr)
489 def test_stub(self) -> None:
490 mode = replace(DEFAULT_MODE, is_pyi=True)
491 source, expected = read_data("stub.pyi")
492 actual = fs(source, mode=mode)
493 self.assertFormatEqual(expected, actual)
494 black.assert_stable(source, actual, mode)
496 @patch("black.dump_to_file", dump_to_stderr)
497 def test_async_as_identifier(self) -> None:
498 source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
499 source, expected = read_data("async_as_identifier")
501 self.assertFormatEqual(expected, actual)
502 major, minor = sys.version_info[:2]
503 if major < 3 or (major <= 3 and minor < 7):
504 black.assert_equivalent(source, actual)
505 black.assert_stable(source, actual, DEFAULT_MODE)
506 # ensure black can parse this when the target is 3.6
507 self.invokeBlack([str(source_path), "--target-version", "py36"])
508 # but not on 3.7, because async/await is no longer an identifier
509 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
511 @patch("black.dump_to_file", dump_to_stderr)
512 def test_python37(self) -> None:
513 source_path = (THIS_DIR / "data" / "python37.py").resolve()
514 source, expected = read_data("python37")
516 self.assertFormatEqual(expected, actual)
517 major, minor = sys.version_info[:2]
518 if major > 3 or (major == 3 and minor >= 7):
519 black.assert_equivalent(source, actual)
520 black.assert_stable(source, actual, DEFAULT_MODE)
521 # ensure black can parse this when the target is 3.7
522 self.invokeBlack([str(source_path), "--target-version", "py37"])
523 # but not on 3.6, because we use async as a reserved keyword
524 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
526 @patch("black.dump_to_file", dump_to_stderr)
527 def test_python38(self) -> None:
528 source, expected = read_data("python38")
530 self.assertFormatEqual(expected, actual)
531 major, minor = sys.version_info[:2]
532 if major > 3 or (major == 3 and minor >= 8):
533 black.assert_equivalent(source, actual)
534 black.assert_stable(source, actual, DEFAULT_MODE)
536 @patch("black.dump_to_file", dump_to_stderr)
537 def test_python39(self) -> None:
538 source, expected = read_data("python39")
540 self.assertFormatEqual(expected, actual)
541 major, minor = sys.version_info[:2]
542 if major > 3 or (major == 3 and minor >= 9):
543 black.assert_equivalent(source, actual)
544 black.assert_stable(source, actual, DEFAULT_MODE)
546 def test_tab_comment_indentation(self) -> None:
547 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
548 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
549 self.assertFormatEqual(contents_spc, fs(contents_spc))
550 self.assertFormatEqual(contents_spc, fs(contents_tab))
552 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
553 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
554 self.assertFormatEqual(contents_spc, fs(contents_spc))
555 self.assertFormatEqual(contents_spc, fs(contents_tab))
557 # mixed tabs and spaces (valid Python 2 code)
558 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
559 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
560 self.assertFormatEqual(contents_spc, fs(contents_spc))
561 self.assertFormatEqual(contents_spc, fs(contents_tab))
563 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
564 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
565 self.assertFormatEqual(contents_spc, fs(contents_spc))
566 self.assertFormatEqual(contents_spc, fs(contents_tab))
568 def test_report_verbose(self) -> None:
569 report = Report(verbose=True)
573 def out(msg: str, **kwargs: Any) -> None:
574 out_lines.append(msg)
576 def err(msg: str, **kwargs: Any) -> None:
577 err_lines.append(msg)
579 with patch("black.output._out", out), patch("black.output._err", err):
580 report.done(Path("f1"), black.Changed.NO)
581 self.assertEqual(len(out_lines), 1)
582 self.assertEqual(len(err_lines), 0)
583 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
584 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
585 self.assertEqual(report.return_code, 0)
586 report.done(Path("f2"), black.Changed.YES)
587 self.assertEqual(len(out_lines), 2)
588 self.assertEqual(len(err_lines), 0)
589 self.assertEqual(out_lines[-1], "reformatted f2")
591 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
593 report.done(Path("f3"), black.Changed.CACHED)
594 self.assertEqual(len(out_lines), 3)
595 self.assertEqual(len(err_lines), 0)
597 out_lines[-1], "f3 wasn't modified on disk since last run."
600 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
602 self.assertEqual(report.return_code, 0)
604 self.assertEqual(report.return_code, 1)
606 report.failed(Path("e1"), "boom")
607 self.assertEqual(len(out_lines), 3)
608 self.assertEqual(len(err_lines), 1)
609 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
611 unstyle(str(report)),
612 "1 file reformatted, 2 files left unchanged, 1 file failed to"
615 self.assertEqual(report.return_code, 123)
616 report.done(Path("f3"), black.Changed.YES)
617 self.assertEqual(len(out_lines), 4)
618 self.assertEqual(len(err_lines), 1)
619 self.assertEqual(out_lines[-1], "reformatted f3")
621 unstyle(str(report)),
622 "2 files reformatted, 2 files left unchanged, 1 file failed to"
625 self.assertEqual(report.return_code, 123)
626 report.failed(Path("e2"), "boom")
627 self.assertEqual(len(out_lines), 4)
628 self.assertEqual(len(err_lines), 2)
629 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
631 unstyle(str(report)),
632 "2 files reformatted, 2 files left unchanged, 2 files failed to"
635 self.assertEqual(report.return_code, 123)
636 report.path_ignored(Path("wat"), "no match")
637 self.assertEqual(len(out_lines), 5)
638 self.assertEqual(len(err_lines), 2)
639 self.assertEqual(out_lines[-1], "wat ignored: no match")
641 unstyle(str(report)),
642 "2 files reformatted, 2 files left unchanged, 2 files failed to"
645 self.assertEqual(report.return_code, 123)
646 report.done(Path("f4"), black.Changed.NO)
647 self.assertEqual(len(out_lines), 6)
648 self.assertEqual(len(err_lines), 2)
649 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
651 unstyle(str(report)),
652 "2 files reformatted, 3 files left unchanged, 2 files failed to"
655 self.assertEqual(report.return_code, 123)
658 unstyle(str(report)),
659 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
660 " would fail to reformat.",
665 unstyle(str(report)),
666 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
667 " would fail to reformat.",
670 def test_report_quiet(self) -> None:
671 report = Report(quiet=True)
675 def out(msg: str, **kwargs: Any) -> None:
676 out_lines.append(msg)
678 def err(msg: str, **kwargs: Any) -> None:
679 err_lines.append(msg)
681 with patch("black.output._out", out), patch("black.output._err", err):
682 report.done(Path("f1"), black.Changed.NO)
683 self.assertEqual(len(out_lines), 0)
684 self.assertEqual(len(err_lines), 0)
685 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
686 self.assertEqual(report.return_code, 0)
687 report.done(Path("f2"), black.Changed.YES)
688 self.assertEqual(len(out_lines), 0)
689 self.assertEqual(len(err_lines), 0)
691 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
693 report.done(Path("f3"), black.Changed.CACHED)
694 self.assertEqual(len(out_lines), 0)
695 self.assertEqual(len(err_lines), 0)
697 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
699 self.assertEqual(report.return_code, 0)
701 self.assertEqual(report.return_code, 1)
703 report.failed(Path("e1"), "boom")
704 self.assertEqual(len(out_lines), 0)
705 self.assertEqual(len(err_lines), 1)
706 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
708 unstyle(str(report)),
709 "1 file reformatted, 2 files left unchanged, 1 file failed to"
712 self.assertEqual(report.return_code, 123)
713 report.done(Path("f3"), black.Changed.YES)
714 self.assertEqual(len(out_lines), 0)
715 self.assertEqual(len(err_lines), 1)
717 unstyle(str(report)),
718 "2 files reformatted, 2 files left unchanged, 1 file failed to"
721 self.assertEqual(report.return_code, 123)
722 report.failed(Path("e2"), "boom")
723 self.assertEqual(len(out_lines), 0)
724 self.assertEqual(len(err_lines), 2)
725 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
727 unstyle(str(report)),
728 "2 files reformatted, 2 files left unchanged, 2 files failed to"
731 self.assertEqual(report.return_code, 123)
732 report.path_ignored(Path("wat"), "no match")
733 self.assertEqual(len(out_lines), 0)
734 self.assertEqual(len(err_lines), 2)
736 unstyle(str(report)),
737 "2 files reformatted, 2 files left unchanged, 2 files failed to"
740 self.assertEqual(report.return_code, 123)
741 report.done(Path("f4"), black.Changed.NO)
742 self.assertEqual(len(out_lines), 0)
743 self.assertEqual(len(err_lines), 2)
745 unstyle(str(report)),
746 "2 files reformatted, 3 files left unchanged, 2 files failed to"
749 self.assertEqual(report.return_code, 123)
752 unstyle(str(report)),
753 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
754 " would fail to reformat.",
759 unstyle(str(report)),
760 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
761 " would fail to reformat.",
764 def test_report_normal(self) -> None:
765 report = black.Report()
769 def out(msg: str, **kwargs: Any) -> None:
770 out_lines.append(msg)
772 def err(msg: str, **kwargs: Any) -> None:
773 err_lines.append(msg)
775 with patch("black.output._out", out), patch("black.output._err", err):
776 report.done(Path("f1"), black.Changed.NO)
777 self.assertEqual(len(out_lines), 0)
778 self.assertEqual(len(err_lines), 0)
779 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
780 self.assertEqual(report.return_code, 0)
781 report.done(Path("f2"), black.Changed.YES)
782 self.assertEqual(len(out_lines), 1)
783 self.assertEqual(len(err_lines), 0)
784 self.assertEqual(out_lines[-1], "reformatted f2")
786 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
788 report.done(Path("f3"), black.Changed.CACHED)
789 self.assertEqual(len(out_lines), 1)
790 self.assertEqual(len(err_lines), 0)
791 self.assertEqual(out_lines[-1], "reformatted f2")
793 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
795 self.assertEqual(report.return_code, 0)
797 self.assertEqual(report.return_code, 1)
799 report.failed(Path("e1"), "boom")
800 self.assertEqual(len(out_lines), 1)
801 self.assertEqual(len(err_lines), 1)
802 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
804 unstyle(str(report)),
805 "1 file reformatted, 2 files left unchanged, 1 file failed to"
808 self.assertEqual(report.return_code, 123)
809 report.done(Path("f3"), black.Changed.YES)
810 self.assertEqual(len(out_lines), 2)
811 self.assertEqual(len(err_lines), 1)
812 self.assertEqual(out_lines[-1], "reformatted f3")
814 unstyle(str(report)),
815 "2 files reformatted, 2 files left unchanged, 1 file failed to"
818 self.assertEqual(report.return_code, 123)
819 report.failed(Path("e2"), "boom")
820 self.assertEqual(len(out_lines), 2)
821 self.assertEqual(len(err_lines), 2)
822 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
824 unstyle(str(report)),
825 "2 files reformatted, 2 files left unchanged, 2 files failed to"
828 self.assertEqual(report.return_code, 123)
829 report.path_ignored(Path("wat"), "no match")
830 self.assertEqual(len(out_lines), 2)
831 self.assertEqual(len(err_lines), 2)
833 unstyle(str(report)),
834 "2 files reformatted, 2 files left unchanged, 2 files failed to"
837 self.assertEqual(report.return_code, 123)
838 report.done(Path("f4"), black.Changed.NO)
839 self.assertEqual(len(out_lines), 2)
840 self.assertEqual(len(err_lines), 2)
842 unstyle(str(report)),
843 "2 files reformatted, 3 files left unchanged, 2 files failed to"
846 self.assertEqual(report.return_code, 123)
849 unstyle(str(report)),
850 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
851 " would fail to reformat.",
856 unstyle(str(report)),
857 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
858 " would fail to reformat.",
861 def test_lib2to3_parse(self) -> None:
862 with self.assertRaises(black.InvalidInput):
863 black.lib2to3_parse("invalid syntax")
866 black.lib2to3_parse(straddling)
867 black.lib2to3_parse(straddling, {TargetVersion.PY27})
868 black.lib2to3_parse(straddling, {TargetVersion.PY36})
869 black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36})
872 black.lib2to3_parse(py2_only)
873 black.lib2to3_parse(py2_only, {TargetVersion.PY27})
874 with self.assertRaises(black.InvalidInput):
875 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
876 with self.assertRaises(black.InvalidInput):
877 black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36})
879 py3_only = "exec(x, end=y)"
880 black.lib2to3_parse(py3_only)
881 with self.assertRaises(black.InvalidInput):
882 black.lib2to3_parse(py3_only, {TargetVersion.PY27})
883 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
884 black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36})
886 def test_get_features_used_decorator(self) -> None:
887 # Test the feature detection of new decorator syntax
888 # since this makes some test cases of test_get_features_used()
889 # fails if it fails, this is tested first so that a useful case
891 simples, relaxed = read_data("decorators")
892 # skip explanation comments at the top of the file
893 for simple_test in simples.split("##")[1:]:
894 node = black.lib2to3_parse(simple_test)
895 decorator = str(node.children[0].children[0]).strip()
897 Feature.RELAXED_DECORATORS,
898 black.get_features_used(node),
900 f"decorator '{decorator}' follows python<=3.8 syntax"
901 "but is detected as 3.9+"
902 # f"The full node is\n{node!r}"
905 # skip the '# output' comment at the top of the output part
906 for relaxed_test in relaxed.split("##")[1:]:
907 node = black.lib2to3_parse(relaxed_test)
908 decorator = str(node.children[0].children[0]).strip()
910 Feature.RELAXED_DECORATORS,
911 black.get_features_used(node),
913 f"decorator '{decorator}' uses python3.9+ syntax"
914 "but is detected as python<=3.8"
915 # f"The full node is\n{node!r}"
919 def test_get_features_used(self) -> None:
920 node = black.lib2to3_parse("def f(*, arg): ...\n")
921 self.assertEqual(black.get_features_used(node), set())
922 node = black.lib2to3_parse("def f(*, arg,): ...\n")
923 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
924 node = black.lib2to3_parse("f(*arg,)\n")
926 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
928 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
929 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
930 node = black.lib2to3_parse("123_456\n")
931 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
932 node = black.lib2to3_parse("123456\n")
933 self.assertEqual(black.get_features_used(node), set())
934 source, expected = read_data("function")
935 node = black.lib2to3_parse(source)
936 expected_features = {
937 Feature.TRAILING_COMMA_IN_CALL,
938 Feature.TRAILING_COMMA_IN_DEF,
941 self.assertEqual(black.get_features_used(node), expected_features)
942 node = black.lib2to3_parse(expected)
943 self.assertEqual(black.get_features_used(node), expected_features)
944 source, expected = read_data("expression")
945 node = black.lib2to3_parse(source)
946 self.assertEqual(black.get_features_used(node), set())
947 node = black.lib2to3_parse(expected)
948 self.assertEqual(black.get_features_used(node), set())
950 def test_get_future_imports(self) -> None:
951 node = black.lib2to3_parse("\n")
952 self.assertEqual(set(), black.get_future_imports(node))
953 node = black.lib2to3_parse("from __future__ import black\n")
954 self.assertEqual({"black"}, black.get_future_imports(node))
955 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
956 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
957 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
958 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
959 node = black.lib2to3_parse(
960 "from __future__ import multiple\nfrom __future__ import imports\n"
962 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
963 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
964 self.assertEqual({"black"}, black.get_future_imports(node))
965 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
966 self.assertEqual({"black"}, black.get_future_imports(node))
967 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
968 self.assertEqual(set(), black.get_future_imports(node))
969 node = black.lib2to3_parse("from some.module import black\n")
970 self.assertEqual(set(), black.get_future_imports(node))
971 node = black.lib2to3_parse(
972 "from __future__ import unicode_literals as _unicode_literals"
974 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
975 node = black.lib2to3_parse(
976 "from __future__ import unicode_literals as _lol, print"
978 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
980 def test_debug_visitor(self) -> None:
981 source, _ = read_data("debug_visitor.py")
982 expected, _ = read_data("debug_visitor.out")
986 def out(msg: str, **kwargs: Any) -> None:
987 out_lines.append(msg)
989 def err(msg: str, **kwargs: Any) -> None:
990 err_lines.append(msg)
992 with patch("black.debug.out", out):
993 DebugVisitor.show(source)
994 actual = "\n".join(out_lines) + "\n"
996 if expected != actual:
997 log_name = black.dump_to_file(*out_lines)
1001 f"AST print out is different. Actual version dumped to {log_name}",
1004 def test_format_file_contents(self) -> None:
1007 with self.assertRaises(black.NothingChanged):
1008 black.format_file_contents(empty, mode=mode, fast=False)
1010 with self.assertRaises(black.NothingChanged):
1011 black.format_file_contents(just_nl, mode=mode, fast=False)
1012 same = "j = [1, 2, 3]\n"
1013 with self.assertRaises(black.NothingChanged):
1014 black.format_file_contents(same, mode=mode, fast=False)
1015 different = "j = [1,2,3]"
1017 actual = black.format_file_contents(different, mode=mode, fast=False)
1018 self.assertEqual(expected, actual)
1019 invalid = "return if you can"
1020 with self.assertRaises(black.InvalidInput) as e:
1021 black.format_file_contents(invalid, mode=mode, fast=False)
1022 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
1024 def test_endmarker(self) -> None:
1025 n = black.lib2to3_parse("\n")
1026 self.assertEqual(n.type, black.syms.file_input)
1027 self.assertEqual(len(n.children), 1)
1028 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
1030 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
1031 def test_assertFormatEqual(self) -> None:
1035 def out(msg: str, **kwargs: Any) -> None:
1036 out_lines.append(msg)
1038 def err(msg: str, **kwargs: Any) -> None:
1039 err_lines.append(msg)
1041 with patch("black.output._out", out), patch("black.output._err", err):
1042 with self.assertRaises(AssertionError):
1043 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
1045 out_str = "".join(out_lines)
1046 self.assertTrue("Expected tree:" in out_str)
1047 self.assertTrue("Actual tree:" in out_str)
1048 self.assertEqual("".join(err_lines), "")
1050 def test_cache_broken_file(self) -> None:
1052 with cache_dir() as workspace:
1053 cache_file = get_cache_file(mode)
1054 with cache_file.open("w") as fobj:
1055 fobj.write("this is not a pickle")
1056 self.assertEqual(black.read_cache(mode), {})
1057 src = (workspace / "test.py").resolve()
1058 with src.open("w") as fobj:
1059 fobj.write("print('hello')")
1060 self.invokeBlack([str(src)])
1061 cache = black.read_cache(mode)
1062 self.assertIn(str(src), cache)
1064 def test_cache_single_file_already_cached(self) -> None:
1066 with cache_dir() as workspace:
1067 src = (workspace / "test.py").resolve()
1068 with src.open("w") as fobj:
1069 fobj.write("print('hello')")
1070 black.write_cache({}, [src], mode)
1071 self.invokeBlack([str(src)])
1072 with src.open("r") as fobj:
1073 self.assertEqual(fobj.read(), "print('hello')")
1076 def test_cache_multiple_files(self) -> None:
1078 with cache_dir() as workspace, patch(
1079 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1081 one = (workspace / "one.py").resolve()
1082 with one.open("w") as fobj:
1083 fobj.write("print('hello')")
1084 two = (workspace / "two.py").resolve()
1085 with two.open("w") as fobj:
1086 fobj.write("print('hello')")
1087 black.write_cache({}, [one], mode)
1088 self.invokeBlack([str(workspace)])
1089 with one.open("r") as fobj:
1090 self.assertEqual(fobj.read(), "print('hello')")
1091 with two.open("r") as fobj:
1092 self.assertEqual(fobj.read(), 'print("hello")\n')
1093 cache = black.read_cache(mode)
1094 self.assertIn(str(one), cache)
1095 self.assertIn(str(two), cache)
1097 def test_no_cache_when_writeback_diff(self) -> None:
1099 with cache_dir() as workspace:
1100 src = (workspace / "test.py").resolve()
1101 with src.open("w") as fobj:
1102 fobj.write("print('hello')")
1103 with patch("black.read_cache") as read_cache, patch(
1106 self.invokeBlack([str(src), "--diff"])
1107 cache_file = get_cache_file(mode)
1108 self.assertFalse(cache_file.exists())
1109 write_cache.assert_not_called()
1110 read_cache.assert_not_called()
1112 def test_no_cache_when_writeback_color_diff(self) -> None:
1114 with cache_dir() as workspace:
1115 src = (workspace / "test.py").resolve()
1116 with src.open("w") as fobj:
1117 fobj.write("print('hello')")
1118 with patch("black.read_cache") as read_cache, patch(
1121 self.invokeBlack([str(src), "--diff", "--color"])
1122 cache_file = get_cache_file(mode)
1123 self.assertFalse(cache_file.exists())
1124 write_cache.assert_not_called()
1125 read_cache.assert_not_called()
1128 def test_output_locking_when_writeback_diff(self) -> None:
1129 with cache_dir() as workspace:
1130 for tag in range(0, 4):
1131 src = (workspace / f"test{tag}.py").resolve()
1132 with src.open("w") as fobj:
1133 fobj.write("print('hello')")
1134 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1135 self.invokeBlack(["--diff", str(workspace)], exit_code=0)
1136 # this isn't quite doing what we want, but if it _isn't_
1137 # called then we cannot be using the lock it provides
1141 def test_output_locking_when_writeback_color_diff(self) -> None:
1142 with cache_dir() as workspace:
1143 for tag in range(0, 4):
1144 src = (workspace / f"test{tag}.py").resolve()
1145 with src.open("w") as fobj:
1146 fobj.write("print('hello')")
1147 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1148 self.invokeBlack(["--diff", "--color", str(workspace)], exit_code=0)
1149 # this isn't quite doing what we want, but if it _isn't_
1150 # called then we cannot be using the lock it provides
1153 def test_no_cache_when_stdin(self) -> None:
1156 result = CliRunner().invoke(
1157 black.main, ["-"], input=BytesIO(b"print('hello')")
1159 self.assertEqual(result.exit_code, 0)
1160 cache_file = get_cache_file(mode)
1161 self.assertFalse(cache_file.exists())
1163 def test_read_cache_no_cachefile(self) -> None:
1166 self.assertEqual(black.read_cache(mode), {})
1168 def test_write_cache_read_cache(self) -> None:
1170 with cache_dir() as workspace:
1171 src = (workspace / "test.py").resolve()
1173 black.write_cache({}, [src], mode)
1174 cache = black.read_cache(mode)
1175 self.assertIn(str(src), cache)
1176 self.assertEqual(cache[str(src)], black.get_cache_info(src))
1178 def test_filter_cached(self) -> None:
1179 with TemporaryDirectory() as workspace:
1180 path = Path(workspace)
1181 uncached = (path / "uncached").resolve()
1182 cached = (path / "cached").resolve()
1183 cached_but_changed = (path / "changed").resolve()
1186 cached_but_changed.touch()
1188 str(cached): black.get_cache_info(cached),
1189 str(cached_but_changed): (0.0, 0),
1191 todo, done = black.filter_cached(
1192 cache, {uncached, cached, cached_but_changed}
1194 self.assertEqual(todo, {uncached, cached_but_changed})
1195 self.assertEqual(done, {cached})
1197 def test_write_cache_creates_directory_if_needed(self) -> None:
1199 with cache_dir(exists=False) as workspace:
1200 self.assertFalse(workspace.exists())
1201 black.write_cache({}, [], mode)
1202 self.assertTrue(workspace.exists())
1205 def test_failed_formatting_does_not_get_cached(self) -> None:
1207 with cache_dir() as workspace, patch(
1208 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1210 failing = (workspace / "failing.py").resolve()
1211 with failing.open("w") as fobj:
1212 fobj.write("not actually python")
1213 clean = (workspace / "clean.py").resolve()
1214 with clean.open("w") as fobj:
1215 fobj.write('print("hello")\n')
1216 self.invokeBlack([str(workspace)], exit_code=123)
1217 cache = black.read_cache(mode)
1218 self.assertNotIn(str(failing), cache)
1219 self.assertIn(str(clean), cache)
1221 def test_write_cache_write_fail(self) -> None:
1223 with cache_dir(), patch.object(Path, "open") as mock:
1224 mock.side_effect = OSError
1225 black.write_cache({}, [], mode)
1228 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1229 def test_works_in_mono_process_only_environment(self) -> None:
1230 with cache_dir() as workspace:
1232 (workspace / "one.py").resolve(),
1233 (workspace / "two.py").resolve(),
1235 f.write_text('print("hello")\n')
1236 self.invokeBlack([str(workspace)])
1239 def test_check_diff_use_together(self) -> None:
1241 # Files which will be reformatted.
1242 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
1243 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1244 # Files which will not be reformatted.
1245 src2 = (THIS_DIR / "data" / "composition.py").resolve()
1246 self.invokeBlack([str(src2), "--diff", "--check"])
1247 # Multi file command.
1248 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1250 def test_no_files(self) -> None:
1252 # Without an argument, black exits with error code 0.
1253 self.invokeBlack([])
1255 def test_broken_symlink(self) -> None:
1256 with cache_dir() as workspace:
1257 symlink = workspace / "broken_link.py"
1259 symlink.symlink_to("nonexistent.py")
1260 except OSError as e:
1261 self.skipTest(f"Can't create symlinks: {e}")
1262 self.invokeBlack([str(workspace.resolve())])
1264 def test_read_cache_line_lengths(self) -> None:
1266 short_mode = replace(DEFAULT_MODE, line_length=1)
1267 with cache_dir() as workspace:
1268 path = (workspace / "file.py").resolve()
1270 black.write_cache({}, [path], mode)
1271 one = black.read_cache(mode)
1272 self.assertIn(str(path), one)
1273 two = black.read_cache(short_mode)
1274 self.assertNotIn(str(path), two)
1276 def test_single_file_force_pyi(self) -> None:
1277 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1278 contents, expected = read_data("force_pyi")
1279 with cache_dir() as workspace:
1280 path = (workspace / "file.py").resolve()
1281 with open(path, "w") as fh:
1283 self.invokeBlack([str(path), "--pyi"])
1284 with open(path, "r") as fh:
1286 # verify cache with --pyi is separate
1287 pyi_cache = black.read_cache(pyi_mode)
1288 self.assertIn(str(path), pyi_cache)
1289 normal_cache = black.read_cache(DEFAULT_MODE)
1290 self.assertNotIn(str(path), normal_cache)
1291 self.assertFormatEqual(expected, actual)
1292 black.assert_equivalent(contents, actual)
1293 black.assert_stable(contents, actual, pyi_mode)
1296 def test_multi_file_force_pyi(self) -> None:
1297 reg_mode = DEFAULT_MODE
1298 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1299 contents, expected = read_data("force_pyi")
1300 with cache_dir() as workspace:
1302 (workspace / "file1.py").resolve(),
1303 (workspace / "file2.py").resolve(),
1306 with open(path, "w") as fh:
1308 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1310 with open(path, "r") as fh:
1312 self.assertEqual(actual, expected)
1313 # verify cache with --pyi is separate
1314 pyi_cache = black.read_cache(pyi_mode)
1315 normal_cache = black.read_cache(reg_mode)
1317 self.assertIn(str(path), pyi_cache)
1318 self.assertNotIn(str(path), normal_cache)
1320 def test_pipe_force_pyi(self) -> None:
1321 source, expected = read_data("force_pyi")
1322 result = CliRunner().invoke(
1323 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1325 self.assertEqual(result.exit_code, 0)
1326 actual = result.output
1327 self.assertFormatEqual(actual, expected)
1329 def test_single_file_force_py36(self) -> None:
1330 reg_mode = DEFAULT_MODE
1331 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1332 source, expected = read_data("force_py36")
1333 with cache_dir() as workspace:
1334 path = (workspace / "file.py").resolve()
1335 with open(path, "w") as fh:
1337 self.invokeBlack([str(path), *PY36_ARGS])
1338 with open(path, "r") as fh:
1340 # verify cache with --target-version is separate
1341 py36_cache = black.read_cache(py36_mode)
1342 self.assertIn(str(path), py36_cache)
1343 normal_cache = black.read_cache(reg_mode)
1344 self.assertNotIn(str(path), normal_cache)
1345 self.assertEqual(actual, expected)
1348 def test_multi_file_force_py36(self) -> None:
1349 reg_mode = DEFAULT_MODE
1350 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1351 source, expected = read_data("force_py36")
1352 with cache_dir() as workspace:
1354 (workspace / "file1.py").resolve(),
1355 (workspace / "file2.py").resolve(),
1358 with open(path, "w") as fh:
1360 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1362 with open(path, "r") as fh:
1364 self.assertEqual(actual, expected)
1365 # verify cache with --target-version is separate
1366 pyi_cache = black.read_cache(py36_mode)
1367 normal_cache = black.read_cache(reg_mode)
1369 self.assertIn(str(path), pyi_cache)
1370 self.assertNotIn(str(path), normal_cache)
1372 def test_pipe_force_py36(self) -> None:
1373 source, expected = read_data("force_py36")
1374 result = CliRunner().invoke(
1376 ["-", "-q", "--target-version=py36"],
1377 input=BytesIO(source.encode("utf8")),
1379 self.assertEqual(result.exit_code, 0)
1380 actual = result.output
1381 self.assertFormatEqual(actual, expected)
1383 def test_include_exclude(self) -> None:
1384 path = THIS_DIR / "data" / "include_exclude_tests"
1385 include = re.compile(r"\.pyi?$")
1386 exclude = re.compile(r"/exclude/|/\.definitely_exclude/")
1387 report = black.Report()
1388 gitignore = PathSpec.from_lines("gitwildmatch", [])
1389 sources: List[Path] = []
1391 Path(path / "b/dont_exclude/a.py"),
1392 Path(path / "b/dont_exclude/a.pyi"),
1394 this_abs = THIS_DIR.resolve()
1396 black.gen_python_files(
1407 self.assertEqual(sorted(expected), sorted(sources))
1409 def test_gitingore_used_as_default(self) -> None:
1410 path = Path(THIS_DIR / "data" / "include_exclude_tests")
1411 include = re.compile(r"\.pyi?$")
1412 extend_exclude = re.compile(r"/exclude/")
1413 src = str(path / "b/")
1414 report = black.Report()
1415 expected: List[Path] = [
1416 path / "b/.definitely_exclude/a.py",
1417 path / "b/.definitely_exclude/a.pyi",
1427 extend_exclude=extend_exclude,
1430 stdin_filename=None,
1433 self.assertEqual(sorted(expected), sorted(sources))
1435 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1436 def test_exclude_for_issue_1572(self) -> None:
1437 # Exclude shouldn't touch files that were explicitly given to Black through the
1438 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1439 # https://github.com/psf/black/issues/1572
1440 path = THIS_DIR / "data" / "include_exclude_tests"
1442 exclude = r"/exclude/|a\.py"
1443 src = str(path / "b/exclude/a.py")
1444 report = black.Report()
1445 expected = [Path(path / "b/exclude/a.py")]
1452 include=re.compile(include),
1453 exclude=re.compile(exclude),
1454 extend_exclude=None,
1457 stdin_filename=None,
1460 self.assertEqual(sorted(expected), sorted(sources))
1462 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1463 def test_get_sources_with_stdin(self) -> None:
1465 exclude = r"/exclude/|a\.py"
1467 report = black.Report()
1468 expected = [Path("-")]
1475 include=re.compile(include),
1476 exclude=re.compile(exclude),
1477 extend_exclude=None,
1480 stdin_filename=None,
1483 self.assertEqual(sorted(expected), sorted(sources))
1485 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1486 def test_get_sources_with_stdin_filename(self) -> None:
1488 exclude = r"/exclude/|a\.py"
1490 report = black.Report()
1491 stdin_filename = str(THIS_DIR / "data/collections.py")
1492 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1499 include=re.compile(include),
1500 exclude=re.compile(exclude),
1501 extend_exclude=None,
1504 stdin_filename=stdin_filename,
1507 self.assertEqual(sorted(expected), sorted(sources))
1509 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1510 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
1511 # Exclude shouldn't exclude stdin_filename since it is mimicking the
1512 # file being passed directly. This is the same as
1513 # test_exclude_for_issue_1572
1514 path = THIS_DIR / "data" / "include_exclude_tests"
1516 exclude = r"/exclude/|a\.py"
1518 report = black.Report()
1519 stdin_filename = str(path / "b/exclude/a.py")
1520 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1527 include=re.compile(include),
1528 exclude=re.compile(exclude),
1529 extend_exclude=None,
1532 stdin_filename=stdin_filename,
1535 self.assertEqual(sorted(expected), sorted(sources))
1537 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1538 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
1539 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
1540 # file being passed directly. This is the same as
1541 # test_exclude_for_issue_1572
1542 path = THIS_DIR / "data" / "include_exclude_tests"
1544 extend_exclude = r"/exclude/|a\.py"
1546 report = black.Report()
1547 stdin_filename = str(path / "b/exclude/a.py")
1548 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1555 include=re.compile(include),
1556 exclude=re.compile(""),
1557 extend_exclude=re.compile(extend_exclude),
1560 stdin_filename=stdin_filename,
1563 self.assertEqual(sorted(expected), sorted(sources))
1565 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1566 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
1567 # Force exclude should exclude the file when passing it through
1569 path = THIS_DIR / "data" / "include_exclude_tests"
1571 force_exclude = r"/exclude/|a\.py"
1573 report = black.Report()
1574 stdin_filename = str(path / "b/exclude/a.py")
1581 include=re.compile(include),
1582 exclude=re.compile(""),
1583 extend_exclude=None,
1584 force_exclude=re.compile(force_exclude),
1586 stdin_filename=stdin_filename,
1589 self.assertEqual([], sorted(sources))
1591 def test_reformat_one_with_stdin(self) -> None:
1593 "black.format_stdin_to_stdout",
1594 return_value=lambda *args, **kwargs: black.Changed.YES,
1596 report = MagicMock()
1601 write_back=black.WriteBack.YES,
1605 fsts.assert_called_once()
1606 report.done.assert_called_with(path, black.Changed.YES)
1608 def test_reformat_one_with_stdin_filename(self) -> None:
1610 "black.format_stdin_to_stdout",
1611 return_value=lambda *args, **kwargs: black.Changed.YES,
1613 report = MagicMock()
1615 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1620 write_back=black.WriteBack.YES,
1624 fsts.assert_called_once_with(
1625 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1627 # __BLACK_STDIN_FILENAME__ should have been stripped
1628 report.done.assert_called_with(expected, black.Changed.YES)
1630 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1632 "black.format_stdin_to_stdout",
1633 return_value=lambda *args, **kwargs: black.Changed.YES,
1635 report = MagicMock()
1637 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1642 write_back=black.WriteBack.YES,
1646 fsts.assert_called_once_with(
1648 write_back=black.WriteBack.YES,
1649 mode=replace(DEFAULT_MODE, is_pyi=True),
1651 # __BLACK_STDIN_FILENAME__ should have been stripped
1652 report.done.assert_called_with(expected, black.Changed.YES)
1654 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1656 "black.format_stdin_to_stdout",
1657 return_value=lambda *args, **kwargs: black.Changed.YES,
1659 report = MagicMock()
1660 # Even with an existing file, since we are forcing stdin, black
1661 # should output to stdout and not modify the file inplace
1662 p = Path(str(THIS_DIR / "data/collections.py"))
1663 # Make sure is_file actually returns True
1664 self.assertTrue(p.is_file())
1665 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1670 write_back=black.WriteBack.YES,
1674 fsts.assert_called_once()
1675 # __BLACK_STDIN_FILENAME__ should have been stripped
1676 report.done.assert_called_with(expected, black.Changed.YES)
1678 def test_gitignore_exclude(self) -> None:
1679 path = THIS_DIR / "data" / "include_exclude_tests"
1680 include = re.compile(r"\.pyi?$")
1681 exclude = re.compile(r"")
1682 report = black.Report()
1683 gitignore = PathSpec.from_lines(
1684 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1686 sources: List[Path] = []
1688 Path(path / "b/dont_exclude/a.py"),
1689 Path(path / "b/dont_exclude/a.pyi"),
1691 this_abs = THIS_DIR.resolve()
1693 black.gen_python_files(
1704 self.assertEqual(sorted(expected), sorted(sources))
1706 def test_empty_include(self) -> None:
1707 path = THIS_DIR / "data" / "include_exclude_tests"
1708 report = black.Report()
1709 gitignore = PathSpec.from_lines("gitwildmatch", [])
1710 empty = re.compile(r"")
1711 sources: List[Path] = []
1713 Path(path / "b/exclude/a.pie"),
1714 Path(path / "b/exclude/a.py"),
1715 Path(path / "b/exclude/a.pyi"),
1716 Path(path / "b/dont_exclude/a.pie"),
1717 Path(path / "b/dont_exclude/a.py"),
1718 Path(path / "b/dont_exclude/a.pyi"),
1719 Path(path / "b/.definitely_exclude/a.pie"),
1720 Path(path / "b/.definitely_exclude/a.py"),
1721 Path(path / "b/.definitely_exclude/a.pyi"),
1722 Path(path / ".gitignore"),
1723 Path(path / "pyproject.toml"),
1725 this_abs = THIS_DIR.resolve()
1727 black.gen_python_files(
1731 re.compile(black.DEFAULT_EXCLUDES),
1738 self.assertEqual(sorted(expected), sorted(sources))
1740 def test_extend_exclude(self) -> None:
1741 path = THIS_DIR / "data" / "include_exclude_tests"
1742 report = black.Report()
1743 gitignore = PathSpec.from_lines("gitwildmatch", [])
1744 sources: List[Path] = []
1746 Path(path / "b/exclude/a.py"),
1747 Path(path / "b/dont_exclude/a.py"),
1749 this_abs = THIS_DIR.resolve()
1751 black.gen_python_files(
1754 re.compile(black.DEFAULT_INCLUDES),
1755 re.compile(r"\.pyi$"),
1756 re.compile(r"\.definitely_exclude"),
1762 self.assertEqual(sorted(expected), sorted(sources))
1764 def test_invalid_cli_regex(self) -> None:
1765 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1766 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1768 def test_preserves_line_endings(self) -> None:
1769 with TemporaryDirectory() as workspace:
1770 test_file = Path(workspace) / "test.py"
1771 for nl in ["\n", "\r\n"]:
1772 contents = nl.join(["def f( ):", " pass"])
1773 test_file.write_bytes(contents.encode())
1774 ff(test_file, write_back=black.WriteBack.YES)
1775 updated_contents: bytes = test_file.read_bytes()
1776 self.assertIn(nl.encode(), updated_contents)
1778 self.assertNotIn(b"\r\n", updated_contents)
1780 def test_preserves_line_endings_via_stdin(self) -> None:
1781 for nl in ["\n", "\r\n"]:
1782 contents = nl.join(["def f( ):", " pass"])
1783 runner = BlackRunner()
1784 result = runner.invoke(
1785 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1787 self.assertEqual(result.exit_code, 0)
1788 output = result.stdout_bytes
1789 self.assertIn(nl.encode("utf8"), output)
1791 self.assertNotIn(b"\r\n", output)
1793 def test_assert_equivalent_different_asts(self) -> None:
1794 with self.assertRaises(AssertionError):
1795 black.assert_equivalent("{}", "None")
1797 def test_symlink_out_of_root_directory(self) -> None:
1799 root = THIS_DIR.resolve()
1801 include = re.compile(black.DEFAULT_INCLUDES)
1802 exclude = re.compile(black.DEFAULT_EXCLUDES)
1803 report = black.Report()
1804 gitignore = PathSpec.from_lines("gitwildmatch", [])
1805 # `child` should behave like a symlink which resolved path is clearly
1806 # outside of the `root` directory.
1807 path.iterdir.return_value = [child]
1808 child.resolve.return_value = Path("/a/b/c")
1809 child.as_posix.return_value = "/a/b/c"
1810 child.is_symlink.return_value = True
1813 black.gen_python_files(
1824 except ValueError as ve:
1825 self.fail(f"`get_python_files_in_dir()` failed: {ve}")
1826 path.iterdir.assert_called_once()
1827 child.resolve.assert_called_once()
1828 child.is_symlink.assert_called_once()
1829 # `child` should behave like a strange file which resolved path is clearly
1830 # outside of the `root` directory.
1831 child.is_symlink.return_value = False
1832 with self.assertRaises(ValueError):
1834 black.gen_python_files(
1845 path.iterdir.assert_called()
1846 self.assertEqual(path.iterdir.call_count, 2)
1847 child.resolve.assert_called()
1848 self.assertEqual(child.resolve.call_count, 2)
1849 child.is_symlink.assert_called()
1850 self.assertEqual(child.is_symlink.call_count, 2)
1852 def test_shhh_click(self) -> None:
1854 from click import _unicodefun # type: ignore
1855 except ModuleNotFoundError:
1856 self.skipTest("Incompatible Click version")
1857 if not hasattr(_unicodefun, "_verify_python3_env"):
1858 self.skipTest("Incompatible Click version")
1859 # First, let's see if Click is crashing with a preferred ASCII charset.
1860 with patch("locale.getpreferredencoding") as gpe:
1861 gpe.return_value = "ASCII"
1862 with self.assertRaises(RuntimeError):
1863 _unicodefun._verify_python3_env()
1864 # Now, let's silence Click...
1866 # ...and confirm it's silent.
1867 with patch("locale.getpreferredencoding") as gpe:
1868 gpe.return_value = "ASCII"
1870 _unicodefun._verify_python3_env()
1871 except RuntimeError as re:
1872 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1874 def test_root_logger_not_used_directly(self) -> None:
1875 def fail(*args: Any, **kwargs: Any) -> None:
1876 self.fail("Record created with root logger")
1878 with patch.multiple(
1887 ff(THIS_DIR / "util.py")
1889 def test_invalid_config_return_code(self) -> None:
1890 tmp_file = Path(black.dump_to_file())
1892 tmp_config = Path(black.dump_to_file())
1894 args = ["--config", str(tmp_config), str(tmp_file)]
1895 self.invokeBlack(args, exit_code=2, ignore_config=False)
1899 def test_parse_pyproject_toml(self) -> None:
1900 test_toml_file = THIS_DIR / "test.toml"
1901 config = black.parse_pyproject_toml(str(test_toml_file))
1902 self.assertEqual(config["verbose"], 1)
1903 self.assertEqual(config["check"], "no")
1904 self.assertEqual(config["diff"], "y")
1905 self.assertEqual(config["color"], True)
1906 self.assertEqual(config["line_length"], 79)
1907 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1908 self.assertEqual(config["exclude"], r"\.pyi?$")
1909 self.assertEqual(config["include"], r"\.py?$")
1911 def test_read_pyproject_toml(self) -> None:
1912 test_toml_file = THIS_DIR / "test.toml"
1913 fake_ctx = FakeContext()
1914 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1915 config = fake_ctx.default_map
1916 self.assertEqual(config["verbose"], "1")
1917 self.assertEqual(config["check"], "no")
1918 self.assertEqual(config["diff"], "y")
1919 self.assertEqual(config["color"], "True")
1920 self.assertEqual(config["line_length"], "79")
1921 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1922 self.assertEqual(config["exclude"], r"\.pyi?$")
1923 self.assertEqual(config["include"], r"\.py?$")
1925 def test_find_project_root(self) -> None:
1926 with TemporaryDirectory() as workspace:
1927 root = Path(workspace)
1928 test_dir = root / "test"
1931 src_dir = root / "src"
1934 root_pyproject = root / "pyproject.toml"
1935 root_pyproject.touch()
1936 src_pyproject = src_dir / "pyproject.toml"
1937 src_pyproject.touch()
1938 src_python = src_dir / "foo.py"
1942 black.find_project_root((src_dir, test_dir)), root.resolve()
1944 self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1945 self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1948 "black.files.find_user_pyproject_toml",
1949 black.files.find_user_pyproject_toml.__wrapped__,
1951 def test_find_user_pyproject_toml_linux(self) -> None:
1952 if system() == "Windows":
1955 # Test if XDG_CONFIG_HOME is checked
1956 with TemporaryDirectory() as workspace:
1957 tmp_user_config = Path(workspace) / "black"
1958 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1960 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1963 # Test fallback for XDG_CONFIG_HOME
1964 with patch.dict("os.environ"):
1965 os.environ.pop("XDG_CONFIG_HOME", None)
1966 fallback_user_config = Path("~/.config").expanduser() / "black"
1968 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1971 def test_find_user_pyproject_toml_windows(self) -> None:
1972 if system() != "Windows":
1975 user_config_path = Path.home() / ".black"
1977 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1980 def test_bpo_33660_workaround(self) -> None:
1981 if system() == "Windows":
1984 # https://bugs.python.org/issue33660
1986 old_cwd = Path.cwd()
1990 path = Path("workspace") / "project"
1991 report = black.Report(verbose=True)
1992 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1993 self.assertEqual(normalized_path, "workspace/project")
1995 os.chdir(str(old_cwd))
1997 def test_newline_comment_interaction(self) -> None:
1998 source = "class A:\\\r\n# type: ignore\n pass\n"
1999 output = black.format_str(source, mode=DEFAULT_MODE)
2000 black.assert_stable(source, output, mode=DEFAULT_MODE)
2002 def test_bpo_2142_workaround(self) -> None:
2004 # https://bugs.python.org/issue2142
2006 source, _ = read_data("missing_final_newline.py")
2007 # read_data adds a trailing newline
2008 source = source.rstrip()
2009 expected, _ = read_data("missing_final_newline.diff")
2010 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
2011 diff_header = re.compile(
2012 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
2013 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
2016 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
2017 self.assertEqual(result.exit_code, 0)
2020 actual = result.output
2021 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
2022 self.assertEqual(actual, expected)
2024 @pytest.mark.python2
2025 def test_docstring_reformat_for_py27(self) -> None:
2027 Check that stripping trailing whitespace from Python 2 docstrings
2028 doesn't trigger a "not equivalent to source" error
2031 b'def foo():\r\n """Testing\r\n Testing """\r\n print "Foo"\r\n'
2033 expected = 'def foo():\n """Testing\n Testing"""\n print "Foo"\n'
2035 result = CliRunner().invoke(
2037 ["-", "-q", "--target-version=py27"],
2038 input=BytesIO(source),
2041 self.assertEqual(result.exit_code, 0)
2042 actual = result.output
2043 self.assertFormatEqual(actual, expected)
2046 with open(black.__file__, "r", encoding="utf-8") as _bf:
2047 black_source_lines = _bf.readlines()
2050 def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
2051 """Show function calls `from black/__init__.py` as they happen.
2053 Register this with `sys.settrace()` in a test you're debugging.
2058 stack = len(inspect.stack()) - 19
2060 filename = frame.f_code.co_filename
2061 lineno = frame.f_lineno
2062 func_sig_lineno = lineno - 1
2063 funcname = black_source_lines[func_sig_lineno].strip()
2064 while funcname.startswith("@"):
2065 func_sig_lineno += 1
2066 funcname = black_source_lines[func_sig_lineno].strip()
2067 if "black/__init__.py" in filename:
2068 print(f"{' ' * stack}{lineno}:{funcname}")
2072 if __name__ == "__main__":
2073 unittest.main(module="test_black")