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 def test_gitingore_used_as_default(self) -> None:
1422 path = Path(THIS_DIR / "data" / "include_exclude_tests")
1423 include = re.compile(r"\.pyi?$")
1424 extend_exclude = re.compile(r"/exclude/")
1425 src = str(path / "b/")
1426 report = black.Report()
1427 expected: List[Path] = [
1428 path / "b/.definitely_exclude/a.py",
1429 path / "b/.definitely_exclude/a.pyi",
1439 extend_exclude=extend_exclude,
1442 stdin_filename=None,
1445 self.assertEqual(sorted(expected), sorted(sources))
1447 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1448 def test_exclude_for_issue_1572(self) -> None:
1449 # Exclude shouldn't touch files that were explicitly given to Black through the
1450 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1451 # https://github.com/psf/black/issues/1572
1452 path = THIS_DIR / "data" / "include_exclude_tests"
1454 exclude = r"/exclude/|a\.py"
1455 src = str(path / "b/exclude/a.py")
1456 report = black.Report()
1457 expected = [Path(path / "b/exclude/a.py")]
1464 include=re.compile(include),
1465 exclude=re.compile(exclude),
1466 extend_exclude=None,
1469 stdin_filename=None,
1472 self.assertEqual(sorted(expected), sorted(sources))
1474 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1475 def test_get_sources_with_stdin(self) -> None:
1477 exclude = r"/exclude/|a\.py"
1479 report = black.Report()
1480 expected = [Path("-")]
1487 include=re.compile(include),
1488 exclude=re.compile(exclude),
1489 extend_exclude=None,
1492 stdin_filename=None,
1495 self.assertEqual(sorted(expected), sorted(sources))
1497 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1498 def test_get_sources_with_stdin_filename(self) -> None:
1500 exclude = r"/exclude/|a\.py"
1502 report = black.Report()
1503 stdin_filename = str(THIS_DIR / "data/collections.py")
1504 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1511 include=re.compile(include),
1512 exclude=re.compile(exclude),
1513 extend_exclude=None,
1516 stdin_filename=stdin_filename,
1519 self.assertEqual(sorted(expected), sorted(sources))
1521 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1522 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
1523 # Exclude shouldn't exclude stdin_filename since it is mimicing the
1524 # file being passed directly. This is the same as
1525 # test_exclude_for_issue_1572
1526 path = THIS_DIR / "data" / "include_exclude_tests"
1528 exclude = r"/exclude/|a\.py"
1530 report = black.Report()
1531 stdin_filename = str(path / "b/exclude/a.py")
1532 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1539 include=re.compile(include),
1540 exclude=re.compile(exclude),
1541 extend_exclude=None,
1544 stdin_filename=stdin_filename,
1547 self.assertEqual(sorted(expected), sorted(sources))
1549 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1550 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
1551 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
1552 # file being passed directly. This is the same as
1553 # test_exclude_for_issue_1572
1554 path = THIS_DIR / "data" / "include_exclude_tests"
1556 extend_exclude = r"/exclude/|a\.py"
1558 report = black.Report()
1559 stdin_filename = str(path / "b/exclude/a.py")
1560 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1567 include=re.compile(include),
1568 exclude=re.compile(""),
1569 extend_exclude=re.compile(extend_exclude),
1572 stdin_filename=stdin_filename,
1575 self.assertEqual(sorted(expected), sorted(sources))
1577 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1578 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
1579 # Force exclude should exclude the file when passing it through
1581 path = THIS_DIR / "data" / "include_exclude_tests"
1583 force_exclude = r"/exclude/|a\.py"
1585 report = black.Report()
1586 stdin_filename = str(path / "b/exclude/a.py")
1593 include=re.compile(include),
1594 exclude=re.compile(""),
1595 extend_exclude=None,
1596 force_exclude=re.compile(force_exclude),
1598 stdin_filename=stdin_filename,
1601 self.assertEqual([], sorted(sources))
1603 def test_reformat_one_with_stdin(self) -> None:
1605 "black.format_stdin_to_stdout",
1606 return_value=lambda *args, **kwargs: black.Changed.YES,
1608 report = MagicMock()
1613 write_back=black.WriteBack.YES,
1617 fsts.assert_called_once()
1618 report.done.assert_called_with(path, black.Changed.YES)
1620 def test_reformat_one_with_stdin_filename(self) -> None:
1622 "black.format_stdin_to_stdout",
1623 return_value=lambda *args, **kwargs: black.Changed.YES,
1625 report = MagicMock()
1627 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1632 write_back=black.WriteBack.YES,
1636 fsts.assert_called_once_with(
1637 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1639 # __BLACK_STDIN_FILENAME__ should have been stripped
1640 report.done.assert_called_with(expected, black.Changed.YES)
1642 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1644 "black.format_stdin_to_stdout",
1645 return_value=lambda *args, **kwargs: black.Changed.YES,
1647 report = MagicMock()
1649 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1654 write_back=black.WriteBack.YES,
1658 fsts.assert_called_once_with(
1660 write_back=black.WriteBack.YES,
1661 mode=replace(DEFAULT_MODE, is_pyi=True),
1663 # __BLACK_STDIN_FILENAME__ should have been stripped
1664 report.done.assert_called_with(expected, black.Changed.YES)
1666 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1668 "black.format_stdin_to_stdout",
1669 return_value=lambda *args, **kwargs: black.Changed.YES,
1671 report = MagicMock()
1672 # Even with an existing file, since we are forcing stdin, black
1673 # should output to stdout and not modify the file inplace
1674 p = Path(str(THIS_DIR / "data/collections.py"))
1675 # Make sure is_file actually returns True
1676 self.assertTrue(p.is_file())
1677 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1682 write_back=black.WriteBack.YES,
1686 fsts.assert_called_once()
1687 # __BLACK_STDIN_FILENAME__ should have been stripped
1688 report.done.assert_called_with(expected, black.Changed.YES)
1690 def test_gitignore_exclude(self) -> None:
1691 path = THIS_DIR / "data" / "include_exclude_tests"
1692 include = re.compile(r"\.pyi?$")
1693 exclude = re.compile(r"")
1694 report = black.Report()
1695 gitignore = PathSpec.from_lines(
1696 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1698 sources: List[Path] = []
1700 Path(path / "b/dont_exclude/a.py"),
1701 Path(path / "b/dont_exclude/a.pyi"),
1703 this_abs = THIS_DIR.resolve()
1705 black.gen_python_files(
1716 self.assertEqual(sorted(expected), sorted(sources))
1718 def test_empty_include(self) -> None:
1719 path = THIS_DIR / "data" / "include_exclude_tests"
1720 report = black.Report()
1721 gitignore = PathSpec.from_lines("gitwildmatch", [])
1722 empty = re.compile(r"")
1723 sources: List[Path] = []
1725 Path(path / "b/exclude/a.pie"),
1726 Path(path / "b/exclude/a.py"),
1727 Path(path / "b/exclude/a.pyi"),
1728 Path(path / "b/dont_exclude/a.pie"),
1729 Path(path / "b/dont_exclude/a.py"),
1730 Path(path / "b/dont_exclude/a.pyi"),
1731 Path(path / "b/.definitely_exclude/a.pie"),
1732 Path(path / "b/.definitely_exclude/a.py"),
1733 Path(path / "b/.definitely_exclude/a.pyi"),
1734 Path(path / ".gitignore"),
1735 Path(path / "pyproject.toml"),
1737 this_abs = THIS_DIR.resolve()
1739 black.gen_python_files(
1743 re.compile(black.DEFAULT_EXCLUDES),
1750 self.assertEqual(sorted(expected), sorted(sources))
1752 def test_extend_exclude(self) -> None:
1753 path = THIS_DIR / "data" / "include_exclude_tests"
1754 report = black.Report()
1755 gitignore = PathSpec.from_lines("gitwildmatch", [])
1756 sources: List[Path] = []
1758 Path(path / "b/exclude/a.py"),
1759 Path(path / "b/dont_exclude/a.py"),
1761 this_abs = THIS_DIR.resolve()
1763 black.gen_python_files(
1766 re.compile(black.DEFAULT_INCLUDES),
1767 re.compile(r"\.pyi$"),
1768 re.compile(r"\.definitely_exclude"),
1774 self.assertEqual(sorted(expected), sorted(sources))
1776 def test_invalid_cli_regex(self) -> None:
1777 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1778 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1780 def test_preserves_line_endings(self) -> None:
1781 with TemporaryDirectory() as workspace:
1782 test_file = Path(workspace) / "test.py"
1783 for nl in ["\n", "\r\n"]:
1784 contents = nl.join(["def f( ):", " pass"])
1785 test_file.write_bytes(contents.encode())
1786 ff(test_file, write_back=black.WriteBack.YES)
1787 updated_contents: bytes = test_file.read_bytes()
1788 self.assertIn(nl.encode(), updated_contents)
1790 self.assertNotIn(b"\r\n", updated_contents)
1792 def test_preserves_line_endings_via_stdin(self) -> None:
1793 for nl in ["\n", "\r\n"]:
1794 contents = nl.join(["def f( ):", " pass"])
1795 runner = BlackRunner()
1796 result = runner.invoke(
1797 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1799 self.assertEqual(result.exit_code, 0)
1800 output = runner.stdout_bytes
1801 self.assertIn(nl.encode("utf8"), output)
1803 self.assertNotIn(b"\r\n", output)
1805 def test_assert_equivalent_different_asts(self) -> None:
1806 with self.assertRaises(AssertionError):
1807 black.assert_equivalent("{}", "None")
1809 def test_symlink_out_of_root_directory(self) -> None:
1811 root = THIS_DIR.resolve()
1813 include = re.compile(black.DEFAULT_INCLUDES)
1814 exclude = re.compile(black.DEFAULT_EXCLUDES)
1815 report = black.Report()
1816 gitignore = PathSpec.from_lines("gitwildmatch", [])
1817 # `child` should behave like a symlink which resolved path is clearly
1818 # outside of the `root` directory.
1819 path.iterdir.return_value = [child]
1820 child.resolve.return_value = Path("/a/b/c")
1821 child.as_posix.return_value = "/a/b/c"
1822 child.is_symlink.return_value = True
1825 black.gen_python_files(
1836 except ValueError as ve:
1837 self.fail(f"`get_python_files_in_dir()` failed: {ve}")
1838 path.iterdir.assert_called_once()
1839 child.resolve.assert_called_once()
1840 child.is_symlink.assert_called_once()
1841 # `child` should behave like a strange file which resolved path is clearly
1842 # outside of the `root` directory.
1843 child.is_symlink.return_value = False
1844 with self.assertRaises(ValueError):
1846 black.gen_python_files(
1857 path.iterdir.assert_called()
1858 self.assertEqual(path.iterdir.call_count, 2)
1859 child.resolve.assert_called()
1860 self.assertEqual(child.resolve.call_count, 2)
1861 child.is_symlink.assert_called()
1862 self.assertEqual(child.is_symlink.call_count, 2)
1864 def test_shhh_click(self) -> None:
1866 from click import _unicodefun # type: ignore
1867 except ModuleNotFoundError:
1868 self.skipTest("Incompatible Click version")
1869 if not hasattr(_unicodefun, "_verify_python3_env"):
1870 self.skipTest("Incompatible Click version")
1871 # First, let's see if Click is crashing with a preferred ASCII charset.
1872 with patch("locale.getpreferredencoding") as gpe:
1873 gpe.return_value = "ASCII"
1874 with self.assertRaises(RuntimeError):
1875 _unicodefun._verify_python3_env()
1876 # Now, let's silence Click...
1878 # ...and confirm it's silent.
1879 with patch("locale.getpreferredencoding") as gpe:
1880 gpe.return_value = "ASCII"
1882 _unicodefun._verify_python3_env()
1883 except RuntimeError as re:
1884 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1886 def test_root_logger_not_used_directly(self) -> None:
1887 def fail(*args: Any, **kwargs: Any) -> None:
1888 self.fail("Record created with root logger")
1890 with patch.multiple(
1901 def test_invalid_config_return_code(self) -> None:
1902 tmp_file = Path(black.dump_to_file())
1904 tmp_config = Path(black.dump_to_file())
1906 args = ["--config", str(tmp_config), str(tmp_file)]
1907 self.invokeBlack(args, exit_code=2, ignore_config=False)
1911 def test_parse_pyproject_toml(self) -> None:
1912 test_toml_file = THIS_DIR / "test.toml"
1913 config = black.parse_pyproject_toml(str(test_toml_file))
1914 self.assertEqual(config["verbose"], 1)
1915 self.assertEqual(config["check"], "no")
1916 self.assertEqual(config["diff"], "y")
1917 self.assertEqual(config["color"], True)
1918 self.assertEqual(config["line_length"], 79)
1919 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1920 self.assertEqual(config["exclude"], r"\.pyi?$")
1921 self.assertEqual(config["include"], r"\.py?$")
1923 def test_read_pyproject_toml(self) -> None:
1924 test_toml_file = THIS_DIR / "test.toml"
1925 fake_ctx = FakeContext()
1926 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1927 config = fake_ctx.default_map
1928 self.assertEqual(config["verbose"], "1")
1929 self.assertEqual(config["check"], "no")
1930 self.assertEqual(config["diff"], "y")
1931 self.assertEqual(config["color"], "True")
1932 self.assertEqual(config["line_length"], "79")
1933 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1934 self.assertEqual(config["exclude"], r"\.pyi?$")
1935 self.assertEqual(config["include"], r"\.py?$")
1937 def test_find_project_root(self) -> None:
1938 with TemporaryDirectory() as workspace:
1939 root = Path(workspace)
1940 test_dir = root / "test"
1943 src_dir = root / "src"
1946 root_pyproject = root / "pyproject.toml"
1947 root_pyproject.touch()
1948 src_pyproject = src_dir / "pyproject.toml"
1949 src_pyproject.touch()
1950 src_python = src_dir / "foo.py"
1954 black.find_project_root((src_dir, test_dir)), root.resolve()
1956 self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1957 self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1959 @patch("black.find_user_pyproject_toml", black.find_user_pyproject_toml.__wrapped__)
1960 def test_find_user_pyproject_toml_linux(self) -> None:
1961 if system() == "Windows":
1964 # Test if XDG_CONFIG_HOME is checked
1965 with TemporaryDirectory() as workspace:
1966 tmp_user_config = Path(workspace) / "black"
1967 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1969 black.find_user_pyproject_toml(), tmp_user_config.resolve()
1972 # Test fallback for XDG_CONFIG_HOME
1973 with patch.dict("os.environ"):
1974 os.environ.pop("XDG_CONFIG_HOME", None)
1975 fallback_user_config = Path("~/.config").expanduser() / "black"
1977 black.find_user_pyproject_toml(), fallback_user_config.resolve()
1980 def test_find_user_pyproject_toml_windows(self) -> None:
1981 if system() != "Windows":
1984 user_config_path = Path.home() / ".black"
1985 self.assertEqual(black.find_user_pyproject_toml(), user_config_path.resolve())
1987 def test_bpo_33660_workaround(self) -> None:
1988 if system() == "Windows":
1991 # https://bugs.python.org/issue33660
1993 old_cwd = Path.cwd()
1997 path = Path("workspace") / "project"
1998 report = black.Report(verbose=True)
1999 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
2000 self.assertEqual(normalized_path, "workspace/project")
2002 os.chdir(str(old_cwd))
2004 def test_newline_comment_interaction(self) -> None:
2005 source = "class A:\\\r\n# type: ignore\n pass\n"
2006 output = black.format_str(source, mode=DEFAULT_MODE)
2007 black.assert_stable(source, output, mode=DEFAULT_MODE)
2009 def test_bpo_2142_workaround(self) -> None:
2011 # https://bugs.python.org/issue2142
2013 source, _ = read_data("missing_final_newline.py")
2014 # read_data adds a trailing newline
2015 source = source.rstrip()
2016 expected, _ = read_data("missing_final_newline.diff")
2017 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
2018 diff_header = re.compile(
2019 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
2020 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
2023 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
2024 self.assertEqual(result.exit_code, 0)
2027 actual = result.output
2028 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
2029 self.assertEqual(actual, expected)
2031 @pytest.mark.python2
2032 def test_docstring_reformat_for_py27(self) -> None:
2034 Check that stripping trailing whitespace from Python 2 docstrings
2035 doesn't trigger a "not equivalent to source" error
2038 b'def foo():\r\n """Testing\r\n Testing """\r\n print "Foo"\r\n'
2040 expected = 'def foo():\n """Testing\n Testing"""\n print "Foo"\n'
2042 result = CliRunner().invoke(
2044 ["-", "-q", "--target-version=py27"],
2045 input=BytesIO(source),
2048 self.assertEqual(result.exit_code, 0)
2049 actual = result.output
2050 self.assertFormatEqual(actual, expected)
2053 with open(black.__file__, "r", encoding="utf-8") as _bf:
2054 black_source_lines = _bf.readlines()
2057 def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
2058 """Show function calls `from black/__init__.py` as they happen.
2060 Register this with `sys.settrace()` in a test you're debugging.
2065 stack = len(inspect.stack()) - 19
2067 filename = frame.f_code.co_filename
2068 lineno = frame.f_lineno
2069 func_sig_lineno = lineno - 1
2070 funcname = black_source_lines[func_sig_lineno].strip()
2071 while funcname.startswith("@"):
2072 func_sig_lineno += 1
2073 funcname = black_source_lines[func_sig_lineno].strip()
2074 if "black/__init__.py" in filename:
2075 print(f"{' ' * stack}{lineno}:{funcname}")
2079 if __name__ == "__main__":
2080 unittest.main(module="test_black")