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
28 from unittest.mock import patch, MagicMock
31 from click import unstyle
32 from click.testing import CliRunner
35 from black import Feature, TargetVersion
37 from pathspec import PathSpec
39 # Import other test classes
40 from tests.util import (
50 from .test_primer import PrimerCLITests # noqa: F401
53 THIS_FILE = Path(__file__)
60 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
66 def cache_dir(exists: bool = True) -> Iterator[Path]:
67 with TemporaryDirectory() as workspace:
68 cache_dir = Path(workspace)
70 cache_dir = cache_dir / "new"
71 with patch("black.CACHE_DIR", cache_dir):
76 def event_loop() -> Iterator[None]:
77 policy = asyncio.get_event_loop_policy()
78 loop = policy.new_event_loop()
79 asyncio.set_event_loop(loop)
87 class FakeContext(click.Context):
88 """A fake click Context for when calling functions that need it."""
90 def __init__(self) -> None:
91 self.default_map: Dict[str, Any] = {}
94 class FakeParameter(click.Parameter):
95 """A fake click Parameter for when calling functions that need it."""
97 def __init__(self) -> None:
101 class BlackRunner(CliRunner):
102 """Modify CliRunner so that stderr is not merged with stdout.
104 This is a hack that can be removed once we depend on Click 7.x"""
106 def __init__(self) -> None:
107 self.stderrbuf = BytesIO()
108 self.stdoutbuf = BytesIO()
109 self.stdout_bytes = b""
110 self.stderr_bytes = b""
114 def isolation(self, *args: Any, **kwargs: Any) -> Generator[BinaryIO, None, None]:
115 with super().isolation(*args, **kwargs) as output:
117 hold_stderr = sys.stderr
118 sys.stderr = TextIOWrapper(self.stderrbuf, encoding=self.charset)
121 self.stdout_bytes = sys.stdout.buffer.getvalue() # type: ignore
122 self.stderr_bytes = sys.stderr.buffer.getvalue() # type: ignore
123 sys.stderr = hold_stderr
126 class BlackTestCase(BlackBaseTestCase):
128 self, args: List[str], exit_code: int = 0, ignore_config: bool = True
130 runner = BlackRunner()
132 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
133 result = runner.invoke(black.main, args)
138 f"Failed with args: {args}\n"
139 f"stdout: {runner.stdout_bytes.decode()!r}\n"
140 f"stderr: {runner.stderr_bytes.decode()!r}\n"
141 f"exception: {result.exception}"
145 @patch("black.dump_to_file", dump_to_stderr)
146 def test_empty(self) -> None:
147 source = expected = ""
149 self.assertFormatEqual(expected, actual)
150 black.assert_equivalent(source, actual)
151 black.assert_stable(source, actual, DEFAULT_MODE)
153 def test_empty_ff(self) -> None:
155 tmp_file = Path(black.dump_to_file())
157 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
158 with open(tmp_file, encoding="utf8") as f:
162 self.assertFormatEqual(expected, actual)
164 def test_piping(self) -> None:
165 source, expected = read_data("src/black/__init__", data=False)
166 result = BlackRunner().invoke(
168 ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
169 input=BytesIO(source.encode("utf8")),
171 self.assertEqual(result.exit_code, 0)
172 self.assertFormatEqual(expected, result.output)
173 black.assert_equivalent(source, result.output)
174 black.assert_stable(source, result.output, DEFAULT_MODE)
176 def test_piping_diff(self) -> None:
177 diff_header = re.compile(
178 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
181 source, _ = read_data("expression.py")
182 expected, _ = read_data("expression.diff")
183 config = THIS_DIR / "data" / "empty_pyproject.toml"
187 f"--line-length={black.DEFAULT_LINE_LENGTH}",
189 f"--config={config}",
191 result = BlackRunner().invoke(
192 black.main, args, input=BytesIO(source.encode("utf8"))
194 self.assertEqual(result.exit_code, 0)
195 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
196 actual = actual.rstrip() + "\n" # the diff output has a trailing space
197 self.assertEqual(expected, actual)
199 def test_piping_diff_with_color(self) -> None:
200 source, _ = read_data("expression.py")
201 config = THIS_DIR / "data" / "empty_pyproject.toml"
205 f"--line-length={black.DEFAULT_LINE_LENGTH}",
208 f"--config={config}",
210 result = BlackRunner().invoke(
211 black.main, args, input=BytesIO(source.encode("utf8"))
213 actual = result.output
214 # Again, the contents are checked in a different test, so only look for colors.
215 self.assertIn("\033[1;37m", actual)
216 self.assertIn("\033[36m", actual)
217 self.assertIn("\033[32m", actual)
218 self.assertIn("\033[31m", actual)
219 self.assertIn("\033[0m", actual)
221 @patch("black.dump_to_file", dump_to_stderr)
222 def _test_wip(self) -> None:
223 source, expected = read_data("wip")
224 sys.settrace(tracefunc)
227 experimental_string_processing=False,
228 target_versions={black.TargetVersion.PY38},
230 actual = fs(source, mode=mode)
232 self.assertFormatEqual(expected, actual)
233 black.assert_equivalent(source, actual)
234 black.assert_stable(source, actual, black.FileMode())
236 @unittest.expectedFailure
237 @patch("black.dump_to_file", dump_to_stderr)
238 def test_trailing_comma_optional_parens_stability1(self) -> None:
239 source, _expected = read_data("trailing_comma_optional_parens1")
241 black.assert_stable(source, actual, DEFAULT_MODE)
243 @unittest.expectedFailure
244 @patch("black.dump_to_file", dump_to_stderr)
245 def test_trailing_comma_optional_parens_stability2(self) -> None:
246 source, _expected = read_data("trailing_comma_optional_parens2")
248 black.assert_stable(source, actual, DEFAULT_MODE)
250 @unittest.expectedFailure
251 @patch("black.dump_to_file", dump_to_stderr)
252 def test_trailing_comma_optional_parens_stability3(self) -> None:
253 source, _expected = read_data("trailing_comma_optional_parens3")
255 black.assert_stable(source, actual, DEFAULT_MODE)
257 @patch("black.dump_to_file", dump_to_stderr)
258 def test_pep_572(self) -> None:
259 source, expected = read_data("pep_572")
261 self.assertFormatEqual(expected, actual)
262 black.assert_stable(source, actual, DEFAULT_MODE)
263 if sys.version_info >= (3, 8):
264 black.assert_equivalent(source, actual)
266 @patch("black.dump_to_file", dump_to_stderr)
267 def test_pep_572_remove_parens(self) -> None:
268 source, expected = read_data("pep_572_remove_parens")
270 self.assertFormatEqual(expected, actual)
271 black.assert_stable(source, actual, DEFAULT_MODE)
272 if sys.version_info >= (3, 8):
273 black.assert_equivalent(source, actual)
275 def test_pep_572_version_detection(self) -> None:
276 source, _ = read_data("pep_572")
277 root = black.lib2to3_parse(source)
278 features = black.get_features_used(root)
279 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
280 versions = black.detect_target_versions(root)
281 self.assertIn(black.TargetVersion.PY38, versions)
283 def test_expression_ff(self) -> None:
284 source, expected = read_data("expression")
285 tmp_file = Path(black.dump_to_file(source))
287 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
288 with open(tmp_file, encoding="utf8") as f:
292 self.assertFormatEqual(expected, actual)
293 with patch("black.dump_to_file", dump_to_stderr):
294 black.assert_equivalent(source, actual)
295 black.assert_stable(source, actual, DEFAULT_MODE)
297 def test_expression_diff(self) -> None:
298 source, _ = read_data("expression.py")
299 expected, _ = read_data("expression.diff")
300 tmp_file = Path(black.dump_to_file(source))
301 diff_header = re.compile(
302 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
303 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
306 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
307 self.assertEqual(result.exit_code, 0)
310 actual = result.output
311 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
312 if expected != actual:
313 dump = black.dump_to_file(actual)
315 "Expected diff isn't equal to the actual. If you made changes to"
316 " expression.py and this is an anticipated difference, overwrite"
317 f" tests/data/expression.diff with {dump}"
319 self.assertEqual(expected, actual, msg)
321 def test_expression_diff_with_color(self) -> None:
322 source, _ = read_data("expression.py")
323 expected, _ = read_data("expression.diff")
324 tmp_file = Path(black.dump_to_file(source))
326 result = BlackRunner().invoke(
327 black.main, ["--diff", "--color", str(tmp_file)]
331 actual = result.output
332 # We check the contents of the diff in `test_expression_diff`. All
333 # we need to check here is that color codes exist in the result.
334 self.assertIn("\033[1;37m", actual)
335 self.assertIn("\033[36m", actual)
336 self.assertIn("\033[32m", actual)
337 self.assertIn("\033[31m", actual)
338 self.assertIn("\033[0m", actual)
340 @patch("black.dump_to_file", dump_to_stderr)
341 def test_pep_570(self) -> None:
342 source, expected = read_data("pep_570")
344 self.assertFormatEqual(expected, actual)
345 black.assert_stable(source, actual, DEFAULT_MODE)
346 if sys.version_info >= (3, 8):
347 black.assert_equivalent(source, actual)
349 def test_detect_pos_only_arguments(self) -> None:
350 source, _ = read_data("pep_570")
351 root = black.lib2to3_parse(source)
352 features = black.get_features_used(root)
353 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
354 versions = black.detect_target_versions(root)
355 self.assertIn(black.TargetVersion.PY38, versions)
357 @patch("black.dump_to_file", dump_to_stderr)
358 def test_string_quotes(self) -> None:
359 source, expected = read_data("string_quotes")
361 self.assertFormatEqual(expected, actual)
362 black.assert_equivalent(source, actual)
363 black.assert_stable(source, actual, DEFAULT_MODE)
364 mode = replace(DEFAULT_MODE, string_normalization=False)
365 not_normalized = fs(source, mode=mode)
366 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
367 black.assert_equivalent(source, not_normalized)
368 black.assert_stable(source, not_normalized, mode=mode)
370 @patch("black.dump_to_file", dump_to_stderr)
371 def test_docstring_no_string_normalization(self) -> None:
372 """Like test_docstring but with string normalization off."""
373 source, expected = read_data("docstring_no_string_normalization")
374 mode = replace(DEFAULT_MODE, string_normalization=False)
375 actual = fs(source, mode=mode)
376 self.assertFormatEqual(expected, actual)
377 black.assert_equivalent(source, actual)
378 black.assert_stable(source, actual, mode)
380 def test_long_strings_flag_disabled(self) -> None:
381 """Tests for turning off the string processing logic."""
382 source, expected = read_data("long_strings_flag_disabled")
383 mode = replace(DEFAULT_MODE, experimental_string_processing=False)
384 actual = fs(source, mode=mode)
385 self.assertFormatEqual(expected, actual)
386 black.assert_stable(expected, actual, mode)
388 @patch("black.dump_to_file", dump_to_stderr)
389 def test_numeric_literals(self) -> None:
390 source, expected = read_data("numeric_literals")
391 mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
392 actual = fs(source, mode=mode)
393 self.assertFormatEqual(expected, actual)
394 black.assert_equivalent(source, actual)
395 black.assert_stable(source, actual, mode)
397 @patch("black.dump_to_file", dump_to_stderr)
398 def test_numeric_literals_ignoring_underscores(self) -> None:
399 source, expected = read_data("numeric_literals_skip_underscores")
400 mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
401 actual = fs(source, mode=mode)
402 self.assertFormatEqual(expected, actual)
403 black.assert_equivalent(source, actual)
404 black.assert_stable(source, actual, mode)
406 def test_skip_magic_trailing_comma(self) -> None:
407 source, _ = read_data("expression.py")
408 expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
409 tmp_file = Path(black.dump_to_file(source))
410 diff_header = re.compile(
411 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
412 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
415 result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
416 self.assertEqual(result.exit_code, 0)
419 actual = result.output
420 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
421 actual = actual.rstrip() + "\n" # the diff output has a trailing space
422 if expected != actual:
423 dump = black.dump_to_file(actual)
425 "Expected diff isn't equal to the actual. If you made changes to"
426 " expression.py and this is an anticipated difference, overwrite"
427 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
429 self.assertEqual(expected, actual, msg)
431 @patch("black.dump_to_file", dump_to_stderr)
432 def test_python2_print_function(self) -> None:
433 source, expected = read_data("python2_print_function")
434 mode = replace(DEFAULT_MODE, target_versions={TargetVersion.PY27})
435 actual = fs(source, mode=mode)
436 self.assertFormatEqual(expected, actual)
437 black.assert_equivalent(source, actual)
438 black.assert_stable(source, actual, mode)
440 @patch("black.dump_to_file", dump_to_stderr)
441 def test_stub(self) -> None:
442 mode = replace(DEFAULT_MODE, is_pyi=True)
443 source, expected = read_data("stub.pyi")
444 actual = fs(source, mode=mode)
445 self.assertFormatEqual(expected, actual)
446 black.assert_stable(source, actual, mode)
448 @patch("black.dump_to_file", dump_to_stderr)
449 def test_async_as_identifier(self) -> None:
450 source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
451 source, expected = read_data("async_as_identifier")
453 self.assertFormatEqual(expected, actual)
454 major, minor = sys.version_info[:2]
455 if major < 3 or (major <= 3 and minor < 7):
456 black.assert_equivalent(source, actual)
457 black.assert_stable(source, actual, DEFAULT_MODE)
458 # ensure black can parse this when the target is 3.6
459 self.invokeBlack([str(source_path), "--target-version", "py36"])
460 # but not on 3.7, because async/await is no longer an identifier
461 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
463 @patch("black.dump_to_file", dump_to_stderr)
464 def test_python37(self) -> None:
465 source_path = (THIS_DIR / "data" / "python37.py").resolve()
466 source, expected = read_data("python37")
468 self.assertFormatEqual(expected, actual)
469 major, minor = sys.version_info[:2]
470 if major > 3 or (major == 3 and minor >= 7):
471 black.assert_equivalent(source, actual)
472 black.assert_stable(source, actual, DEFAULT_MODE)
473 # ensure black can parse this when the target is 3.7
474 self.invokeBlack([str(source_path), "--target-version", "py37"])
475 # but not on 3.6, because we use async as a reserved keyword
476 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
478 @patch("black.dump_to_file", dump_to_stderr)
479 def test_python38(self) -> None:
480 source, expected = read_data("python38")
482 self.assertFormatEqual(expected, actual)
483 major, minor = sys.version_info[:2]
484 if major > 3 or (major == 3 and minor >= 8):
485 black.assert_equivalent(source, actual)
486 black.assert_stable(source, actual, DEFAULT_MODE)
488 @patch("black.dump_to_file", dump_to_stderr)
489 def test_python39(self) -> None:
490 source, expected = read_data("python39")
492 self.assertFormatEqual(expected, actual)
493 major, minor = sys.version_info[:2]
494 if major > 3 or (major == 3 and minor >= 9):
495 black.assert_equivalent(source, actual)
496 black.assert_stable(source, actual, DEFAULT_MODE)
498 def test_tab_comment_indentation(self) -> None:
499 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
500 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
501 self.assertFormatEqual(contents_spc, fs(contents_spc))
502 self.assertFormatEqual(contents_spc, fs(contents_tab))
504 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
505 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
506 self.assertFormatEqual(contents_spc, fs(contents_spc))
507 self.assertFormatEqual(contents_spc, fs(contents_tab))
509 # mixed tabs and spaces (valid Python 2 code)
510 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
511 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
512 self.assertFormatEqual(contents_spc, fs(contents_spc))
513 self.assertFormatEqual(contents_spc, fs(contents_tab))
515 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
516 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
517 self.assertFormatEqual(contents_spc, fs(contents_spc))
518 self.assertFormatEqual(contents_spc, fs(contents_tab))
520 def test_report_verbose(self) -> None:
521 report = black.Report(verbose=True)
525 def out(msg: str, **kwargs: Any) -> None:
526 out_lines.append(msg)
528 def err(msg: str, **kwargs: Any) -> None:
529 err_lines.append(msg)
531 with patch("black.out", out), patch("black.err", err):
532 report.done(Path("f1"), black.Changed.NO)
533 self.assertEqual(len(out_lines), 1)
534 self.assertEqual(len(err_lines), 0)
535 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
536 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
537 self.assertEqual(report.return_code, 0)
538 report.done(Path("f2"), black.Changed.YES)
539 self.assertEqual(len(out_lines), 2)
540 self.assertEqual(len(err_lines), 0)
541 self.assertEqual(out_lines[-1], "reformatted f2")
543 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
545 report.done(Path("f3"), black.Changed.CACHED)
546 self.assertEqual(len(out_lines), 3)
547 self.assertEqual(len(err_lines), 0)
549 out_lines[-1], "f3 wasn't modified on disk since last run."
552 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
554 self.assertEqual(report.return_code, 0)
556 self.assertEqual(report.return_code, 1)
558 report.failed(Path("e1"), "boom")
559 self.assertEqual(len(out_lines), 3)
560 self.assertEqual(len(err_lines), 1)
561 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
563 unstyle(str(report)),
564 "1 file reformatted, 2 files left unchanged, 1 file failed to"
567 self.assertEqual(report.return_code, 123)
568 report.done(Path("f3"), black.Changed.YES)
569 self.assertEqual(len(out_lines), 4)
570 self.assertEqual(len(err_lines), 1)
571 self.assertEqual(out_lines[-1], "reformatted f3")
573 unstyle(str(report)),
574 "2 files reformatted, 2 files left unchanged, 1 file failed to"
577 self.assertEqual(report.return_code, 123)
578 report.failed(Path("e2"), "boom")
579 self.assertEqual(len(out_lines), 4)
580 self.assertEqual(len(err_lines), 2)
581 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
583 unstyle(str(report)),
584 "2 files reformatted, 2 files left unchanged, 2 files failed to"
587 self.assertEqual(report.return_code, 123)
588 report.path_ignored(Path("wat"), "no match")
589 self.assertEqual(len(out_lines), 5)
590 self.assertEqual(len(err_lines), 2)
591 self.assertEqual(out_lines[-1], "wat ignored: no match")
593 unstyle(str(report)),
594 "2 files reformatted, 2 files left unchanged, 2 files failed to"
597 self.assertEqual(report.return_code, 123)
598 report.done(Path("f4"), black.Changed.NO)
599 self.assertEqual(len(out_lines), 6)
600 self.assertEqual(len(err_lines), 2)
601 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
603 unstyle(str(report)),
604 "2 files reformatted, 3 files left unchanged, 2 files failed to"
607 self.assertEqual(report.return_code, 123)
610 unstyle(str(report)),
611 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
612 " would fail to reformat.",
617 unstyle(str(report)),
618 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
619 " would fail to reformat.",
622 def test_report_quiet(self) -> None:
623 report = black.Report(quiet=True)
627 def out(msg: str, **kwargs: Any) -> None:
628 out_lines.append(msg)
630 def err(msg: str, **kwargs: Any) -> None:
631 err_lines.append(msg)
633 with patch("black.out", out), patch("black.err", err):
634 report.done(Path("f1"), black.Changed.NO)
635 self.assertEqual(len(out_lines), 0)
636 self.assertEqual(len(err_lines), 0)
637 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
638 self.assertEqual(report.return_code, 0)
639 report.done(Path("f2"), black.Changed.YES)
640 self.assertEqual(len(out_lines), 0)
641 self.assertEqual(len(err_lines), 0)
643 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
645 report.done(Path("f3"), black.Changed.CACHED)
646 self.assertEqual(len(out_lines), 0)
647 self.assertEqual(len(err_lines), 0)
649 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
651 self.assertEqual(report.return_code, 0)
653 self.assertEqual(report.return_code, 1)
655 report.failed(Path("e1"), "boom")
656 self.assertEqual(len(out_lines), 0)
657 self.assertEqual(len(err_lines), 1)
658 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
660 unstyle(str(report)),
661 "1 file reformatted, 2 files left unchanged, 1 file failed to"
664 self.assertEqual(report.return_code, 123)
665 report.done(Path("f3"), black.Changed.YES)
666 self.assertEqual(len(out_lines), 0)
667 self.assertEqual(len(err_lines), 1)
669 unstyle(str(report)),
670 "2 files reformatted, 2 files left unchanged, 1 file failed to"
673 self.assertEqual(report.return_code, 123)
674 report.failed(Path("e2"), "boom")
675 self.assertEqual(len(out_lines), 0)
676 self.assertEqual(len(err_lines), 2)
677 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
679 unstyle(str(report)),
680 "2 files reformatted, 2 files left unchanged, 2 files failed to"
683 self.assertEqual(report.return_code, 123)
684 report.path_ignored(Path("wat"), "no match")
685 self.assertEqual(len(out_lines), 0)
686 self.assertEqual(len(err_lines), 2)
688 unstyle(str(report)),
689 "2 files reformatted, 2 files left unchanged, 2 files failed to"
692 self.assertEqual(report.return_code, 123)
693 report.done(Path("f4"), black.Changed.NO)
694 self.assertEqual(len(out_lines), 0)
695 self.assertEqual(len(err_lines), 2)
697 unstyle(str(report)),
698 "2 files reformatted, 3 files left unchanged, 2 files failed to"
701 self.assertEqual(report.return_code, 123)
704 unstyle(str(report)),
705 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
706 " would fail to reformat.",
711 unstyle(str(report)),
712 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
713 " would fail to reformat.",
716 def test_report_normal(self) -> None:
717 report = black.Report()
721 def out(msg: str, **kwargs: Any) -> None:
722 out_lines.append(msg)
724 def err(msg: str, **kwargs: Any) -> None:
725 err_lines.append(msg)
727 with patch("black.out", out), patch("black.err", err):
728 report.done(Path("f1"), black.Changed.NO)
729 self.assertEqual(len(out_lines), 0)
730 self.assertEqual(len(err_lines), 0)
731 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
732 self.assertEqual(report.return_code, 0)
733 report.done(Path("f2"), black.Changed.YES)
734 self.assertEqual(len(out_lines), 1)
735 self.assertEqual(len(err_lines), 0)
736 self.assertEqual(out_lines[-1], "reformatted f2")
738 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
740 report.done(Path("f3"), black.Changed.CACHED)
741 self.assertEqual(len(out_lines), 1)
742 self.assertEqual(len(err_lines), 0)
743 self.assertEqual(out_lines[-1], "reformatted f2")
745 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
747 self.assertEqual(report.return_code, 0)
749 self.assertEqual(report.return_code, 1)
751 report.failed(Path("e1"), "boom")
752 self.assertEqual(len(out_lines), 1)
753 self.assertEqual(len(err_lines), 1)
754 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
756 unstyle(str(report)),
757 "1 file reformatted, 2 files left unchanged, 1 file failed to"
760 self.assertEqual(report.return_code, 123)
761 report.done(Path("f3"), black.Changed.YES)
762 self.assertEqual(len(out_lines), 2)
763 self.assertEqual(len(err_lines), 1)
764 self.assertEqual(out_lines[-1], "reformatted f3")
766 unstyle(str(report)),
767 "2 files reformatted, 2 files left unchanged, 1 file failed to"
770 self.assertEqual(report.return_code, 123)
771 report.failed(Path("e2"), "boom")
772 self.assertEqual(len(out_lines), 2)
773 self.assertEqual(len(err_lines), 2)
774 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
776 unstyle(str(report)),
777 "2 files reformatted, 2 files left unchanged, 2 files failed to"
780 self.assertEqual(report.return_code, 123)
781 report.path_ignored(Path("wat"), "no match")
782 self.assertEqual(len(out_lines), 2)
783 self.assertEqual(len(err_lines), 2)
785 unstyle(str(report)),
786 "2 files reformatted, 2 files left unchanged, 2 files failed to"
789 self.assertEqual(report.return_code, 123)
790 report.done(Path("f4"), black.Changed.NO)
791 self.assertEqual(len(out_lines), 2)
792 self.assertEqual(len(err_lines), 2)
794 unstyle(str(report)),
795 "2 files reformatted, 3 files left unchanged, 2 files failed to"
798 self.assertEqual(report.return_code, 123)
801 unstyle(str(report)),
802 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
803 " would fail to reformat.",
808 unstyle(str(report)),
809 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
810 " would fail to reformat.",
813 def test_lib2to3_parse(self) -> None:
814 with self.assertRaises(black.InvalidInput):
815 black.lib2to3_parse("invalid syntax")
818 black.lib2to3_parse(straddling)
819 black.lib2to3_parse(straddling, {TargetVersion.PY27})
820 black.lib2to3_parse(straddling, {TargetVersion.PY36})
821 black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36})
824 black.lib2to3_parse(py2_only)
825 black.lib2to3_parse(py2_only, {TargetVersion.PY27})
826 with self.assertRaises(black.InvalidInput):
827 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
828 with self.assertRaises(black.InvalidInput):
829 black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36})
831 py3_only = "exec(x, end=y)"
832 black.lib2to3_parse(py3_only)
833 with self.assertRaises(black.InvalidInput):
834 black.lib2to3_parse(py3_only, {TargetVersion.PY27})
835 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
836 black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36})
838 def test_get_features_used_decorator(self) -> None:
839 # Test the feature detection of new decorator syntax
840 # since this makes some test cases of test_get_features_used()
841 # fails if it fails, this is tested first so that a useful case
843 simples, relaxed = read_data("decorators")
844 # skip explanation comments at the top of the file
845 for simple_test in simples.split("##")[1:]:
846 node = black.lib2to3_parse(simple_test)
847 decorator = str(node.children[0].children[0]).strip()
849 Feature.RELAXED_DECORATORS,
850 black.get_features_used(node),
852 f"decorator '{decorator}' follows python<=3.8 syntax"
853 "but is detected as 3.9+"
854 # f"The full node is\n{node!r}"
857 # skip the '# output' comment at the top of the output part
858 for relaxed_test in relaxed.split("##")[1:]:
859 node = black.lib2to3_parse(relaxed_test)
860 decorator = str(node.children[0].children[0]).strip()
862 Feature.RELAXED_DECORATORS,
863 black.get_features_used(node),
865 f"decorator '{decorator}' uses python3.9+ syntax"
866 "but is detected as python<=3.8"
867 # f"The full node is\n{node!r}"
871 def test_get_features_used(self) -> None:
872 node = black.lib2to3_parse("def f(*, arg): ...\n")
873 self.assertEqual(black.get_features_used(node), set())
874 node = black.lib2to3_parse("def f(*, arg,): ...\n")
875 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
876 node = black.lib2to3_parse("f(*arg,)\n")
878 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
880 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
881 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
882 node = black.lib2to3_parse("123_456\n")
883 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
884 node = black.lib2to3_parse("123456\n")
885 self.assertEqual(black.get_features_used(node), set())
886 source, expected = read_data("function")
887 node = black.lib2to3_parse(source)
888 expected_features = {
889 Feature.TRAILING_COMMA_IN_CALL,
890 Feature.TRAILING_COMMA_IN_DEF,
893 self.assertEqual(black.get_features_used(node), expected_features)
894 node = black.lib2to3_parse(expected)
895 self.assertEqual(black.get_features_used(node), expected_features)
896 source, expected = read_data("expression")
897 node = black.lib2to3_parse(source)
898 self.assertEqual(black.get_features_used(node), set())
899 node = black.lib2to3_parse(expected)
900 self.assertEqual(black.get_features_used(node), set())
902 def test_get_future_imports(self) -> None:
903 node = black.lib2to3_parse("\n")
904 self.assertEqual(set(), black.get_future_imports(node))
905 node = black.lib2to3_parse("from __future__ import black\n")
906 self.assertEqual({"black"}, black.get_future_imports(node))
907 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
908 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
909 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
910 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
911 node = black.lib2to3_parse(
912 "from __future__ import multiple\nfrom __future__ import imports\n"
914 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
915 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
916 self.assertEqual({"black"}, black.get_future_imports(node))
917 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
918 self.assertEqual({"black"}, black.get_future_imports(node))
919 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
920 self.assertEqual(set(), black.get_future_imports(node))
921 node = black.lib2to3_parse("from some.module import black\n")
922 self.assertEqual(set(), black.get_future_imports(node))
923 node = black.lib2to3_parse(
924 "from __future__ import unicode_literals as _unicode_literals"
926 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
927 node = black.lib2to3_parse(
928 "from __future__ import unicode_literals as _lol, print"
930 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
932 def test_debug_visitor(self) -> None:
933 source, _ = read_data("debug_visitor.py")
934 expected, _ = read_data("debug_visitor.out")
938 def out(msg: str, **kwargs: Any) -> None:
939 out_lines.append(msg)
941 def err(msg: str, **kwargs: Any) -> None:
942 err_lines.append(msg)
944 with patch("black.out", out), patch("black.err", err):
945 black.DebugVisitor.show(source)
946 actual = "\n".join(out_lines) + "\n"
948 if expected != actual:
949 log_name = black.dump_to_file(*out_lines)
953 f"AST print out is different. Actual version dumped to {log_name}",
956 def test_format_file_contents(self) -> None:
959 with self.assertRaises(black.NothingChanged):
960 black.format_file_contents(empty, mode=mode, fast=False)
962 with self.assertRaises(black.NothingChanged):
963 black.format_file_contents(just_nl, mode=mode, fast=False)
964 same = "j = [1, 2, 3]\n"
965 with self.assertRaises(black.NothingChanged):
966 black.format_file_contents(same, mode=mode, fast=False)
967 different = "j = [1,2,3]"
969 actual = black.format_file_contents(different, mode=mode, fast=False)
970 self.assertEqual(expected, actual)
971 invalid = "return if you can"
972 with self.assertRaises(black.InvalidInput) as e:
973 black.format_file_contents(invalid, mode=mode, fast=False)
974 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
976 def test_endmarker(self) -> None:
977 n = black.lib2to3_parse("\n")
978 self.assertEqual(n.type, black.syms.file_input)
979 self.assertEqual(len(n.children), 1)
980 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
982 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
983 def test_assertFormatEqual(self) -> None:
987 def out(msg: str, **kwargs: Any) -> None:
988 out_lines.append(msg)
990 def err(msg: str, **kwargs: Any) -> None:
991 err_lines.append(msg)
993 with patch("black.out", out), patch("black.err", err):
994 with self.assertRaises(AssertionError):
995 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
997 out_str = "".join(out_lines)
998 self.assertTrue("Expected tree:" in out_str)
999 self.assertTrue("Actual tree:" in out_str)
1000 self.assertEqual("".join(err_lines), "")
1002 def test_cache_broken_file(self) -> None:
1004 with cache_dir() as workspace:
1005 cache_file = black.get_cache_file(mode)
1006 with cache_file.open("w") as fobj:
1007 fobj.write("this is not a pickle")
1008 self.assertEqual(black.read_cache(mode), {})
1009 src = (workspace / "test.py").resolve()
1010 with src.open("w") as fobj:
1011 fobj.write("print('hello')")
1012 self.invokeBlack([str(src)])
1013 cache = black.read_cache(mode)
1014 self.assertIn(str(src), cache)
1016 def test_cache_single_file_already_cached(self) -> None:
1018 with cache_dir() as workspace:
1019 src = (workspace / "test.py").resolve()
1020 with src.open("w") as fobj:
1021 fobj.write("print('hello')")
1022 black.write_cache({}, [src], mode)
1023 self.invokeBlack([str(src)])
1024 with src.open("r") as fobj:
1025 self.assertEqual(fobj.read(), "print('hello')")
1028 def test_cache_multiple_files(self) -> None:
1030 with cache_dir() as workspace, patch(
1031 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1033 one = (workspace / "one.py").resolve()
1034 with one.open("w") as fobj:
1035 fobj.write("print('hello')")
1036 two = (workspace / "two.py").resolve()
1037 with two.open("w") as fobj:
1038 fobj.write("print('hello')")
1039 black.write_cache({}, [one], mode)
1040 self.invokeBlack([str(workspace)])
1041 with one.open("r") as fobj:
1042 self.assertEqual(fobj.read(), "print('hello')")
1043 with two.open("r") as fobj:
1044 self.assertEqual(fobj.read(), 'print("hello")\n')
1045 cache = black.read_cache(mode)
1046 self.assertIn(str(one), cache)
1047 self.assertIn(str(two), cache)
1049 def test_no_cache_when_writeback_diff(self) -> None:
1051 with cache_dir() as workspace:
1052 src = (workspace / "test.py").resolve()
1053 with src.open("w") as fobj:
1054 fobj.write("print('hello')")
1055 with patch("black.read_cache") as read_cache, patch(
1058 self.invokeBlack([str(src), "--diff"])
1059 cache_file = black.get_cache_file(mode)
1060 self.assertFalse(cache_file.exists())
1061 write_cache.assert_not_called()
1062 read_cache.assert_not_called()
1064 def test_no_cache_when_writeback_color_diff(self) -> None:
1066 with cache_dir() as workspace:
1067 src = (workspace / "test.py").resolve()
1068 with src.open("w") as fobj:
1069 fobj.write("print('hello')")
1070 with patch("black.read_cache") as read_cache, patch(
1073 self.invokeBlack([str(src), "--diff", "--color"])
1074 cache_file = black.get_cache_file(mode)
1075 self.assertFalse(cache_file.exists())
1076 write_cache.assert_not_called()
1077 read_cache.assert_not_called()
1080 def test_output_locking_when_writeback_diff(self) -> None:
1081 with cache_dir() as workspace:
1082 for tag in range(0, 4):
1083 src = (workspace / f"test{tag}.py").resolve()
1084 with src.open("w") as fobj:
1085 fobj.write("print('hello')")
1086 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1087 self.invokeBlack(["--diff", str(workspace)], exit_code=0)
1088 # this isn't quite doing what we want, but if it _isn't_
1089 # called then we cannot be using the lock it provides
1093 def test_output_locking_when_writeback_color_diff(self) -> None:
1094 with cache_dir() as workspace:
1095 for tag in range(0, 4):
1096 src = (workspace / f"test{tag}.py").resolve()
1097 with src.open("w") as fobj:
1098 fobj.write("print('hello')")
1099 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1100 self.invokeBlack(["--diff", "--color", str(workspace)], exit_code=0)
1101 # this isn't quite doing what we want, but if it _isn't_
1102 # called then we cannot be using the lock it provides
1105 def test_no_cache_when_stdin(self) -> None:
1108 result = CliRunner().invoke(
1109 black.main, ["-"], input=BytesIO(b"print('hello')")
1111 self.assertEqual(result.exit_code, 0)
1112 cache_file = black.get_cache_file(mode)
1113 self.assertFalse(cache_file.exists())
1115 def test_read_cache_no_cachefile(self) -> None:
1118 self.assertEqual(black.read_cache(mode), {})
1120 def test_write_cache_read_cache(self) -> None:
1122 with cache_dir() as workspace:
1123 src = (workspace / "test.py").resolve()
1125 black.write_cache({}, [src], mode)
1126 cache = black.read_cache(mode)
1127 self.assertIn(str(src), cache)
1128 self.assertEqual(cache[str(src)], black.get_cache_info(src))
1130 def test_filter_cached(self) -> None:
1131 with TemporaryDirectory() as workspace:
1132 path = Path(workspace)
1133 uncached = (path / "uncached").resolve()
1134 cached = (path / "cached").resolve()
1135 cached_but_changed = (path / "changed").resolve()
1138 cached_but_changed.touch()
1140 str(cached): black.get_cache_info(cached),
1141 str(cached_but_changed): (0.0, 0),
1143 todo, done = black.filter_cached(
1144 cache, {uncached, cached, cached_but_changed}
1146 self.assertEqual(todo, {uncached, cached_but_changed})
1147 self.assertEqual(done, {cached})
1149 def test_write_cache_creates_directory_if_needed(self) -> None:
1151 with cache_dir(exists=False) as workspace:
1152 self.assertFalse(workspace.exists())
1153 black.write_cache({}, [], mode)
1154 self.assertTrue(workspace.exists())
1157 def test_failed_formatting_does_not_get_cached(self) -> None:
1159 with cache_dir() as workspace, patch(
1160 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1162 failing = (workspace / "failing.py").resolve()
1163 with failing.open("w") as fobj:
1164 fobj.write("not actually python")
1165 clean = (workspace / "clean.py").resolve()
1166 with clean.open("w") as fobj:
1167 fobj.write('print("hello")\n')
1168 self.invokeBlack([str(workspace)], exit_code=123)
1169 cache = black.read_cache(mode)
1170 self.assertNotIn(str(failing), cache)
1171 self.assertIn(str(clean), cache)
1173 def test_write_cache_write_fail(self) -> None:
1175 with cache_dir(), patch.object(Path, "open") as mock:
1176 mock.side_effect = OSError
1177 black.write_cache({}, [], mode)
1180 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1181 def test_works_in_mono_process_only_environment(self) -> None:
1182 with cache_dir() as workspace:
1184 (workspace / "one.py").resolve(),
1185 (workspace / "two.py").resolve(),
1187 f.write_text('print("hello")\n')
1188 self.invokeBlack([str(workspace)])
1191 def test_check_diff_use_together(self) -> None:
1193 # Files which will be reformatted.
1194 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
1195 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1196 # Files which will not be reformatted.
1197 src2 = (THIS_DIR / "data" / "composition.py").resolve()
1198 self.invokeBlack([str(src2), "--diff", "--check"])
1199 # Multi file command.
1200 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1202 def test_no_files(self) -> None:
1204 # Without an argument, black exits with error code 0.
1205 self.invokeBlack([])
1207 def test_broken_symlink(self) -> None:
1208 with cache_dir() as workspace:
1209 symlink = workspace / "broken_link.py"
1211 symlink.symlink_to("nonexistent.py")
1212 except OSError as e:
1213 self.skipTest(f"Can't create symlinks: {e}")
1214 self.invokeBlack([str(workspace.resolve())])
1216 def test_read_cache_line_lengths(self) -> None:
1218 short_mode = replace(DEFAULT_MODE, line_length=1)
1219 with cache_dir() as workspace:
1220 path = (workspace / "file.py").resolve()
1222 black.write_cache({}, [path], mode)
1223 one = black.read_cache(mode)
1224 self.assertIn(str(path), one)
1225 two = black.read_cache(short_mode)
1226 self.assertNotIn(str(path), two)
1228 def test_single_file_force_pyi(self) -> None:
1229 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1230 contents, expected = read_data("force_pyi")
1231 with cache_dir() as workspace:
1232 path = (workspace / "file.py").resolve()
1233 with open(path, "w") as fh:
1235 self.invokeBlack([str(path), "--pyi"])
1236 with open(path, "r") as fh:
1238 # verify cache with --pyi is separate
1239 pyi_cache = black.read_cache(pyi_mode)
1240 self.assertIn(str(path), pyi_cache)
1241 normal_cache = black.read_cache(DEFAULT_MODE)
1242 self.assertNotIn(str(path), normal_cache)
1243 self.assertFormatEqual(expected, actual)
1244 black.assert_equivalent(contents, actual)
1245 black.assert_stable(contents, actual, pyi_mode)
1248 def test_multi_file_force_pyi(self) -> None:
1249 reg_mode = DEFAULT_MODE
1250 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1251 contents, expected = read_data("force_pyi")
1252 with cache_dir() as workspace:
1254 (workspace / "file1.py").resolve(),
1255 (workspace / "file2.py").resolve(),
1258 with open(path, "w") as fh:
1260 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1262 with open(path, "r") as fh:
1264 self.assertEqual(actual, expected)
1265 # verify cache with --pyi is separate
1266 pyi_cache = black.read_cache(pyi_mode)
1267 normal_cache = black.read_cache(reg_mode)
1269 self.assertIn(str(path), pyi_cache)
1270 self.assertNotIn(str(path), normal_cache)
1272 def test_pipe_force_pyi(self) -> None:
1273 source, expected = read_data("force_pyi")
1274 result = CliRunner().invoke(
1275 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1277 self.assertEqual(result.exit_code, 0)
1278 actual = result.output
1279 self.assertFormatEqual(actual, expected)
1281 def test_single_file_force_py36(self) -> None:
1282 reg_mode = DEFAULT_MODE
1283 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1284 source, expected = read_data("force_py36")
1285 with cache_dir() as workspace:
1286 path = (workspace / "file.py").resolve()
1287 with open(path, "w") as fh:
1289 self.invokeBlack([str(path), *PY36_ARGS])
1290 with open(path, "r") as fh:
1292 # verify cache with --target-version is separate
1293 py36_cache = black.read_cache(py36_mode)
1294 self.assertIn(str(path), py36_cache)
1295 normal_cache = black.read_cache(reg_mode)
1296 self.assertNotIn(str(path), normal_cache)
1297 self.assertEqual(actual, expected)
1300 def test_multi_file_force_py36(self) -> None:
1301 reg_mode = DEFAULT_MODE
1302 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1303 source, expected = read_data("force_py36")
1304 with cache_dir() as workspace:
1306 (workspace / "file1.py").resolve(),
1307 (workspace / "file2.py").resolve(),
1310 with open(path, "w") as fh:
1312 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1314 with open(path, "r") as fh:
1316 self.assertEqual(actual, expected)
1317 # verify cache with --target-version is separate
1318 pyi_cache = black.read_cache(py36_mode)
1319 normal_cache = black.read_cache(reg_mode)
1321 self.assertIn(str(path), pyi_cache)
1322 self.assertNotIn(str(path), normal_cache)
1324 def test_pipe_force_py36(self) -> None:
1325 source, expected = read_data("force_py36")
1326 result = CliRunner().invoke(
1328 ["-", "-q", "--target-version=py36"],
1329 input=BytesIO(source.encode("utf8")),
1331 self.assertEqual(result.exit_code, 0)
1332 actual = result.output
1333 self.assertFormatEqual(actual, expected)
1335 def test_include_exclude(self) -> None:
1336 path = THIS_DIR / "data" / "include_exclude_tests"
1337 include = re.compile(r"\.pyi?$")
1338 exclude = re.compile(r"/exclude/|/\.definitely_exclude/")
1339 report = black.Report()
1340 gitignore = PathSpec.from_lines("gitwildmatch", [])
1341 sources: List[Path] = []
1343 Path(path / "b/dont_exclude/a.py"),
1344 Path(path / "b/dont_exclude/a.pyi"),
1346 this_abs = THIS_DIR.resolve()
1348 black.gen_python_files(
1359 self.assertEqual(sorted(expected), sorted(sources))
1361 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1362 def test_exclude_for_issue_1572(self) -> None:
1363 # Exclude shouldn't touch files that were explicitly given to Black through the
1364 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1365 # https://github.com/psf/black/issues/1572
1366 path = THIS_DIR / "data" / "include_exclude_tests"
1368 exclude = r"/exclude/|a\.py"
1369 src = str(path / "b/exclude/a.py")
1370 report = black.Report()
1371 expected = [Path(path / "b/exclude/a.py")]
1380 extend_exclude=None,
1383 stdin_filename=None,
1386 self.assertEqual(sorted(expected), sorted(sources))
1388 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1389 def test_get_sources_with_stdin(self) -> None:
1391 exclude = r"/exclude/|a\.py"
1393 report = black.Report()
1394 expected = [Path("-")]
1403 extend_exclude=None,
1406 stdin_filename=None,
1409 self.assertEqual(sorted(expected), sorted(sources))
1411 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1412 def test_get_sources_with_stdin_filename(self) -> None:
1414 exclude = r"/exclude/|a\.py"
1416 report = black.Report()
1417 stdin_filename = str(THIS_DIR / "data/collections.py")
1418 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1427 extend_exclude=None,
1430 stdin_filename=stdin_filename,
1433 self.assertEqual(sorted(expected), sorted(sources))
1435 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1436 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
1437 # Exclude shouldn't exclude stdin_filename since it is mimicing the
1438 # file being passed directly. This is the same as
1439 # test_exclude_for_issue_1572
1440 path = THIS_DIR / "data" / "include_exclude_tests"
1442 exclude = r"/exclude/|a\.py"
1444 report = black.Report()
1445 stdin_filename = str(path / "b/exclude/a.py")
1446 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1455 extend_exclude=None,
1458 stdin_filename=stdin_filename,
1461 self.assertEqual(sorted(expected), sorted(sources))
1463 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1464 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
1465 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
1466 # file being passed directly. This is the same as
1467 # test_exclude_for_issue_1572
1468 path = THIS_DIR / "data" / "include_exclude_tests"
1470 extend_exclude = r"/exclude/|a\.py"
1472 report = black.Report()
1473 stdin_filename = str(path / "b/exclude/a.py")
1474 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1483 extend_exclude=extend_exclude,
1486 stdin_filename=stdin_filename,
1489 self.assertEqual(sorted(expected), sorted(sources))
1491 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1492 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
1493 # Force exclude should exclude the file when passing it through
1495 path = THIS_DIR / "data" / "include_exclude_tests"
1497 force_exclude = r"/exclude/|a\.py"
1499 report = black.Report()
1500 stdin_filename = str(path / "b/exclude/a.py")
1509 extend_exclude=None,
1510 force_exclude=force_exclude,
1512 stdin_filename=stdin_filename,
1515 self.assertEqual([], sorted(sources))
1517 def test_reformat_one_with_stdin(self) -> None:
1519 "black.format_stdin_to_stdout",
1520 return_value=lambda *args, **kwargs: black.Changed.YES,
1522 report = MagicMock()
1527 write_back=black.WriteBack.YES,
1531 fsts.assert_called_once()
1532 report.done.assert_called_with(path, black.Changed.YES)
1534 def test_reformat_one_with_stdin_filename(self) -> None:
1536 "black.format_stdin_to_stdout",
1537 return_value=lambda *args, **kwargs: black.Changed.YES,
1539 report = MagicMock()
1541 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1546 write_back=black.WriteBack.YES,
1550 fsts.assert_called_once()
1551 # __BLACK_STDIN_FILENAME__ should have been striped
1552 report.done.assert_called_with(expected, black.Changed.YES)
1554 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1556 "black.format_stdin_to_stdout",
1557 return_value=lambda *args, **kwargs: black.Changed.YES,
1559 report = MagicMock()
1560 # Even with an existing file, since we are forcing stdin, black
1561 # should output to stdout and not modify the file inplace
1562 p = Path(str(THIS_DIR / "data/collections.py"))
1563 # Make sure is_file actually returns True
1564 self.assertTrue(p.is_file())
1565 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1570 write_back=black.WriteBack.YES,
1574 fsts.assert_called_once()
1575 # __BLACK_STDIN_FILENAME__ should have been striped
1576 report.done.assert_called_with(expected, black.Changed.YES)
1578 def test_gitignore_exclude(self) -> None:
1579 path = THIS_DIR / "data" / "include_exclude_tests"
1580 include = re.compile(r"\.pyi?$")
1581 exclude = re.compile(r"")
1582 report = black.Report()
1583 gitignore = PathSpec.from_lines(
1584 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1586 sources: List[Path] = []
1588 Path(path / "b/dont_exclude/a.py"),
1589 Path(path / "b/dont_exclude/a.pyi"),
1591 this_abs = THIS_DIR.resolve()
1593 black.gen_python_files(
1604 self.assertEqual(sorted(expected), sorted(sources))
1606 def test_empty_include(self) -> None:
1607 path = THIS_DIR / "data" / "include_exclude_tests"
1608 report = black.Report()
1609 gitignore = PathSpec.from_lines("gitwildmatch", [])
1610 empty = re.compile(r"")
1611 sources: List[Path] = []
1613 Path(path / "b/exclude/a.pie"),
1614 Path(path / "b/exclude/a.py"),
1615 Path(path / "b/exclude/a.pyi"),
1616 Path(path / "b/dont_exclude/a.pie"),
1617 Path(path / "b/dont_exclude/a.py"),
1618 Path(path / "b/dont_exclude/a.pyi"),
1619 Path(path / "b/.definitely_exclude/a.pie"),
1620 Path(path / "b/.definitely_exclude/a.py"),
1621 Path(path / "b/.definitely_exclude/a.pyi"),
1623 this_abs = THIS_DIR.resolve()
1625 black.gen_python_files(
1629 re.compile(black.DEFAULT_EXCLUDES),
1636 self.assertEqual(sorted(expected), sorted(sources))
1638 def test_extend_exclude(self) -> None:
1639 path = THIS_DIR / "data" / "include_exclude_tests"
1640 report = black.Report()
1641 gitignore = PathSpec.from_lines("gitwildmatch", [])
1642 sources: List[Path] = []
1644 Path(path / "b/exclude/a.py"),
1645 Path(path / "b/dont_exclude/a.py"),
1647 this_abs = THIS_DIR.resolve()
1649 black.gen_python_files(
1652 re.compile(black.DEFAULT_INCLUDES),
1653 re.compile(r"\.pyi$"),
1654 re.compile(r"\.definitely_exclude"),
1660 self.assertEqual(sorted(expected), sorted(sources))
1662 def test_invalid_cli_regex(self) -> None:
1663 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1664 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1666 def test_preserves_line_endings(self) -> None:
1667 with TemporaryDirectory() as workspace:
1668 test_file = Path(workspace) / "test.py"
1669 for nl in ["\n", "\r\n"]:
1670 contents = nl.join(["def f( ):", " pass"])
1671 test_file.write_bytes(contents.encode())
1672 ff(test_file, write_back=black.WriteBack.YES)
1673 updated_contents: bytes = test_file.read_bytes()
1674 self.assertIn(nl.encode(), updated_contents)
1676 self.assertNotIn(b"\r\n", updated_contents)
1678 def test_preserves_line_endings_via_stdin(self) -> None:
1679 for nl in ["\n", "\r\n"]:
1680 contents = nl.join(["def f( ):", " pass"])
1681 runner = BlackRunner()
1682 result = runner.invoke(
1683 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1685 self.assertEqual(result.exit_code, 0)
1686 output = runner.stdout_bytes
1687 self.assertIn(nl.encode("utf8"), output)
1689 self.assertNotIn(b"\r\n", output)
1691 def test_assert_equivalent_different_asts(self) -> None:
1692 with self.assertRaises(AssertionError):
1693 black.assert_equivalent("{}", "None")
1695 def test_symlink_out_of_root_directory(self) -> None:
1697 root = THIS_DIR.resolve()
1699 include = re.compile(black.DEFAULT_INCLUDES)
1700 exclude = re.compile(black.DEFAULT_EXCLUDES)
1701 report = black.Report()
1702 gitignore = PathSpec.from_lines("gitwildmatch", [])
1703 # `child` should behave like a symlink which resolved path is clearly
1704 # outside of the `root` directory.
1705 path.iterdir.return_value = [child]
1706 child.resolve.return_value = Path("/a/b/c")
1707 child.as_posix.return_value = "/a/b/c"
1708 child.is_symlink.return_value = True
1711 black.gen_python_files(
1722 except ValueError as ve:
1723 self.fail(f"`get_python_files_in_dir()` failed: {ve}")
1724 path.iterdir.assert_called_once()
1725 child.resolve.assert_called_once()
1726 child.is_symlink.assert_called_once()
1727 # `child` should behave like a strange file which resolved path is clearly
1728 # outside of the `root` directory.
1729 child.is_symlink.return_value = False
1730 with self.assertRaises(ValueError):
1732 black.gen_python_files(
1743 path.iterdir.assert_called()
1744 self.assertEqual(path.iterdir.call_count, 2)
1745 child.resolve.assert_called()
1746 self.assertEqual(child.resolve.call_count, 2)
1747 child.is_symlink.assert_called()
1748 self.assertEqual(child.is_symlink.call_count, 2)
1750 def test_shhh_click(self) -> None:
1752 from click import _unicodefun # type: ignore
1753 except ModuleNotFoundError:
1754 self.skipTest("Incompatible Click version")
1755 if not hasattr(_unicodefun, "_verify_python3_env"):
1756 self.skipTest("Incompatible Click version")
1757 # First, let's see if Click is crashing with a preferred ASCII charset.
1758 with patch("locale.getpreferredencoding") as gpe:
1759 gpe.return_value = "ASCII"
1760 with self.assertRaises(RuntimeError):
1761 _unicodefun._verify_python3_env()
1762 # Now, let's silence Click...
1764 # ...and confirm it's silent.
1765 with patch("locale.getpreferredencoding") as gpe:
1766 gpe.return_value = "ASCII"
1768 _unicodefun._verify_python3_env()
1769 except RuntimeError as re:
1770 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1772 def test_root_logger_not_used_directly(self) -> None:
1773 def fail(*args: Any, **kwargs: Any) -> None:
1774 self.fail("Record created with root logger")
1776 with patch.multiple(
1787 def test_invalid_config_return_code(self) -> None:
1788 tmp_file = Path(black.dump_to_file())
1790 tmp_config = Path(black.dump_to_file())
1792 args = ["--config", str(tmp_config), str(tmp_file)]
1793 self.invokeBlack(args, exit_code=2, ignore_config=False)
1797 def test_parse_pyproject_toml(self) -> None:
1798 test_toml_file = THIS_DIR / "test.toml"
1799 config = black.parse_pyproject_toml(str(test_toml_file))
1800 self.assertEqual(config["verbose"], 1)
1801 self.assertEqual(config["check"], "no")
1802 self.assertEqual(config["diff"], "y")
1803 self.assertEqual(config["color"], True)
1804 self.assertEqual(config["line_length"], 79)
1805 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1806 self.assertEqual(config["exclude"], r"\.pyi?$")
1807 self.assertEqual(config["include"], r"\.py?$")
1809 def test_read_pyproject_toml(self) -> None:
1810 test_toml_file = THIS_DIR / "test.toml"
1811 fake_ctx = FakeContext()
1812 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1813 config = fake_ctx.default_map
1814 self.assertEqual(config["verbose"], "1")
1815 self.assertEqual(config["check"], "no")
1816 self.assertEqual(config["diff"], "y")
1817 self.assertEqual(config["color"], "True")
1818 self.assertEqual(config["line_length"], "79")
1819 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1820 self.assertEqual(config["exclude"], r"\.pyi?$")
1821 self.assertEqual(config["include"], r"\.py?$")
1823 def test_find_project_root(self) -> None:
1824 with TemporaryDirectory() as workspace:
1825 root = Path(workspace)
1826 test_dir = root / "test"
1829 src_dir = root / "src"
1832 root_pyproject = root / "pyproject.toml"
1833 root_pyproject.touch()
1834 src_pyproject = src_dir / "pyproject.toml"
1835 src_pyproject.touch()
1836 src_python = src_dir / "foo.py"
1840 black.find_project_root((src_dir, test_dir)), root.resolve()
1842 self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1843 self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1845 def test_bpo_33660_workaround(self) -> None:
1846 if system() == "Windows":
1849 # https://bugs.python.org/issue33660
1851 old_cwd = Path.cwd()
1855 path = Path("workspace") / "project"
1856 report = black.Report(verbose=True)
1857 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1858 self.assertEqual(normalized_path, "workspace/project")
1860 os.chdir(str(old_cwd))
1862 def test_newline_comment_interaction(self) -> None:
1863 source = "class A:\\\r\n# type: ignore\n pass\n"
1864 output = black.format_str(source, mode=DEFAULT_MODE)
1865 black.assert_stable(source, output, mode=DEFAULT_MODE)
1867 def test_bpo_2142_workaround(self) -> None:
1869 # https://bugs.python.org/issue2142
1871 source, _ = read_data("missing_final_newline.py")
1872 # read_data adds a trailing newline
1873 source = source.rstrip()
1874 expected, _ = read_data("missing_final_newline.diff")
1875 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1876 diff_header = re.compile(
1877 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1878 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1881 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1882 self.assertEqual(result.exit_code, 0)
1885 actual = result.output
1886 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1887 self.assertEqual(actual, expected)
1890 with open(black.__file__, "r", encoding="utf-8") as _bf:
1891 black_source_lines = _bf.readlines()
1894 def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
1895 """Show function calls `from black/__init__.py` as they happen.
1897 Register this with `sys.settrace()` in a test you're debugging.
1902 stack = len(inspect.stack()) - 19
1904 filename = frame.f_code.co_filename
1905 lineno = frame.f_lineno
1906 func_sig_lineno = lineno - 1
1907 funcname = black_source_lines[func_sig_lineno].strip()
1908 while funcname.startswith("@"):
1909 func_sig_lineno += 1
1910 funcname = black_source_lines[func_sig_lineno].strip()
1911 if "black/__init__.py" in filename:
1912 print(f"{' ' * stack}{lineno}:{funcname}")
1916 if __name__ == "__main__":
1917 unittest.main(module="test_black")