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
9 from io import BytesIO, TextIOWrapper
11 from pathlib import Path
12 from platform import system
15 from tempfile import TemporaryDirectory
29 from unittest.mock import patch, MagicMock
32 from click import unstyle
33 from click.testing import CliRunner
36 from black import Feature, TargetVersion
38 from pathspec import PathSpec
40 # Import other test classes
41 from tests.util import (
51 from .test_primer import PrimerCLITests # noqa: F401
54 THIS_FILE = Path(__file__)
61 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
67 def cache_dir(exists: bool = True) -> Iterator[Path]:
68 with TemporaryDirectory() as workspace:
69 cache_dir = Path(workspace)
71 cache_dir = cache_dir / "new"
72 with patch("black.CACHE_DIR", cache_dir):
77 def event_loop() -> Iterator[None]:
78 policy = asyncio.get_event_loop_policy()
79 loop = policy.new_event_loop()
80 asyncio.set_event_loop(loop)
88 class FakeContext(click.Context):
89 """A fake click Context for when calling functions that need it."""
91 def __init__(self) -> None:
92 self.default_map: Dict[str, Any] = {}
95 class FakeParameter(click.Parameter):
96 """A fake click Parameter for when calling functions that need it."""
98 def __init__(self) -> None:
102 class BlackRunner(CliRunner):
103 """Modify CliRunner so that stderr is not merged with stdout.
105 This is a hack that can be removed once we depend on Click 7.x"""
107 def __init__(self) -> None:
108 self.stderrbuf = BytesIO()
109 self.stdoutbuf = BytesIO()
110 self.stdout_bytes = b""
111 self.stderr_bytes = b""
115 def isolation(self, *args: Any, **kwargs: Any) -> Generator[BinaryIO, None, None]:
116 with super().isolation(*args, **kwargs) as output:
118 hold_stderr = sys.stderr
119 sys.stderr = TextIOWrapper(self.stderrbuf, encoding=self.charset)
122 self.stdout_bytes = sys.stdout.buffer.getvalue() # type: ignore
123 self.stderr_bytes = sys.stderr.buffer.getvalue() # type: ignore
124 sys.stderr = hold_stderr
127 class BlackTestCase(BlackBaseTestCase):
129 self, args: List[str], exit_code: int = 0, ignore_config: bool = True
131 runner = BlackRunner()
133 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
134 result = runner.invoke(black.main, args)
139 f"Failed with args: {args}\n"
140 f"stdout: {runner.stdout_bytes.decode()!r}\n"
141 f"stderr: {runner.stderr_bytes.decode()!r}\n"
142 f"exception: {result.exception}"
146 @patch("black.dump_to_file", dump_to_stderr)
147 def test_empty(self) -> None:
148 source = expected = ""
150 self.assertFormatEqual(expected, actual)
151 black.assert_equivalent(source, actual)
152 black.assert_stable(source, actual, DEFAULT_MODE)
154 def test_empty_ff(self) -> None:
156 tmp_file = Path(black.dump_to_file())
158 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
159 with open(tmp_file, encoding="utf8") as f:
163 self.assertFormatEqual(expected, actual)
165 def test_piping(self) -> None:
166 source, expected = read_data("src/black/__init__", data=False)
167 result = BlackRunner().invoke(
169 ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
170 input=BytesIO(source.encode("utf8")),
172 self.assertEqual(result.exit_code, 0)
173 self.assertFormatEqual(expected, result.output)
174 black.assert_equivalent(source, result.output)
175 black.assert_stable(source, result.output, DEFAULT_MODE)
177 def test_piping_diff(self) -> None:
178 diff_header = re.compile(
179 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
182 source, _ = read_data("expression.py")
183 expected, _ = read_data("expression.diff")
184 config = THIS_DIR / "data" / "empty_pyproject.toml"
188 f"--line-length={black.DEFAULT_LINE_LENGTH}",
190 f"--config={config}",
192 result = BlackRunner().invoke(
193 black.main, args, input=BytesIO(source.encode("utf8"))
195 self.assertEqual(result.exit_code, 0)
196 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
197 actual = actual.rstrip() + "\n" # the diff output has a trailing space
198 self.assertEqual(expected, actual)
200 def test_piping_diff_with_color(self) -> None:
201 source, _ = read_data("expression.py")
202 config = THIS_DIR / "data" / "empty_pyproject.toml"
206 f"--line-length={black.DEFAULT_LINE_LENGTH}",
209 f"--config={config}",
211 result = BlackRunner().invoke(
212 black.main, args, input=BytesIO(source.encode("utf8"))
214 actual = result.output
215 # Again, the contents are checked in a different test, so only look for colors.
216 self.assertIn("\033[1;37m", actual)
217 self.assertIn("\033[36m", actual)
218 self.assertIn("\033[32m", actual)
219 self.assertIn("\033[31m", actual)
220 self.assertIn("\033[0m", actual)
222 @patch("black.dump_to_file", dump_to_stderr)
223 def _test_wip(self) -> None:
224 source, expected = read_data("wip")
225 sys.settrace(tracefunc)
228 experimental_string_processing=False,
229 target_versions={black.TargetVersion.PY38},
231 actual = fs(source, mode=mode)
233 self.assertFormatEqual(expected, actual)
234 black.assert_equivalent(source, actual)
235 black.assert_stable(source, actual, black.FileMode())
237 @unittest.expectedFailure
238 @patch("black.dump_to_file", dump_to_stderr)
239 def test_trailing_comma_optional_parens_stability1(self) -> None:
240 source, _expected = read_data("trailing_comma_optional_parens1")
242 black.assert_stable(source, actual, DEFAULT_MODE)
244 @unittest.expectedFailure
245 @patch("black.dump_to_file", dump_to_stderr)
246 def test_trailing_comma_optional_parens_stability2(self) -> None:
247 source, _expected = read_data("trailing_comma_optional_parens2")
249 black.assert_stable(source, actual, DEFAULT_MODE)
251 @unittest.expectedFailure
252 @patch("black.dump_to_file", dump_to_stderr)
253 def test_trailing_comma_optional_parens_stability3(self) -> None:
254 source, _expected = read_data("trailing_comma_optional_parens3")
256 black.assert_stable(source, actual, DEFAULT_MODE)
258 @patch("black.dump_to_file", dump_to_stderr)
259 def test_trailing_comma_optional_parens_stability1_pass2(self) -> None:
260 source, _expected = read_data("trailing_comma_optional_parens1")
261 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
262 black.assert_stable(source, actual, DEFAULT_MODE)
264 @patch("black.dump_to_file", dump_to_stderr)
265 def test_trailing_comma_optional_parens_stability2_pass2(self) -> None:
266 source, _expected = read_data("trailing_comma_optional_parens2")
267 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
268 black.assert_stable(source, actual, DEFAULT_MODE)
270 @patch("black.dump_to_file", dump_to_stderr)
271 def test_trailing_comma_optional_parens_stability3_pass2(self) -> None:
272 source, _expected = read_data("trailing_comma_optional_parens3")
273 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
274 black.assert_stable(source, actual, DEFAULT_MODE)
276 @patch("black.dump_to_file", dump_to_stderr)
277 def test_pep_572(self) -> None:
278 source, expected = read_data("pep_572")
280 self.assertFormatEqual(expected, actual)
281 black.assert_stable(source, actual, DEFAULT_MODE)
282 if sys.version_info >= (3, 8):
283 black.assert_equivalent(source, actual)
285 @patch("black.dump_to_file", dump_to_stderr)
286 def test_pep_572_remove_parens(self) -> None:
287 source, expected = read_data("pep_572_remove_parens")
289 self.assertFormatEqual(expected, actual)
290 black.assert_stable(source, actual, DEFAULT_MODE)
291 if sys.version_info >= (3, 8):
292 black.assert_equivalent(source, actual)
294 @patch("black.dump_to_file", dump_to_stderr)
295 def test_pep_572_do_not_remove_parens(self) -> None:
296 source, expected = read_data("pep_572_do_not_remove_parens")
297 # the AST safety checks will fail, but that's expected, just make sure no
298 # parentheses are touched
299 actual = black.format_str(source, mode=DEFAULT_MODE)
300 self.assertFormatEqual(expected, actual)
302 def test_pep_572_version_detection(self) -> None:
303 source, _ = read_data("pep_572")
304 root = black.lib2to3_parse(source)
305 features = black.get_features_used(root)
306 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
307 versions = black.detect_target_versions(root)
308 self.assertIn(black.TargetVersion.PY38, versions)
310 def test_expression_ff(self) -> None:
311 source, expected = read_data("expression")
312 tmp_file = Path(black.dump_to_file(source))
314 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
315 with open(tmp_file, encoding="utf8") as f:
319 self.assertFormatEqual(expected, actual)
320 with patch("black.dump_to_file", dump_to_stderr):
321 black.assert_equivalent(source, actual)
322 black.assert_stable(source, actual, DEFAULT_MODE)
324 def test_expression_diff(self) -> None:
325 source, _ = read_data("expression.py")
326 config = THIS_DIR / "data" / "empty_pyproject.toml"
327 expected, _ = read_data("expression.diff")
328 tmp_file = Path(black.dump_to_file(source))
329 diff_header = re.compile(
330 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
331 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
334 result = BlackRunner().invoke(
335 black.main, ["--diff", str(tmp_file), f"--config={config}"]
337 self.assertEqual(result.exit_code, 0)
340 actual = result.output
341 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
342 if expected != actual:
343 dump = black.dump_to_file(actual)
345 "Expected diff isn't equal to the actual. If you made changes to"
346 " expression.py and this is an anticipated difference, overwrite"
347 f" tests/data/expression.diff with {dump}"
349 self.assertEqual(expected, actual, msg)
351 def test_expression_diff_with_color(self) -> None:
352 source, _ = read_data("expression.py")
353 config = THIS_DIR / "data" / "empty_pyproject.toml"
354 expected, _ = read_data("expression.diff")
355 tmp_file = Path(black.dump_to_file(source))
357 result = BlackRunner().invoke(
358 black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
362 actual = result.output
363 # We check the contents of the diff in `test_expression_diff`. All
364 # we need to check here is that color codes exist in the result.
365 self.assertIn("\033[1;37m", actual)
366 self.assertIn("\033[36m", actual)
367 self.assertIn("\033[32m", actual)
368 self.assertIn("\033[31m", actual)
369 self.assertIn("\033[0m", actual)
371 @patch("black.dump_to_file", dump_to_stderr)
372 def test_pep_570(self) -> None:
373 source, expected = read_data("pep_570")
375 self.assertFormatEqual(expected, actual)
376 black.assert_stable(source, actual, DEFAULT_MODE)
377 if sys.version_info >= (3, 8):
378 black.assert_equivalent(source, actual)
380 def test_detect_pos_only_arguments(self) -> None:
381 source, _ = read_data("pep_570")
382 root = black.lib2to3_parse(source)
383 features = black.get_features_used(root)
384 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
385 versions = black.detect_target_versions(root)
386 self.assertIn(black.TargetVersion.PY38, versions)
388 @patch("black.dump_to_file", dump_to_stderr)
389 def test_string_quotes(self) -> None:
390 source, expected = read_data("string_quotes")
391 mode = black.Mode(experimental_string_processing=True)
392 actual = fs(source, mode=mode)
393 self.assertFormatEqual(expected, actual)
394 black.assert_equivalent(source, actual)
395 black.assert_stable(source, actual, mode)
396 mode = replace(mode, string_normalization=False)
397 not_normalized = fs(source, mode=mode)
398 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
399 black.assert_equivalent(source, not_normalized)
400 black.assert_stable(source, not_normalized, mode=mode)
402 @patch("black.dump_to_file", dump_to_stderr)
403 def test_docstring_no_string_normalization(self) -> None:
404 """Like test_docstring but with string normalization off."""
405 source, expected = read_data("docstring_no_string_normalization")
406 mode = replace(DEFAULT_MODE, string_normalization=False)
407 actual = fs(source, mode=mode)
408 self.assertFormatEqual(expected, actual)
409 black.assert_equivalent(source, actual)
410 black.assert_stable(source, actual, mode)
412 def test_long_strings_flag_disabled(self) -> None:
413 """Tests for turning off the string processing logic."""
414 source, expected = read_data("long_strings_flag_disabled")
415 mode = replace(DEFAULT_MODE, experimental_string_processing=False)
416 actual = fs(source, mode=mode)
417 self.assertFormatEqual(expected, actual)
418 black.assert_stable(expected, actual, mode)
420 @patch("black.dump_to_file", dump_to_stderr)
421 def test_numeric_literals(self) -> None:
422 source, expected = read_data("numeric_literals")
423 mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
424 actual = fs(source, mode=mode)
425 self.assertFormatEqual(expected, actual)
426 black.assert_equivalent(source, actual)
427 black.assert_stable(source, actual, mode)
429 @patch("black.dump_to_file", dump_to_stderr)
430 def test_numeric_literals_ignoring_underscores(self) -> None:
431 source, expected = read_data("numeric_literals_skip_underscores")
432 mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
433 actual = fs(source, mode=mode)
434 self.assertFormatEqual(expected, actual)
435 black.assert_equivalent(source, actual)
436 black.assert_stable(source, actual, mode)
438 def test_skip_magic_trailing_comma(self) -> None:
439 source, _ = read_data("expression.py")
440 expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
441 tmp_file = Path(black.dump_to_file(source))
442 diff_header = re.compile(
443 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
444 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
447 result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
448 self.assertEqual(result.exit_code, 0)
451 actual = result.output
452 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
453 actual = actual.rstrip() + "\n" # the diff output has a trailing space
454 if expected != actual:
455 dump = black.dump_to_file(actual)
457 "Expected diff isn't equal to the actual. If you made changes to"
458 " expression.py and this is an anticipated difference, overwrite"
459 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
461 self.assertEqual(expected, actual, msg)
463 @pytest.mark.without_python2
464 def test_python2_should_fail_without_optional_install(self) -> None:
465 # python 3.7 and below will install typed-ast and will be able to parse Python 2
466 if sys.version_info < (3, 8):
469 tmp_file = Path(black.dump_to_file(source))
471 runner = BlackRunner()
472 result = runner.invoke(black.main, [str(tmp_file)])
473 self.assertEqual(result.exit_code, 123)
477 runner.stderr_bytes.decode()
484 "The requested source code has invalid Python 3 syntax."
485 "If you are trying to format Python 2 files please reinstall Black"
486 " with the 'python2' extra: `python3 -m pip install black[python2]`."
488 self.assertIn(msg, actual)
491 @patch("black.dump_to_file", dump_to_stderr)
492 def test_python2_print_function(self) -> None:
493 source, expected = read_data("python2_print_function")
494 mode = replace(DEFAULT_MODE, target_versions={TargetVersion.PY27})
495 actual = fs(source, mode=mode)
496 self.assertFormatEqual(expected, actual)
497 black.assert_equivalent(source, actual)
498 black.assert_stable(source, actual, mode)
500 @patch("black.dump_to_file", dump_to_stderr)
501 def test_stub(self) -> None:
502 mode = replace(DEFAULT_MODE, is_pyi=True)
503 source, expected = read_data("stub.pyi")
504 actual = fs(source, mode=mode)
505 self.assertFormatEqual(expected, actual)
506 black.assert_stable(source, actual, mode)
508 @patch("black.dump_to_file", dump_to_stderr)
509 def test_async_as_identifier(self) -> None:
510 source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
511 source, expected = read_data("async_as_identifier")
513 self.assertFormatEqual(expected, actual)
514 major, minor = sys.version_info[:2]
515 if major < 3 or (major <= 3 and minor < 7):
516 black.assert_equivalent(source, actual)
517 black.assert_stable(source, actual, DEFAULT_MODE)
518 # ensure black can parse this when the target is 3.6
519 self.invokeBlack([str(source_path), "--target-version", "py36"])
520 # but not on 3.7, because async/await is no longer an identifier
521 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
523 @patch("black.dump_to_file", dump_to_stderr)
524 def test_python37(self) -> None:
525 source_path = (THIS_DIR / "data" / "python37.py").resolve()
526 source, expected = read_data("python37")
528 self.assertFormatEqual(expected, actual)
529 major, minor = sys.version_info[:2]
530 if major > 3 or (major == 3 and minor >= 7):
531 black.assert_equivalent(source, actual)
532 black.assert_stable(source, actual, DEFAULT_MODE)
533 # ensure black can parse this when the target is 3.7
534 self.invokeBlack([str(source_path), "--target-version", "py37"])
535 # but not on 3.6, because we use async as a reserved keyword
536 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
538 @patch("black.dump_to_file", dump_to_stderr)
539 def test_python38(self) -> None:
540 source, expected = read_data("python38")
542 self.assertFormatEqual(expected, actual)
543 major, minor = sys.version_info[:2]
544 if major > 3 or (major == 3 and minor >= 8):
545 black.assert_equivalent(source, actual)
546 black.assert_stable(source, actual, DEFAULT_MODE)
548 @patch("black.dump_to_file", dump_to_stderr)
549 def test_python39(self) -> None:
550 source, expected = read_data("python39")
552 self.assertFormatEqual(expected, actual)
553 major, minor = sys.version_info[:2]
554 if major > 3 or (major == 3 and minor >= 9):
555 black.assert_equivalent(source, actual)
556 black.assert_stable(source, actual, DEFAULT_MODE)
558 def test_tab_comment_indentation(self) -> None:
559 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
560 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
561 self.assertFormatEqual(contents_spc, fs(contents_spc))
562 self.assertFormatEqual(contents_spc, fs(contents_tab))
564 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
565 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
566 self.assertFormatEqual(contents_spc, fs(contents_spc))
567 self.assertFormatEqual(contents_spc, fs(contents_tab))
569 # mixed tabs and spaces (valid Python 2 code)
570 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
571 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
572 self.assertFormatEqual(contents_spc, fs(contents_spc))
573 self.assertFormatEqual(contents_spc, fs(contents_tab))
575 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
576 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
577 self.assertFormatEqual(contents_spc, fs(contents_spc))
578 self.assertFormatEqual(contents_spc, fs(contents_tab))
580 def test_report_verbose(self) -> None:
581 report = black.Report(verbose=True)
585 def out(msg: str, **kwargs: Any) -> None:
586 out_lines.append(msg)
588 def err(msg: str, **kwargs: Any) -> None:
589 err_lines.append(msg)
591 with patch("black.out", out), patch("black.err", err):
592 report.done(Path("f1"), black.Changed.NO)
593 self.assertEqual(len(out_lines), 1)
594 self.assertEqual(len(err_lines), 0)
595 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
596 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
597 self.assertEqual(report.return_code, 0)
598 report.done(Path("f2"), black.Changed.YES)
599 self.assertEqual(len(out_lines), 2)
600 self.assertEqual(len(err_lines), 0)
601 self.assertEqual(out_lines[-1], "reformatted f2")
603 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
605 report.done(Path("f3"), black.Changed.CACHED)
606 self.assertEqual(len(out_lines), 3)
607 self.assertEqual(len(err_lines), 0)
609 out_lines[-1], "f3 wasn't modified on disk since last run."
612 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
614 self.assertEqual(report.return_code, 0)
616 self.assertEqual(report.return_code, 1)
618 report.failed(Path("e1"), "boom")
619 self.assertEqual(len(out_lines), 3)
620 self.assertEqual(len(err_lines), 1)
621 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
623 unstyle(str(report)),
624 "1 file reformatted, 2 files left unchanged, 1 file failed to"
627 self.assertEqual(report.return_code, 123)
628 report.done(Path("f3"), black.Changed.YES)
629 self.assertEqual(len(out_lines), 4)
630 self.assertEqual(len(err_lines), 1)
631 self.assertEqual(out_lines[-1], "reformatted f3")
633 unstyle(str(report)),
634 "2 files reformatted, 2 files left unchanged, 1 file failed to"
637 self.assertEqual(report.return_code, 123)
638 report.failed(Path("e2"), "boom")
639 self.assertEqual(len(out_lines), 4)
640 self.assertEqual(len(err_lines), 2)
641 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
643 unstyle(str(report)),
644 "2 files reformatted, 2 files left unchanged, 2 files failed to"
647 self.assertEqual(report.return_code, 123)
648 report.path_ignored(Path("wat"), "no match")
649 self.assertEqual(len(out_lines), 5)
650 self.assertEqual(len(err_lines), 2)
651 self.assertEqual(out_lines[-1], "wat ignored: no match")
653 unstyle(str(report)),
654 "2 files reformatted, 2 files left unchanged, 2 files failed to"
657 self.assertEqual(report.return_code, 123)
658 report.done(Path("f4"), black.Changed.NO)
659 self.assertEqual(len(out_lines), 6)
660 self.assertEqual(len(err_lines), 2)
661 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
663 unstyle(str(report)),
664 "2 files reformatted, 3 files left unchanged, 2 files failed to"
667 self.assertEqual(report.return_code, 123)
670 unstyle(str(report)),
671 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
672 " would fail to reformat.",
677 unstyle(str(report)),
678 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
679 " would fail to reformat.",
682 def test_report_quiet(self) -> None:
683 report = black.Report(quiet=True)
687 def out(msg: str, **kwargs: Any) -> None:
688 out_lines.append(msg)
690 def err(msg: str, **kwargs: Any) -> None:
691 err_lines.append(msg)
693 with patch("black.out", out), patch("black.err", err):
694 report.done(Path("f1"), black.Changed.NO)
695 self.assertEqual(len(out_lines), 0)
696 self.assertEqual(len(err_lines), 0)
697 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
698 self.assertEqual(report.return_code, 0)
699 report.done(Path("f2"), black.Changed.YES)
700 self.assertEqual(len(out_lines), 0)
701 self.assertEqual(len(err_lines), 0)
703 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
705 report.done(Path("f3"), black.Changed.CACHED)
706 self.assertEqual(len(out_lines), 0)
707 self.assertEqual(len(err_lines), 0)
709 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
711 self.assertEqual(report.return_code, 0)
713 self.assertEqual(report.return_code, 1)
715 report.failed(Path("e1"), "boom")
716 self.assertEqual(len(out_lines), 0)
717 self.assertEqual(len(err_lines), 1)
718 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
720 unstyle(str(report)),
721 "1 file reformatted, 2 files left unchanged, 1 file failed to"
724 self.assertEqual(report.return_code, 123)
725 report.done(Path("f3"), black.Changed.YES)
726 self.assertEqual(len(out_lines), 0)
727 self.assertEqual(len(err_lines), 1)
729 unstyle(str(report)),
730 "2 files reformatted, 2 files left unchanged, 1 file failed to"
733 self.assertEqual(report.return_code, 123)
734 report.failed(Path("e2"), "boom")
735 self.assertEqual(len(out_lines), 0)
736 self.assertEqual(len(err_lines), 2)
737 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
739 unstyle(str(report)),
740 "2 files reformatted, 2 files left unchanged, 2 files failed to"
743 self.assertEqual(report.return_code, 123)
744 report.path_ignored(Path("wat"), "no match")
745 self.assertEqual(len(out_lines), 0)
746 self.assertEqual(len(err_lines), 2)
748 unstyle(str(report)),
749 "2 files reformatted, 2 files left unchanged, 2 files failed to"
752 self.assertEqual(report.return_code, 123)
753 report.done(Path("f4"), black.Changed.NO)
754 self.assertEqual(len(out_lines), 0)
755 self.assertEqual(len(err_lines), 2)
757 unstyle(str(report)),
758 "2 files reformatted, 3 files left unchanged, 2 files failed to"
761 self.assertEqual(report.return_code, 123)
764 unstyle(str(report)),
765 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
766 " would fail to reformat.",
771 unstyle(str(report)),
772 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
773 " would fail to reformat.",
776 def test_report_normal(self) -> None:
777 report = black.Report()
781 def out(msg: str, **kwargs: Any) -> None:
782 out_lines.append(msg)
784 def err(msg: str, **kwargs: Any) -> None:
785 err_lines.append(msg)
787 with patch("black.out", out), patch("black.err", err):
788 report.done(Path("f1"), black.Changed.NO)
789 self.assertEqual(len(out_lines), 0)
790 self.assertEqual(len(err_lines), 0)
791 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
792 self.assertEqual(report.return_code, 0)
793 report.done(Path("f2"), black.Changed.YES)
794 self.assertEqual(len(out_lines), 1)
795 self.assertEqual(len(err_lines), 0)
796 self.assertEqual(out_lines[-1], "reformatted f2")
798 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
800 report.done(Path("f3"), black.Changed.CACHED)
801 self.assertEqual(len(out_lines), 1)
802 self.assertEqual(len(err_lines), 0)
803 self.assertEqual(out_lines[-1], "reformatted f2")
805 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
807 self.assertEqual(report.return_code, 0)
809 self.assertEqual(report.return_code, 1)
811 report.failed(Path("e1"), "boom")
812 self.assertEqual(len(out_lines), 1)
813 self.assertEqual(len(err_lines), 1)
814 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
816 unstyle(str(report)),
817 "1 file reformatted, 2 files left unchanged, 1 file failed to"
820 self.assertEqual(report.return_code, 123)
821 report.done(Path("f3"), black.Changed.YES)
822 self.assertEqual(len(out_lines), 2)
823 self.assertEqual(len(err_lines), 1)
824 self.assertEqual(out_lines[-1], "reformatted f3")
826 unstyle(str(report)),
827 "2 files reformatted, 2 files left unchanged, 1 file failed to"
830 self.assertEqual(report.return_code, 123)
831 report.failed(Path("e2"), "boom")
832 self.assertEqual(len(out_lines), 2)
833 self.assertEqual(len(err_lines), 2)
834 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
836 unstyle(str(report)),
837 "2 files reformatted, 2 files left unchanged, 2 files failed to"
840 self.assertEqual(report.return_code, 123)
841 report.path_ignored(Path("wat"), "no match")
842 self.assertEqual(len(out_lines), 2)
843 self.assertEqual(len(err_lines), 2)
845 unstyle(str(report)),
846 "2 files reformatted, 2 files left unchanged, 2 files failed to"
849 self.assertEqual(report.return_code, 123)
850 report.done(Path("f4"), black.Changed.NO)
851 self.assertEqual(len(out_lines), 2)
852 self.assertEqual(len(err_lines), 2)
854 unstyle(str(report)),
855 "2 files reformatted, 3 files left unchanged, 2 files failed to"
858 self.assertEqual(report.return_code, 123)
861 unstyle(str(report)),
862 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
863 " would fail to reformat.",
868 unstyle(str(report)),
869 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
870 " would fail to reformat.",
873 def test_lib2to3_parse(self) -> None:
874 with self.assertRaises(black.InvalidInput):
875 black.lib2to3_parse("invalid syntax")
878 black.lib2to3_parse(straddling)
879 black.lib2to3_parse(straddling, {TargetVersion.PY27})
880 black.lib2to3_parse(straddling, {TargetVersion.PY36})
881 black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36})
884 black.lib2to3_parse(py2_only)
885 black.lib2to3_parse(py2_only, {TargetVersion.PY27})
886 with self.assertRaises(black.InvalidInput):
887 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
888 with self.assertRaises(black.InvalidInput):
889 black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36})
891 py3_only = "exec(x, end=y)"
892 black.lib2to3_parse(py3_only)
893 with self.assertRaises(black.InvalidInput):
894 black.lib2to3_parse(py3_only, {TargetVersion.PY27})
895 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
896 black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36})
898 def test_get_features_used_decorator(self) -> None:
899 # Test the feature detection of new decorator syntax
900 # since this makes some test cases of test_get_features_used()
901 # fails if it fails, this is tested first so that a useful case
903 simples, relaxed = read_data("decorators")
904 # skip explanation comments at the top of the file
905 for simple_test in simples.split("##")[1:]:
906 node = black.lib2to3_parse(simple_test)
907 decorator = str(node.children[0].children[0]).strip()
909 Feature.RELAXED_DECORATORS,
910 black.get_features_used(node),
912 f"decorator '{decorator}' follows python<=3.8 syntax"
913 "but is detected as 3.9+"
914 # f"The full node is\n{node!r}"
917 # skip the '# output' comment at the top of the output part
918 for relaxed_test in relaxed.split("##")[1:]:
919 node = black.lib2to3_parse(relaxed_test)
920 decorator = str(node.children[0].children[0]).strip()
922 Feature.RELAXED_DECORATORS,
923 black.get_features_used(node),
925 f"decorator '{decorator}' uses python3.9+ syntax"
926 "but is detected as python<=3.8"
927 # f"The full node is\n{node!r}"
931 def test_get_features_used(self) -> None:
932 node = black.lib2to3_parse("def f(*, arg): ...\n")
933 self.assertEqual(black.get_features_used(node), set())
934 node = black.lib2to3_parse("def f(*, arg,): ...\n")
935 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
936 node = black.lib2to3_parse("f(*arg,)\n")
938 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
940 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
941 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
942 node = black.lib2to3_parse("123_456\n")
943 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
944 node = black.lib2to3_parse("123456\n")
945 self.assertEqual(black.get_features_used(node), set())
946 source, expected = read_data("function")
947 node = black.lib2to3_parse(source)
948 expected_features = {
949 Feature.TRAILING_COMMA_IN_CALL,
950 Feature.TRAILING_COMMA_IN_DEF,
953 self.assertEqual(black.get_features_used(node), expected_features)
954 node = black.lib2to3_parse(expected)
955 self.assertEqual(black.get_features_used(node), expected_features)
956 source, expected = read_data("expression")
957 node = black.lib2to3_parse(source)
958 self.assertEqual(black.get_features_used(node), set())
959 node = black.lib2to3_parse(expected)
960 self.assertEqual(black.get_features_used(node), set())
962 def test_get_future_imports(self) -> None:
963 node = black.lib2to3_parse("\n")
964 self.assertEqual(set(), black.get_future_imports(node))
965 node = black.lib2to3_parse("from __future__ import black\n")
966 self.assertEqual({"black"}, black.get_future_imports(node))
967 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
968 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
969 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
970 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
971 node = black.lib2to3_parse(
972 "from __future__ import multiple\nfrom __future__ import imports\n"
974 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
975 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
976 self.assertEqual({"black"}, black.get_future_imports(node))
977 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
978 self.assertEqual({"black"}, black.get_future_imports(node))
979 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
980 self.assertEqual(set(), black.get_future_imports(node))
981 node = black.lib2to3_parse("from some.module import black\n")
982 self.assertEqual(set(), black.get_future_imports(node))
983 node = black.lib2to3_parse(
984 "from __future__ import unicode_literals as _unicode_literals"
986 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
987 node = black.lib2to3_parse(
988 "from __future__ import unicode_literals as _lol, print"
990 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
992 def test_debug_visitor(self) -> None:
993 source, _ = read_data("debug_visitor.py")
994 expected, _ = read_data("debug_visitor.out")
998 def out(msg: str, **kwargs: Any) -> None:
999 out_lines.append(msg)
1001 def err(msg: str, **kwargs: Any) -> None:
1002 err_lines.append(msg)
1004 with patch("black.out", out), patch("black.err", err):
1005 black.DebugVisitor.show(source)
1006 actual = "\n".join(out_lines) + "\n"
1008 if expected != actual:
1009 log_name = black.dump_to_file(*out_lines)
1013 f"AST print out is different. Actual version dumped to {log_name}",
1016 def test_format_file_contents(self) -> None:
1019 with self.assertRaises(black.NothingChanged):
1020 black.format_file_contents(empty, mode=mode, fast=False)
1022 with self.assertRaises(black.NothingChanged):
1023 black.format_file_contents(just_nl, mode=mode, fast=False)
1024 same = "j = [1, 2, 3]\n"
1025 with self.assertRaises(black.NothingChanged):
1026 black.format_file_contents(same, mode=mode, fast=False)
1027 different = "j = [1,2,3]"
1029 actual = black.format_file_contents(different, mode=mode, fast=False)
1030 self.assertEqual(expected, actual)
1031 invalid = "return if you can"
1032 with self.assertRaises(black.InvalidInput) as e:
1033 black.format_file_contents(invalid, mode=mode, fast=False)
1034 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
1036 def test_endmarker(self) -> None:
1037 n = black.lib2to3_parse("\n")
1038 self.assertEqual(n.type, black.syms.file_input)
1039 self.assertEqual(len(n.children), 1)
1040 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
1042 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
1043 def test_assertFormatEqual(self) -> None:
1047 def out(msg: str, **kwargs: Any) -> None:
1048 out_lines.append(msg)
1050 def err(msg: str, **kwargs: Any) -> None:
1051 err_lines.append(msg)
1053 with patch("black.out", out), patch("black.err", err):
1054 with self.assertRaises(AssertionError):
1055 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
1057 out_str = "".join(out_lines)
1058 self.assertTrue("Expected tree:" in out_str)
1059 self.assertTrue("Actual tree:" in out_str)
1060 self.assertEqual("".join(err_lines), "")
1062 def test_cache_broken_file(self) -> None:
1064 with cache_dir() as workspace:
1065 cache_file = black.get_cache_file(mode)
1066 with cache_file.open("w") as fobj:
1067 fobj.write("this is not a pickle")
1068 self.assertEqual(black.read_cache(mode), {})
1069 src = (workspace / "test.py").resolve()
1070 with src.open("w") as fobj:
1071 fobj.write("print('hello')")
1072 self.invokeBlack([str(src)])
1073 cache = black.read_cache(mode)
1074 self.assertIn(str(src), cache)
1076 def test_cache_single_file_already_cached(self) -> None:
1078 with cache_dir() as workspace:
1079 src = (workspace / "test.py").resolve()
1080 with src.open("w") as fobj:
1081 fobj.write("print('hello')")
1082 black.write_cache({}, [src], mode)
1083 self.invokeBlack([str(src)])
1084 with src.open("r") as fobj:
1085 self.assertEqual(fobj.read(), "print('hello')")
1088 def test_cache_multiple_files(self) -> None:
1090 with cache_dir() as workspace, patch(
1091 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1093 one = (workspace / "one.py").resolve()
1094 with one.open("w") as fobj:
1095 fobj.write("print('hello')")
1096 two = (workspace / "two.py").resolve()
1097 with two.open("w") as fobj:
1098 fobj.write("print('hello')")
1099 black.write_cache({}, [one], mode)
1100 self.invokeBlack([str(workspace)])
1101 with one.open("r") as fobj:
1102 self.assertEqual(fobj.read(), "print('hello')")
1103 with two.open("r") as fobj:
1104 self.assertEqual(fobj.read(), 'print("hello")\n')
1105 cache = black.read_cache(mode)
1106 self.assertIn(str(one), cache)
1107 self.assertIn(str(two), cache)
1109 def test_no_cache_when_writeback_diff(self) -> None:
1111 with cache_dir() as workspace:
1112 src = (workspace / "test.py").resolve()
1113 with src.open("w") as fobj:
1114 fobj.write("print('hello')")
1115 with patch("black.read_cache") as read_cache, patch(
1118 self.invokeBlack([str(src), "--diff"])
1119 cache_file = black.get_cache_file(mode)
1120 self.assertFalse(cache_file.exists())
1121 write_cache.assert_not_called()
1122 read_cache.assert_not_called()
1124 def test_no_cache_when_writeback_color_diff(self) -> None:
1126 with cache_dir() as workspace:
1127 src = (workspace / "test.py").resolve()
1128 with src.open("w") as fobj:
1129 fobj.write("print('hello')")
1130 with patch("black.read_cache") as read_cache, patch(
1133 self.invokeBlack([str(src), "--diff", "--color"])
1134 cache_file = black.get_cache_file(mode)
1135 self.assertFalse(cache_file.exists())
1136 write_cache.assert_not_called()
1137 read_cache.assert_not_called()
1140 def test_output_locking_when_writeback_diff(self) -> None:
1141 with cache_dir() as workspace:
1142 for tag in range(0, 4):
1143 src = (workspace / f"test{tag}.py").resolve()
1144 with src.open("w") as fobj:
1145 fobj.write("print('hello')")
1146 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1147 self.invokeBlack(["--diff", str(workspace)], exit_code=0)
1148 # this isn't quite doing what we want, but if it _isn't_
1149 # called then we cannot be using the lock it provides
1153 def test_output_locking_when_writeback_color_diff(self) -> None:
1154 with cache_dir() as workspace:
1155 for tag in range(0, 4):
1156 src = (workspace / f"test{tag}.py").resolve()
1157 with src.open("w") as fobj:
1158 fobj.write("print('hello')")
1159 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1160 self.invokeBlack(["--diff", "--color", str(workspace)], exit_code=0)
1161 # this isn't quite doing what we want, but if it _isn't_
1162 # called then we cannot be using the lock it provides
1165 def test_no_cache_when_stdin(self) -> None:
1168 result = CliRunner().invoke(
1169 black.main, ["-"], input=BytesIO(b"print('hello')")
1171 self.assertEqual(result.exit_code, 0)
1172 cache_file = black.get_cache_file(mode)
1173 self.assertFalse(cache_file.exists())
1175 def test_read_cache_no_cachefile(self) -> None:
1178 self.assertEqual(black.read_cache(mode), {})
1180 def test_write_cache_read_cache(self) -> None:
1182 with cache_dir() as workspace:
1183 src = (workspace / "test.py").resolve()
1185 black.write_cache({}, [src], mode)
1186 cache = black.read_cache(mode)
1187 self.assertIn(str(src), cache)
1188 self.assertEqual(cache[str(src)], black.get_cache_info(src))
1190 def test_filter_cached(self) -> None:
1191 with TemporaryDirectory() as workspace:
1192 path = Path(workspace)
1193 uncached = (path / "uncached").resolve()
1194 cached = (path / "cached").resolve()
1195 cached_but_changed = (path / "changed").resolve()
1198 cached_but_changed.touch()
1200 str(cached): black.get_cache_info(cached),
1201 str(cached_but_changed): (0.0, 0),
1203 todo, done = black.filter_cached(
1204 cache, {uncached, cached, cached_but_changed}
1206 self.assertEqual(todo, {uncached, cached_but_changed})
1207 self.assertEqual(done, {cached})
1209 def test_write_cache_creates_directory_if_needed(self) -> None:
1211 with cache_dir(exists=False) as workspace:
1212 self.assertFalse(workspace.exists())
1213 black.write_cache({}, [], mode)
1214 self.assertTrue(workspace.exists())
1217 def test_failed_formatting_does_not_get_cached(self) -> None:
1219 with cache_dir() as workspace, patch(
1220 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1222 failing = (workspace / "failing.py").resolve()
1223 with failing.open("w") as fobj:
1224 fobj.write("not actually python")
1225 clean = (workspace / "clean.py").resolve()
1226 with clean.open("w") as fobj:
1227 fobj.write('print("hello")\n')
1228 self.invokeBlack([str(workspace)], exit_code=123)
1229 cache = black.read_cache(mode)
1230 self.assertNotIn(str(failing), cache)
1231 self.assertIn(str(clean), cache)
1233 def test_write_cache_write_fail(self) -> None:
1235 with cache_dir(), patch.object(Path, "open") as mock:
1236 mock.side_effect = OSError
1237 black.write_cache({}, [], mode)
1240 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1241 def test_works_in_mono_process_only_environment(self) -> None:
1242 with cache_dir() as workspace:
1244 (workspace / "one.py").resolve(),
1245 (workspace / "two.py").resolve(),
1247 f.write_text('print("hello")\n')
1248 self.invokeBlack([str(workspace)])
1251 def test_check_diff_use_together(self) -> None:
1253 # Files which will be reformatted.
1254 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
1255 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1256 # Files which will not be reformatted.
1257 src2 = (THIS_DIR / "data" / "composition.py").resolve()
1258 self.invokeBlack([str(src2), "--diff", "--check"])
1259 # Multi file command.
1260 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1262 def test_no_files(self) -> None:
1264 # Without an argument, black exits with error code 0.
1265 self.invokeBlack([])
1267 def test_broken_symlink(self) -> None:
1268 with cache_dir() as workspace:
1269 symlink = workspace / "broken_link.py"
1271 symlink.symlink_to("nonexistent.py")
1272 except OSError as e:
1273 self.skipTest(f"Can't create symlinks: {e}")
1274 self.invokeBlack([str(workspace.resolve())])
1276 def test_read_cache_line_lengths(self) -> None:
1278 short_mode = replace(DEFAULT_MODE, line_length=1)
1279 with cache_dir() as workspace:
1280 path = (workspace / "file.py").resolve()
1282 black.write_cache({}, [path], mode)
1283 one = black.read_cache(mode)
1284 self.assertIn(str(path), one)
1285 two = black.read_cache(short_mode)
1286 self.assertNotIn(str(path), two)
1288 def test_single_file_force_pyi(self) -> None:
1289 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1290 contents, expected = read_data("force_pyi")
1291 with cache_dir() as workspace:
1292 path = (workspace / "file.py").resolve()
1293 with open(path, "w") as fh:
1295 self.invokeBlack([str(path), "--pyi"])
1296 with open(path, "r") as fh:
1298 # verify cache with --pyi is separate
1299 pyi_cache = black.read_cache(pyi_mode)
1300 self.assertIn(str(path), pyi_cache)
1301 normal_cache = black.read_cache(DEFAULT_MODE)
1302 self.assertNotIn(str(path), normal_cache)
1303 self.assertFormatEqual(expected, actual)
1304 black.assert_equivalent(contents, actual)
1305 black.assert_stable(contents, actual, pyi_mode)
1308 def test_multi_file_force_pyi(self) -> None:
1309 reg_mode = DEFAULT_MODE
1310 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1311 contents, expected = read_data("force_pyi")
1312 with cache_dir() as workspace:
1314 (workspace / "file1.py").resolve(),
1315 (workspace / "file2.py").resolve(),
1318 with open(path, "w") as fh:
1320 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1322 with open(path, "r") as fh:
1324 self.assertEqual(actual, expected)
1325 # verify cache with --pyi is separate
1326 pyi_cache = black.read_cache(pyi_mode)
1327 normal_cache = black.read_cache(reg_mode)
1329 self.assertIn(str(path), pyi_cache)
1330 self.assertNotIn(str(path), normal_cache)
1332 def test_pipe_force_pyi(self) -> None:
1333 source, expected = read_data("force_pyi")
1334 result = CliRunner().invoke(
1335 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1337 self.assertEqual(result.exit_code, 0)
1338 actual = result.output
1339 self.assertFormatEqual(actual, expected)
1341 def test_single_file_force_py36(self) -> None:
1342 reg_mode = DEFAULT_MODE
1343 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1344 source, expected = read_data("force_py36")
1345 with cache_dir() as workspace:
1346 path = (workspace / "file.py").resolve()
1347 with open(path, "w") as fh:
1349 self.invokeBlack([str(path), *PY36_ARGS])
1350 with open(path, "r") as fh:
1352 # verify cache with --target-version is separate
1353 py36_cache = black.read_cache(py36_mode)
1354 self.assertIn(str(path), py36_cache)
1355 normal_cache = black.read_cache(reg_mode)
1356 self.assertNotIn(str(path), normal_cache)
1357 self.assertEqual(actual, expected)
1360 def test_multi_file_force_py36(self) -> None:
1361 reg_mode = DEFAULT_MODE
1362 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1363 source, expected = read_data("force_py36")
1364 with cache_dir() as workspace:
1366 (workspace / "file1.py").resolve(),
1367 (workspace / "file2.py").resolve(),
1370 with open(path, "w") as fh:
1372 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1374 with open(path, "r") as fh:
1376 self.assertEqual(actual, expected)
1377 # verify cache with --target-version is separate
1378 pyi_cache = black.read_cache(py36_mode)
1379 normal_cache = black.read_cache(reg_mode)
1381 self.assertIn(str(path), pyi_cache)
1382 self.assertNotIn(str(path), normal_cache)
1384 def test_pipe_force_py36(self) -> None:
1385 source, expected = read_data("force_py36")
1386 result = CliRunner().invoke(
1388 ["-", "-q", "--target-version=py36"],
1389 input=BytesIO(source.encode("utf8")),
1391 self.assertEqual(result.exit_code, 0)
1392 actual = result.output
1393 self.assertFormatEqual(actual, expected)
1395 def test_include_exclude(self) -> None:
1396 path = THIS_DIR / "data" / "include_exclude_tests"
1397 include = re.compile(r"\.pyi?$")
1398 exclude = re.compile(r"/exclude/|/\.definitely_exclude/")
1399 report = black.Report()
1400 gitignore = PathSpec.from_lines("gitwildmatch", [])
1401 sources: List[Path] = []
1403 Path(path / "b/dont_exclude/a.py"),
1404 Path(path / "b/dont_exclude/a.pyi"),
1406 this_abs = THIS_DIR.resolve()
1408 black.gen_python_files(
1419 self.assertEqual(sorted(expected), sorted(sources))
1421 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1422 def test_exclude_for_issue_1572(self) -> None:
1423 # Exclude shouldn't touch files that were explicitly given to Black through the
1424 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1425 # https://github.com/psf/black/issues/1572
1426 path = THIS_DIR / "data" / "include_exclude_tests"
1428 exclude = r"/exclude/|a\.py"
1429 src = str(path / "b/exclude/a.py")
1430 report = black.Report()
1431 expected = [Path(path / "b/exclude/a.py")]
1438 include=re.compile(include),
1439 exclude=re.compile(exclude),
1440 extend_exclude=None,
1443 stdin_filename=None,
1446 self.assertEqual(sorted(expected), sorted(sources))
1448 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1449 def test_get_sources_with_stdin(self) -> None:
1451 exclude = r"/exclude/|a\.py"
1453 report = black.Report()
1454 expected = [Path("-")]
1461 include=re.compile(include),
1462 exclude=re.compile(exclude),
1463 extend_exclude=None,
1466 stdin_filename=None,
1469 self.assertEqual(sorted(expected), sorted(sources))
1471 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1472 def test_get_sources_with_stdin_filename(self) -> None:
1474 exclude = r"/exclude/|a\.py"
1476 report = black.Report()
1477 stdin_filename = str(THIS_DIR / "data/collections.py")
1478 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1485 include=re.compile(include),
1486 exclude=re.compile(exclude),
1487 extend_exclude=None,
1490 stdin_filename=stdin_filename,
1493 self.assertEqual(sorted(expected), sorted(sources))
1495 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1496 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
1497 # Exclude shouldn't exclude stdin_filename since it is mimicing the
1498 # file being passed directly. This is the same as
1499 # test_exclude_for_issue_1572
1500 path = THIS_DIR / "data" / "include_exclude_tests"
1502 exclude = r"/exclude/|a\.py"
1504 report = black.Report()
1505 stdin_filename = str(path / "b/exclude/a.py")
1506 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1513 include=re.compile(include),
1514 exclude=re.compile(exclude),
1515 extend_exclude=None,
1518 stdin_filename=stdin_filename,
1521 self.assertEqual(sorted(expected), sorted(sources))
1523 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1524 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
1525 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
1526 # file being passed directly. This is the same as
1527 # test_exclude_for_issue_1572
1528 path = THIS_DIR / "data" / "include_exclude_tests"
1530 extend_exclude = r"/exclude/|a\.py"
1532 report = black.Report()
1533 stdin_filename = str(path / "b/exclude/a.py")
1534 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1541 include=re.compile(include),
1542 exclude=re.compile(""),
1543 extend_exclude=re.compile(extend_exclude),
1546 stdin_filename=stdin_filename,
1549 self.assertEqual(sorted(expected), sorted(sources))
1551 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1552 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
1553 # Force exclude should exclude the file when passing it through
1555 path = THIS_DIR / "data" / "include_exclude_tests"
1557 force_exclude = r"/exclude/|a\.py"
1559 report = black.Report()
1560 stdin_filename = str(path / "b/exclude/a.py")
1567 include=re.compile(include),
1568 exclude=re.compile(""),
1569 extend_exclude=None,
1570 force_exclude=re.compile(force_exclude),
1572 stdin_filename=stdin_filename,
1575 self.assertEqual([], sorted(sources))
1577 def test_reformat_one_with_stdin(self) -> None:
1579 "black.format_stdin_to_stdout",
1580 return_value=lambda *args, **kwargs: black.Changed.YES,
1582 report = MagicMock()
1587 write_back=black.WriteBack.YES,
1591 fsts.assert_called_once()
1592 report.done.assert_called_with(path, black.Changed.YES)
1594 def test_reformat_one_with_stdin_filename(self) -> None:
1596 "black.format_stdin_to_stdout",
1597 return_value=lambda *args, **kwargs: black.Changed.YES,
1599 report = MagicMock()
1601 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1606 write_back=black.WriteBack.YES,
1610 fsts.assert_called_once_with(
1611 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1613 # __BLACK_STDIN_FILENAME__ should have been stripped
1614 report.done.assert_called_with(expected, black.Changed.YES)
1616 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1618 "black.format_stdin_to_stdout",
1619 return_value=lambda *args, **kwargs: black.Changed.YES,
1621 report = MagicMock()
1623 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1628 write_back=black.WriteBack.YES,
1632 fsts.assert_called_once_with(
1634 write_back=black.WriteBack.YES,
1635 mode=replace(DEFAULT_MODE, is_pyi=True),
1637 # __BLACK_STDIN_FILENAME__ should have been stripped
1638 report.done.assert_called_with(expected, black.Changed.YES)
1640 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1642 "black.format_stdin_to_stdout",
1643 return_value=lambda *args, **kwargs: black.Changed.YES,
1645 report = MagicMock()
1646 # Even with an existing file, since we are forcing stdin, black
1647 # should output to stdout and not modify the file inplace
1648 p = Path(str(THIS_DIR / "data/collections.py"))
1649 # Make sure is_file actually returns True
1650 self.assertTrue(p.is_file())
1651 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1656 write_back=black.WriteBack.YES,
1660 fsts.assert_called_once()
1661 # __BLACK_STDIN_FILENAME__ should have been stripped
1662 report.done.assert_called_with(expected, black.Changed.YES)
1664 def test_gitignore_exclude(self) -> None:
1665 path = THIS_DIR / "data" / "include_exclude_tests"
1666 include = re.compile(r"\.pyi?$")
1667 exclude = re.compile(r"")
1668 report = black.Report()
1669 gitignore = PathSpec.from_lines(
1670 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1672 sources: List[Path] = []
1674 Path(path / "b/dont_exclude/a.py"),
1675 Path(path / "b/dont_exclude/a.pyi"),
1677 this_abs = THIS_DIR.resolve()
1679 black.gen_python_files(
1690 self.assertEqual(sorted(expected), sorted(sources))
1692 def test_empty_include(self) -> None:
1693 path = THIS_DIR / "data" / "include_exclude_tests"
1694 report = black.Report()
1695 gitignore = PathSpec.from_lines("gitwildmatch", [])
1696 empty = re.compile(r"")
1697 sources: List[Path] = []
1699 Path(path / "b/exclude/a.pie"),
1700 Path(path / "b/exclude/a.py"),
1701 Path(path / "b/exclude/a.pyi"),
1702 Path(path / "b/dont_exclude/a.pie"),
1703 Path(path / "b/dont_exclude/a.py"),
1704 Path(path / "b/dont_exclude/a.pyi"),
1705 Path(path / "b/.definitely_exclude/a.pie"),
1706 Path(path / "b/.definitely_exclude/a.py"),
1707 Path(path / "b/.definitely_exclude/a.pyi"),
1709 this_abs = THIS_DIR.resolve()
1711 black.gen_python_files(
1715 re.compile(black.DEFAULT_EXCLUDES),
1722 self.assertEqual(sorted(expected), sorted(sources))
1724 def test_extend_exclude(self) -> None:
1725 path = THIS_DIR / "data" / "include_exclude_tests"
1726 report = black.Report()
1727 gitignore = PathSpec.from_lines("gitwildmatch", [])
1728 sources: List[Path] = []
1730 Path(path / "b/exclude/a.py"),
1731 Path(path / "b/dont_exclude/a.py"),
1733 this_abs = THIS_DIR.resolve()
1735 black.gen_python_files(
1738 re.compile(black.DEFAULT_INCLUDES),
1739 re.compile(r"\.pyi$"),
1740 re.compile(r"\.definitely_exclude"),
1746 self.assertEqual(sorted(expected), sorted(sources))
1748 def test_invalid_cli_regex(self) -> None:
1749 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1750 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1752 def test_preserves_line_endings(self) -> None:
1753 with TemporaryDirectory() as workspace:
1754 test_file = Path(workspace) / "test.py"
1755 for nl in ["\n", "\r\n"]:
1756 contents = nl.join(["def f( ):", " pass"])
1757 test_file.write_bytes(contents.encode())
1758 ff(test_file, write_back=black.WriteBack.YES)
1759 updated_contents: bytes = test_file.read_bytes()
1760 self.assertIn(nl.encode(), updated_contents)
1762 self.assertNotIn(b"\r\n", updated_contents)
1764 def test_preserves_line_endings_via_stdin(self) -> None:
1765 for nl in ["\n", "\r\n"]:
1766 contents = nl.join(["def f( ):", " pass"])
1767 runner = BlackRunner()
1768 result = runner.invoke(
1769 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1771 self.assertEqual(result.exit_code, 0)
1772 output = runner.stdout_bytes
1773 self.assertIn(nl.encode("utf8"), output)
1775 self.assertNotIn(b"\r\n", output)
1777 def test_assert_equivalent_different_asts(self) -> None:
1778 with self.assertRaises(AssertionError):
1779 black.assert_equivalent("{}", "None")
1781 def test_symlink_out_of_root_directory(self) -> None:
1783 root = THIS_DIR.resolve()
1785 include = re.compile(black.DEFAULT_INCLUDES)
1786 exclude = re.compile(black.DEFAULT_EXCLUDES)
1787 report = black.Report()
1788 gitignore = PathSpec.from_lines("gitwildmatch", [])
1789 # `child` should behave like a symlink which resolved path is clearly
1790 # outside of the `root` directory.
1791 path.iterdir.return_value = [child]
1792 child.resolve.return_value = Path("/a/b/c")
1793 child.as_posix.return_value = "/a/b/c"
1794 child.is_symlink.return_value = True
1797 black.gen_python_files(
1808 except ValueError as ve:
1809 self.fail(f"`get_python_files_in_dir()` failed: {ve}")
1810 path.iterdir.assert_called_once()
1811 child.resolve.assert_called_once()
1812 child.is_symlink.assert_called_once()
1813 # `child` should behave like a strange file which resolved path is clearly
1814 # outside of the `root` directory.
1815 child.is_symlink.return_value = False
1816 with self.assertRaises(ValueError):
1818 black.gen_python_files(
1829 path.iterdir.assert_called()
1830 self.assertEqual(path.iterdir.call_count, 2)
1831 child.resolve.assert_called()
1832 self.assertEqual(child.resolve.call_count, 2)
1833 child.is_symlink.assert_called()
1834 self.assertEqual(child.is_symlink.call_count, 2)
1836 def test_shhh_click(self) -> None:
1838 from click import _unicodefun # type: ignore
1839 except ModuleNotFoundError:
1840 self.skipTest("Incompatible Click version")
1841 if not hasattr(_unicodefun, "_verify_python3_env"):
1842 self.skipTest("Incompatible Click version")
1843 # First, let's see if Click is crashing with a preferred ASCII charset.
1844 with patch("locale.getpreferredencoding") as gpe:
1845 gpe.return_value = "ASCII"
1846 with self.assertRaises(RuntimeError):
1847 _unicodefun._verify_python3_env()
1848 # Now, let's silence Click...
1850 # ...and confirm it's silent.
1851 with patch("locale.getpreferredencoding") as gpe:
1852 gpe.return_value = "ASCII"
1854 _unicodefun._verify_python3_env()
1855 except RuntimeError as re:
1856 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1858 def test_root_logger_not_used_directly(self) -> None:
1859 def fail(*args: Any, **kwargs: Any) -> None:
1860 self.fail("Record created with root logger")
1862 with patch.multiple(
1873 def test_invalid_config_return_code(self) -> None:
1874 tmp_file = Path(black.dump_to_file())
1876 tmp_config = Path(black.dump_to_file())
1878 args = ["--config", str(tmp_config), str(tmp_file)]
1879 self.invokeBlack(args, exit_code=2, ignore_config=False)
1883 def test_parse_pyproject_toml(self) -> None:
1884 test_toml_file = THIS_DIR / "test.toml"
1885 config = black.parse_pyproject_toml(str(test_toml_file))
1886 self.assertEqual(config["verbose"], 1)
1887 self.assertEqual(config["check"], "no")
1888 self.assertEqual(config["diff"], "y")
1889 self.assertEqual(config["color"], True)
1890 self.assertEqual(config["line_length"], 79)
1891 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1892 self.assertEqual(config["exclude"], r"\.pyi?$")
1893 self.assertEqual(config["include"], r"\.py?$")
1895 def test_read_pyproject_toml(self) -> None:
1896 test_toml_file = THIS_DIR / "test.toml"
1897 fake_ctx = FakeContext()
1898 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1899 config = fake_ctx.default_map
1900 self.assertEqual(config["verbose"], "1")
1901 self.assertEqual(config["check"], "no")
1902 self.assertEqual(config["diff"], "y")
1903 self.assertEqual(config["color"], "True")
1904 self.assertEqual(config["line_length"], "79")
1905 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1906 self.assertEqual(config["exclude"], r"\.pyi?$")
1907 self.assertEqual(config["include"], r"\.py?$")
1909 def test_find_project_root(self) -> None:
1910 with TemporaryDirectory() as workspace:
1911 root = Path(workspace)
1912 test_dir = root / "test"
1915 src_dir = root / "src"
1918 root_pyproject = root / "pyproject.toml"
1919 root_pyproject.touch()
1920 src_pyproject = src_dir / "pyproject.toml"
1921 src_pyproject.touch()
1922 src_python = src_dir / "foo.py"
1926 black.find_project_root((src_dir, test_dir)), root.resolve()
1928 self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1929 self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1931 @patch("black.find_user_pyproject_toml", black.find_user_pyproject_toml.__wrapped__)
1932 def test_find_user_pyproject_toml_linux(self) -> None:
1933 if system() == "Windows":
1936 # Test if XDG_CONFIG_HOME is checked
1937 with TemporaryDirectory() as workspace:
1938 tmp_user_config = Path(workspace) / "black"
1939 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1941 black.find_user_pyproject_toml(), tmp_user_config.resolve()
1944 # Test fallback for XDG_CONFIG_HOME
1945 with patch.dict("os.environ"):
1946 os.environ.pop("XDG_CONFIG_HOME", None)
1947 fallback_user_config = Path("~/.config").expanduser() / "black"
1949 black.find_user_pyproject_toml(), fallback_user_config.resolve()
1952 def test_find_user_pyproject_toml_windows(self) -> None:
1953 if system() != "Windows":
1956 user_config_path = Path.home() / ".black"
1957 self.assertEqual(black.find_user_pyproject_toml(), user_config_path.resolve())
1959 def test_bpo_33660_workaround(self) -> None:
1960 if system() == "Windows":
1963 # https://bugs.python.org/issue33660
1965 old_cwd = Path.cwd()
1969 path = Path("workspace") / "project"
1970 report = black.Report(verbose=True)
1971 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1972 self.assertEqual(normalized_path, "workspace/project")
1974 os.chdir(str(old_cwd))
1976 def test_newline_comment_interaction(self) -> None:
1977 source = "class A:\\\r\n# type: ignore\n pass\n"
1978 output = black.format_str(source, mode=DEFAULT_MODE)
1979 black.assert_stable(source, output, mode=DEFAULT_MODE)
1981 def test_bpo_2142_workaround(self) -> None:
1983 # https://bugs.python.org/issue2142
1985 source, _ = read_data("missing_final_newline.py")
1986 # read_data adds a trailing newline
1987 source = source.rstrip()
1988 expected, _ = read_data("missing_final_newline.diff")
1989 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1990 diff_header = re.compile(
1991 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1992 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1995 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1996 self.assertEqual(result.exit_code, 0)
1999 actual = result.output
2000 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
2001 self.assertEqual(actual, expected)
2003 @pytest.mark.python2
2004 def test_docstring_reformat_for_py27(self) -> None:
2006 Check that stripping trailing whitespace from Python 2 docstrings
2007 doesn't trigger a "not equivalent to source" error
2010 b'def foo():\r\n """Testing\r\n Testing """\r\n print "Foo"\r\n'
2012 expected = 'def foo():\n """Testing\n Testing"""\n print "Foo"\n'
2014 result = CliRunner().invoke(
2016 ["-", "-q", "--target-version=py27"],
2017 input=BytesIO(source),
2020 self.assertEqual(result.exit_code, 0)
2021 actual = result.output
2022 self.assertFormatEqual(actual, expected)
2025 with open(black.__file__, "r", encoding="utf-8") as _bf:
2026 black_source_lines = _bf.readlines()
2029 def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
2030 """Show function calls `from black/__init__.py` as they happen.
2032 Register this with `sys.settrace()` in a test you're debugging.
2037 stack = len(inspect.stack()) - 19
2039 filename = frame.f_code.co_filename
2040 lineno = frame.f_lineno
2041 func_sig_lineno = lineno - 1
2042 funcname = black_source_lines[func_sig_lineno].strip()
2043 while funcname.startswith("@"):
2044 func_sig_lineno += 1
2045 funcname = black_source_lines[func_sig_lineno].strip()
2046 if "black/__init__.py" in filename:
2047 print(f"{' ' * stack}{lineno}:{funcname}")
2051 if __name__ == "__main__":
2052 unittest.main(module="test_black")