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 def test_pep_572_version_detection(self) -> None:
267 source, _ = read_data("pep_572")
268 root = black.lib2to3_parse(source)
269 features = black.get_features_used(root)
270 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
271 versions = black.detect_target_versions(root)
272 self.assertIn(black.TargetVersion.PY38, versions)
274 def test_expression_ff(self) -> None:
275 source, expected = read_data("expression")
276 tmp_file = Path(black.dump_to_file(source))
278 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
279 with open(tmp_file, encoding="utf8") as f:
283 self.assertFormatEqual(expected, actual)
284 with patch("black.dump_to_file", dump_to_stderr):
285 black.assert_equivalent(source, actual)
286 black.assert_stable(source, actual, DEFAULT_MODE)
288 def test_expression_diff(self) -> None:
289 source, _ = read_data("expression.py")
290 expected, _ = read_data("expression.diff")
291 tmp_file = Path(black.dump_to_file(source))
292 diff_header = re.compile(
293 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
294 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
297 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
298 self.assertEqual(result.exit_code, 0)
301 actual = result.output
302 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
303 if expected != actual:
304 dump = black.dump_to_file(actual)
306 "Expected diff isn't equal to the actual. If you made changes to"
307 " expression.py and this is an anticipated difference, overwrite"
308 f" tests/data/expression.diff with {dump}"
310 self.assertEqual(expected, actual, msg)
312 def test_expression_diff_with_color(self) -> None:
313 source, _ = read_data("expression.py")
314 expected, _ = read_data("expression.diff")
315 tmp_file = Path(black.dump_to_file(source))
317 result = BlackRunner().invoke(
318 black.main, ["--diff", "--color", str(tmp_file)]
322 actual = result.output
323 # We check the contents of the diff in `test_expression_diff`. All
324 # we need to check here is that color codes exist in the result.
325 self.assertIn("\033[1;37m", actual)
326 self.assertIn("\033[36m", actual)
327 self.assertIn("\033[32m", actual)
328 self.assertIn("\033[31m", actual)
329 self.assertIn("\033[0m", actual)
331 @patch("black.dump_to_file", dump_to_stderr)
332 def test_pep_570(self) -> None:
333 source, expected = read_data("pep_570")
335 self.assertFormatEqual(expected, actual)
336 black.assert_stable(source, actual, DEFAULT_MODE)
337 if sys.version_info >= (3, 8):
338 black.assert_equivalent(source, actual)
340 def test_detect_pos_only_arguments(self) -> None:
341 source, _ = read_data("pep_570")
342 root = black.lib2to3_parse(source)
343 features = black.get_features_used(root)
344 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
345 versions = black.detect_target_versions(root)
346 self.assertIn(black.TargetVersion.PY38, versions)
348 @patch("black.dump_to_file", dump_to_stderr)
349 def test_string_quotes(self) -> None:
350 source, expected = read_data("string_quotes")
352 self.assertFormatEqual(expected, actual)
353 black.assert_equivalent(source, actual)
354 black.assert_stable(source, actual, DEFAULT_MODE)
355 mode = replace(DEFAULT_MODE, string_normalization=False)
356 not_normalized = fs(source, mode=mode)
357 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
358 black.assert_equivalent(source, not_normalized)
359 black.assert_stable(source, not_normalized, mode=mode)
361 @patch("black.dump_to_file", dump_to_stderr)
362 def test_docstring_no_string_normalization(self) -> None:
363 """Like test_docstring but with string normalization off."""
364 source, expected = read_data("docstring_no_string_normalization")
365 mode = replace(DEFAULT_MODE, string_normalization=False)
366 actual = fs(source, mode=mode)
367 self.assertFormatEqual(expected, actual)
368 black.assert_equivalent(source, actual)
369 black.assert_stable(source, actual, mode)
371 def test_long_strings_flag_disabled(self) -> None:
372 """Tests for turning off the string processing logic."""
373 source, expected = read_data("long_strings_flag_disabled")
374 mode = replace(DEFAULT_MODE, experimental_string_processing=False)
375 actual = fs(source, mode=mode)
376 self.assertFormatEqual(expected, actual)
377 black.assert_stable(expected, actual, mode)
379 @patch("black.dump_to_file", dump_to_stderr)
380 def test_numeric_literals(self) -> None:
381 source, expected = read_data("numeric_literals")
382 mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
383 actual = fs(source, mode=mode)
384 self.assertFormatEqual(expected, actual)
385 black.assert_equivalent(source, actual)
386 black.assert_stable(source, actual, mode)
388 @patch("black.dump_to_file", dump_to_stderr)
389 def test_numeric_literals_ignoring_underscores(self) -> None:
390 source, expected = read_data("numeric_literals_skip_underscores")
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 def test_skip_magic_trailing_comma(self) -> None:
398 source, _ = read_data("expression.py")
399 expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
400 tmp_file = Path(black.dump_to_file(source))
401 diff_header = re.compile(
402 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
403 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
406 result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
407 self.assertEqual(result.exit_code, 0)
410 actual = result.output
411 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
412 actual = actual.rstrip() + "\n" # the diff output has a trailing space
413 if expected != actual:
414 dump = black.dump_to_file(actual)
416 "Expected diff isn't equal to the actual. If you made changes to"
417 " expression.py and this is an anticipated difference, overwrite"
418 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
420 self.assertEqual(expected, actual, msg)
422 @patch("black.dump_to_file", dump_to_stderr)
423 def test_python2_print_function(self) -> None:
424 source, expected = read_data("python2_print_function")
425 mode = replace(DEFAULT_MODE, target_versions={TargetVersion.PY27})
426 actual = fs(source, mode=mode)
427 self.assertFormatEqual(expected, actual)
428 black.assert_equivalent(source, actual)
429 black.assert_stable(source, actual, mode)
431 @patch("black.dump_to_file", dump_to_stderr)
432 def test_stub(self) -> None:
433 mode = replace(DEFAULT_MODE, is_pyi=True)
434 source, expected = read_data("stub.pyi")
435 actual = fs(source, mode=mode)
436 self.assertFormatEqual(expected, actual)
437 black.assert_stable(source, actual, mode)
439 @patch("black.dump_to_file", dump_to_stderr)
440 def test_async_as_identifier(self) -> None:
441 source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
442 source, expected = read_data("async_as_identifier")
444 self.assertFormatEqual(expected, actual)
445 major, minor = sys.version_info[:2]
446 if major < 3 or (major <= 3 and minor < 7):
447 black.assert_equivalent(source, actual)
448 black.assert_stable(source, actual, DEFAULT_MODE)
449 # ensure black can parse this when the target is 3.6
450 self.invokeBlack([str(source_path), "--target-version", "py36"])
451 # but not on 3.7, because async/await is no longer an identifier
452 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
454 @patch("black.dump_to_file", dump_to_stderr)
455 def test_python37(self) -> None:
456 source_path = (THIS_DIR / "data" / "python37.py").resolve()
457 source, expected = read_data("python37")
459 self.assertFormatEqual(expected, actual)
460 major, minor = sys.version_info[:2]
461 if major > 3 or (major == 3 and minor >= 7):
462 black.assert_equivalent(source, actual)
463 black.assert_stable(source, actual, DEFAULT_MODE)
464 # ensure black can parse this when the target is 3.7
465 self.invokeBlack([str(source_path), "--target-version", "py37"])
466 # but not on 3.6, because we use async as a reserved keyword
467 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
469 @patch("black.dump_to_file", dump_to_stderr)
470 def test_python38(self) -> None:
471 source, expected = read_data("python38")
473 self.assertFormatEqual(expected, actual)
474 major, minor = sys.version_info[:2]
475 if major > 3 or (major == 3 and minor >= 8):
476 black.assert_equivalent(source, actual)
477 black.assert_stable(source, actual, DEFAULT_MODE)
479 @patch("black.dump_to_file", dump_to_stderr)
480 def test_python39(self) -> None:
481 source, expected = read_data("python39")
483 self.assertFormatEqual(expected, actual)
484 major, minor = sys.version_info[:2]
485 if major > 3 or (major == 3 and minor >= 9):
486 black.assert_equivalent(source, actual)
487 black.assert_stable(source, actual, DEFAULT_MODE)
489 def test_tab_comment_indentation(self) -> None:
490 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
491 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
492 self.assertFormatEqual(contents_spc, fs(contents_spc))
493 self.assertFormatEqual(contents_spc, fs(contents_tab))
495 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
496 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
497 self.assertFormatEqual(contents_spc, fs(contents_spc))
498 self.assertFormatEqual(contents_spc, fs(contents_tab))
500 # mixed tabs and spaces (valid Python 2 code)
501 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
502 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
503 self.assertFormatEqual(contents_spc, fs(contents_spc))
504 self.assertFormatEqual(contents_spc, fs(contents_tab))
506 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
507 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
508 self.assertFormatEqual(contents_spc, fs(contents_spc))
509 self.assertFormatEqual(contents_spc, fs(contents_tab))
511 def test_report_verbose(self) -> None:
512 report = black.Report(verbose=True)
516 def out(msg: str, **kwargs: Any) -> None:
517 out_lines.append(msg)
519 def err(msg: str, **kwargs: Any) -> None:
520 err_lines.append(msg)
522 with patch("black.out", out), patch("black.err", err):
523 report.done(Path("f1"), black.Changed.NO)
524 self.assertEqual(len(out_lines), 1)
525 self.assertEqual(len(err_lines), 0)
526 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
527 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
528 self.assertEqual(report.return_code, 0)
529 report.done(Path("f2"), black.Changed.YES)
530 self.assertEqual(len(out_lines), 2)
531 self.assertEqual(len(err_lines), 0)
532 self.assertEqual(out_lines[-1], "reformatted f2")
534 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
536 report.done(Path("f3"), black.Changed.CACHED)
537 self.assertEqual(len(out_lines), 3)
538 self.assertEqual(len(err_lines), 0)
540 out_lines[-1], "f3 wasn't modified on disk since last run."
543 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
545 self.assertEqual(report.return_code, 0)
547 self.assertEqual(report.return_code, 1)
549 report.failed(Path("e1"), "boom")
550 self.assertEqual(len(out_lines), 3)
551 self.assertEqual(len(err_lines), 1)
552 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
554 unstyle(str(report)),
555 "1 file reformatted, 2 files left unchanged, 1 file failed to"
558 self.assertEqual(report.return_code, 123)
559 report.done(Path("f3"), black.Changed.YES)
560 self.assertEqual(len(out_lines), 4)
561 self.assertEqual(len(err_lines), 1)
562 self.assertEqual(out_lines[-1], "reformatted f3")
564 unstyle(str(report)),
565 "2 files reformatted, 2 files left unchanged, 1 file failed to"
568 self.assertEqual(report.return_code, 123)
569 report.failed(Path("e2"), "boom")
570 self.assertEqual(len(out_lines), 4)
571 self.assertEqual(len(err_lines), 2)
572 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
574 unstyle(str(report)),
575 "2 files reformatted, 2 files left unchanged, 2 files failed to"
578 self.assertEqual(report.return_code, 123)
579 report.path_ignored(Path("wat"), "no match")
580 self.assertEqual(len(out_lines), 5)
581 self.assertEqual(len(err_lines), 2)
582 self.assertEqual(out_lines[-1], "wat ignored: no match")
584 unstyle(str(report)),
585 "2 files reformatted, 2 files left unchanged, 2 files failed to"
588 self.assertEqual(report.return_code, 123)
589 report.done(Path("f4"), black.Changed.NO)
590 self.assertEqual(len(out_lines), 6)
591 self.assertEqual(len(err_lines), 2)
592 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
594 unstyle(str(report)),
595 "2 files reformatted, 3 files left unchanged, 2 files failed to"
598 self.assertEqual(report.return_code, 123)
601 unstyle(str(report)),
602 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
603 " would fail to reformat.",
608 unstyle(str(report)),
609 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
610 " would fail to reformat.",
613 def test_report_quiet(self) -> None:
614 report = black.Report(quiet=True)
618 def out(msg: str, **kwargs: Any) -> None:
619 out_lines.append(msg)
621 def err(msg: str, **kwargs: Any) -> None:
622 err_lines.append(msg)
624 with patch("black.out", out), patch("black.err", err):
625 report.done(Path("f1"), black.Changed.NO)
626 self.assertEqual(len(out_lines), 0)
627 self.assertEqual(len(err_lines), 0)
628 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
629 self.assertEqual(report.return_code, 0)
630 report.done(Path("f2"), black.Changed.YES)
631 self.assertEqual(len(out_lines), 0)
632 self.assertEqual(len(err_lines), 0)
634 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
636 report.done(Path("f3"), black.Changed.CACHED)
637 self.assertEqual(len(out_lines), 0)
638 self.assertEqual(len(err_lines), 0)
640 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
642 self.assertEqual(report.return_code, 0)
644 self.assertEqual(report.return_code, 1)
646 report.failed(Path("e1"), "boom")
647 self.assertEqual(len(out_lines), 0)
648 self.assertEqual(len(err_lines), 1)
649 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
651 unstyle(str(report)),
652 "1 file reformatted, 2 files left unchanged, 1 file failed to"
655 self.assertEqual(report.return_code, 123)
656 report.done(Path("f3"), black.Changed.YES)
657 self.assertEqual(len(out_lines), 0)
658 self.assertEqual(len(err_lines), 1)
660 unstyle(str(report)),
661 "2 files reformatted, 2 files left unchanged, 1 file failed to"
664 self.assertEqual(report.return_code, 123)
665 report.failed(Path("e2"), "boom")
666 self.assertEqual(len(out_lines), 0)
667 self.assertEqual(len(err_lines), 2)
668 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
670 unstyle(str(report)),
671 "2 files reformatted, 2 files left unchanged, 2 files failed to"
674 self.assertEqual(report.return_code, 123)
675 report.path_ignored(Path("wat"), "no match")
676 self.assertEqual(len(out_lines), 0)
677 self.assertEqual(len(err_lines), 2)
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.done(Path("f4"), black.Changed.NO)
685 self.assertEqual(len(out_lines), 0)
686 self.assertEqual(len(err_lines), 2)
688 unstyle(str(report)),
689 "2 files reformatted, 3 files left unchanged, 2 files failed to"
692 self.assertEqual(report.return_code, 123)
695 unstyle(str(report)),
696 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
697 " would fail to reformat.",
702 unstyle(str(report)),
703 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
704 " would fail to reformat.",
707 def test_report_normal(self) -> None:
708 report = black.Report()
712 def out(msg: str, **kwargs: Any) -> None:
713 out_lines.append(msg)
715 def err(msg: str, **kwargs: Any) -> None:
716 err_lines.append(msg)
718 with patch("black.out", out), patch("black.err", err):
719 report.done(Path("f1"), black.Changed.NO)
720 self.assertEqual(len(out_lines), 0)
721 self.assertEqual(len(err_lines), 0)
722 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
723 self.assertEqual(report.return_code, 0)
724 report.done(Path("f2"), black.Changed.YES)
725 self.assertEqual(len(out_lines), 1)
726 self.assertEqual(len(err_lines), 0)
727 self.assertEqual(out_lines[-1], "reformatted f2")
729 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
731 report.done(Path("f3"), black.Changed.CACHED)
732 self.assertEqual(len(out_lines), 1)
733 self.assertEqual(len(err_lines), 0)
734 self.assertEqual(out_lines[-1], "reformatted f2")
736 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
738 self.assertEqual(report.return_code, 0)
740 self.assertEqual(report.return_code, 1)
742 report.failed(Path("e1"), "boom")
743 self.assertEqual(len(out_lines), 1)
744 self.assertEqual(len(err_lines), 1)
745 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
747 unstyle(str(report)),
748 "1 file reformatted, 2 files left unchanged, 1 file failed to"
751 self.assertEqual(report.return_code, 123)
752 report.done(Path("f3"), black.Changed.YES)
753 self.assertEqual(len(out_lines), 2)
754 self.assertEqual(len(err_lines), 1)
755 self.assertEqual(out_lines[-1], "reformatted f3")
757 unstyle(str(report)),
758 "2 files reformatted, 2 files left unchanged, 1 file failed to"
761 self.assertEqual(report.return_code, 123)
762 report.failed(Path("e2"), "boom")
763 self.assertEqual(len(out_lines), 2)
764 self.assertEqual(len(err_lines), 2)
765 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
767 unstyle(str(report)),
768 "2 files reformatted, 2 files left unchanged, 2 files failed to"
771 self.assertEqual(report.return_code, 123)
772 report.path_ignored(Path("wat"), "no match")
773 self.assertEqual(len(out_lines), 2)
774 self.assertEqual(len(err_lines), 2)
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.done(Path("f4"), black.Changed.NO)
782 self.assertEqual(len(out_lines), 2)
783 self.assertEqual(len(err_lines), 2)
785 unstyle(str(report)),
786 "2 files reformatted, 3 files left unchanged, 2 files failed to"
789 self.assertEqual(report.return_code, 123)
792 unstyle(str(report)),
793 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
794 " would fail to reformat.",
799 unstyle(str(report)),
800 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
801 " would fail to reformat.",
804 def test_lib2to3_parse(self) -> None:
805 with self.assertRaises(black.InvalidInput):
806 black.lib2to3_parse("invalid syntax")
809 black.lib2to3_parse(straddling)
810 black.lib2to3_parse(straddling, {TargetVersion.PY27})
811 black.lib2to3_parse(straddling, {TargetVersion.PY36})
812 black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36})
815 black.lib2to3_parse(py2_only)
816 black.lib2to3_parse(py2_only, {TargetVersion.PY27})
817 with self.assertRaises(black.InvalidInput):
818 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
819 with self.assertRaises(black.InvalidInput):
820 black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36})
822 py3_only = "exec(x, end=y)"
823 black.lib2to3_parse(py3_only)
824 with self.assertRaises(black.InvalidInput):
825 black.lib2to3_parse(py3_only, {TargetVersion.PY27})
826 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
827 black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36})
829 def test_get_features_used_decorator(self) -> None:
830 # Test the feature detection of new decorator syntax
831 # since this makes some test cases of test_get_features_used()
832 # fails if it fails, this is tested first so that a useful case
834 simples, relaxed = read_data("decorators")
835 # skip explanation comments at the top of the file
836 for simple_test in simples.split("##")[1:]:
837 node = black.lib2to3_parse(simple_test)
838 decorator = str(node.children[0].children[0]).strip()
840 Feature.RELAXED_DECORATORS,
841 black.get_features_used(node),
843 f"decorator '{decorator}' follows python<=3.8 syntax"
844 "but is detected as 3.9+"
845 # f"The full node is\n{node!r}"
848 # skip the '# output' comment at the top of the output part
849 for relaxed_test in relaxed.split("##")[1:]:
850 node = black.lib2to3_parse(relaxed_test)
851 decorator = str(node.children[0].children[0]).strip()
853 Feature.RELAXED_DECORATORS,
854 black.get_features_used(node),
856 f"decorator '{decorator}' uses python3.9+ syntax"
857 "but is detected as python<=3.8"
858 # f"The full node is\n{node!r}"
862 def test_get_features_used(self) -> None:
863 node = black.lib2to3_parse("def f(*, arg): ...\n")
864 self.assertEqual(black.get_features_used(node), set())
865 node = black.lib2to3_parse("def f(*, arg,): ...\n")
866 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
867 node = black.lib2to3_parse("f(*arg,)\n")
869 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
871 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
872 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
873 node = black.lib2to3_parse("123_456\n")
874 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
875 node = black.lib2to3_parse("123456\n")
876 self.assertEqual(black.get_features_used(node), set())
877 source, expected = read_data("function")
878 node = black.lib2to3_parse(source)
879 expected_features = {
880 Feature.TRAILING_COMMA_IN_CALL,
881 Feature.TRAILING_COMMA_IN_DEF,
884 self.assertEqual(black.get_features_used(node), expected_features)
885 node = black.lib2to3_parse(expected)
886 self.assertEqual(black.get_features_used(node), expected_features)
887 source, expected = read_data("expression")
888 node = black.lib2to3_parse(source)
889 self.assertEqual(black.get_features_used(node), set())
890 node = black.lib2to3_parse(expected)
891 self.assertEqual(black.get_features_used(node), set())
893 def test_get_future_imports(self) -> None:
894 node = black.lib2to3_parse("\n")
895 self.assertEqual(set(), black.get_future_imports(node))
896 node = black.lib2to3_parse("from __future__ import black\n")
897 self.assertEqual({"black"}, black.get_future_imports(node))
898 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
899 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
900 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
901 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
902 node = black.lib2to3_parse(
903 "from __future__ import multiple\nfrom __future__ import imports\n"
905 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
906 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
907 self.assertEqual({"black"}, black.get_future_imports(node))
908 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
909 self.assertEqual({"black"}, black.get_future_imports(node))
910 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
911 self.assertEqual(set(), black.get_future_imports(node))
912 node = black.lib2to3_parse("from some.module import black\n")
913 self.assertEqual(set(), black.get_future_imports(node))
914 node = black.lib2to3_parse(
915 "from __future__ import unicode_literals as _unicode_literals"
917 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
918 node = black.lib2to3_parse(
919 "from __future__ import unicode_literals as _lol, print"
921 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
923 def test_debug_visitor(self) -> None:
924 source, _ = read_data("debug_visitor.py")
925 expected, _ = read_data("debug_visitor.out")
929 def out(msg: str, **kwargs: Any) -> None:
930 out_lines.append(msg)
932 def err(msg: str, **kwargs: Any) -> None:
933 err_lines.append(msg)
935 with patch("black.out", out), patch("black.err", err):
936 black.DebugVisitor.show(source)
937 actual = "\n".join(out_lines) + "\n"
939 if expected != actual:
940 log_name = black.dump_to_file(*out_lines)
944 f"AST print out is different. Actual version dumped to {log_name}",
947 def test_format_file_contents(self) -> None:
950 with self.assertRaises(black.NothingChanged):
951 black.format_file_contents(empty, mode=mode, fast=False)
953 with self.assertRaises(black.NothingChanged):
954 black.format_file_contents(just_nl, mode=mode, fast=False)
955 same = "j = [1, 2, 3]\n"
956 with self.assertRaises(black.NothingChanged):
957 black.format_file_contents(same, mode=mode, fast=False)
958 different = "j = [1,2,3]"
960 actual = black.format_file_contents(different, mode=mode, fast=False)
961 self.assertEqual(expected, actual)
962 invalid = "return if you can"
963 with self.assertRaises(black.InvalidInput) as e:
964 black.format_file_contents(invalid, mode=mode, fast=False)
965 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
967 def test_endmarker(self) -> None:
968 n = black.lib2to3_parse("\n")
969 self.assertEqual(n.type, black.syms.file_input)
970 self.assertEqual(len(n.children), 1)
971 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
973 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
974 def test_assertFormatEqual(self) -> None:
978 def out(msg: str, **kwargs: Any) -> None:
979 out_lines.append(msg)
981 def err(msg: str, **kwargs: Any) -> None:
982 err_lines.append(msg)
984 with patch("black.out", out), patch("black.err", err):
985 with self.assertRaises(AssertionError):
986 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
988 out_str = "".join(out_lines)
989 self.assertTrue("Expected tree:" in out_str)
990 self.assertTrue("Actual tree:" in out_str)
991 self.assertEqual("".join(err_lines), "")
993 def test_cache_broken_file(self) -> None:
995 with cache_dir() as workspace:
996 cache_file = black.get_cache_file(mode)
997 with cache_file.open("w") as fobj:
998 fobj.write("this is not a pickle")
999 self.assertEqual(black.read_cache(mode), {})
1000 src = (workspace / "test.py").resolve()
1001 with src.open("w") as fobj:
1002 fobj.write("print('hello')")
1003 self.invokeBlack([str(src)])
1004 cache = black.read_cache(mode)
1005 self.assertIn(str(src), cache)
1007 def test_cache_single_file_already_cached(self) -> None:
1009 with cache_dir() as workspace:
1010 src = (workspace / "test.py").resolve()
1011 with src.open("w") as fobj:
1012 fobj.write("print('hello')")
1013 black.write_cache({}, [src], mode)
1014 self.invokeBlack([str(src)])
1015 with src.open("r") as fobj:
1016 self.assertEqual(fobj.read(), "print('hello')")
1019 def test_cache_multiple_files(self) -> None:
1021 with cache_dir() as workspace, patch(
1022 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1024 one = (workspace / "one.py").resolve()
1025 with one.open("w") as fobj:
1026 fobj.write("print('hello')")
1027 two = (workspace / "two.py").resolve()
1028 with two.open("w") as fobj:
1029 fobj.write("print('hello')")
1030 black.write_cache({}, [one], mode)
1031 self.invokeBlack([str(workspace)])
1032 with one.open("r") as fobj:
1033 self.assertEqual(fobj.read(), "print('hello')")
1034 with two.open("r") as fobj:
1035 self.assertEqual(fobj.read(), 'print("hello")\n')
1036 cache = black.read_cache(mode)
1037 self.assertIn(str(one), cache)
1038 self.assertIn(str(two), cache)
1040 def test_no_cache_when_writeback_diff(self) -> None:
1042 with cache_dir() as workspace:
1043 src = (workspace / "test.py").resolve()
1044 with src.open("w") as fobj:
1045 fobj.write("print('hello')")
1046 with patch("black.read_cache") as read_cache, patch(
1049 self.invokeBlack([str(src), "--diff"])
1050 cache_file = black.get_cache_file(mode)
1051 self.assertFalse(cache_file.exists())
1052 write_cache.assert_not_called()
1053 read_cache.assert_not_called()
1055 def test_no_cache_when_writeback_color_diff(self) -> None:
1057 with cache_dir() as workspace:
1058 src = (workspace / "test.py").resolve()
1059 with src.open("w") as fobj:
1060 fobj.write("print('hello')")
1061 with patch("black.read_cache") as read_cache, patch(
1064 self.invokeBlack([str(src), "--diff", "--color"])
1065 cache_file = black.get_cache_file(mode)
1066 self.assertFalse(cache_file.exists())
1067 write_cache.assert_not_called()
1068 read_cache.assert_not_called()
1071 def test_output_locking_when_writeback_diff(self) -> None:
1072 with cache_dir() as workspace:
1073 for tag in range(0, 4):
1074 src = (workspace / f"test{tag}.py").resolve()
1075 with src.open("w") as fobj:
1076 fobj.write("print('hello')")
1077 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1078 self.invokeBlack(["--diff", str(workspace)], exit_code=0)
1079 # this isn't quite doing what we want, but if it _isn't_
1080 # called then we cannot be using the lock it provides
1084 def test_output_locking_when_writeback_color_diff(self) -> None:
1085 with cache_dir() as workspace:
1086 for tag in range(0, 4):
1087 src = (workspace / f"test{tag}.py").resolve()
1088 with src.open("w") as fobj:
1089 fobj.write("print('hello')")
1090 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1091 self.invokeBlack(["--diff", "--color", str(workspace)], exit_code=0)
1092 # this isn't quite doing what we want, but if it _isn't_
1093 # called then we cannot be using the lock it provides
1096 def test_no_cache_when_stdin(self) -> None:
1099 result = CliRunner().invoke(
1100 black.main, ["-"], input=BytesIO(b"print('hello')")
1102 self.assertEqual(result.exit_code, 0)
1103 cache_file = black.get_cache_file(mode)
1104 self.assertFalse(cache_file.exists())
1106 def test_read_cache_no_cachefile(self) -> None:
1109 self.assertEqual(black.read_cache(mode), {})
1111 def test_write_cache_read_cache(self) -> None:
1113 with cache_dir() as workspace:
1114 src = (workspace / "test.py").resolve()
1116 black.write_cache({}, [src], mode)
1117 cache = black.read_cache(mode)
1118 self.assertIn(str(src), cache)
1119 self.assertEqual(cache[str(src)], black.get_cache_info(src))
1121 def test_filter_cached(self) -> None:
1122 with TemporaryDirectory() as workspace:
1123 path = Path(workspace)
1124 uncached = (path / "uncached").resolve()
1125 cached = (path / "cached").resolve()
1126 cached_but_changed = (path / "changed").resolve()
1129 cached_but_changed.touch()
1131 str(cached): black.get_cache_info(cached),
1132 str(cached_but_changed): (0.0, 0),
1134 todo, done = black.filter_cached(
1135 cache, {uncached, cached, cached_but_changed}
1137 self.assertEqual(todo, {uncached, cached_but_changed})
1138 self.assertEqual(done, {cached})
1140 def test_write_cache_creates_directory_if_needed(self) -> None:
1142 with cache_dir(exists=False) as workspace:
1143 self.assertFalse(workspace.exists())
1144 black.write_cache({}, [], mode)
1145 self.assertTrue(workspace.exists())
1148 def test_failed_formatting_does_not_get_cached(self) -> None:
1150 with cache_dir() as workspace, patch(
1151 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1153 failing = (workspace / "failing.py").resolve()
1154 with failing.open("w") as fobj:
1155 fobj.write("not actually python")
1156 clean = (workspace / "clean.py").resolve()
1157 with clean.open("w") as fobj:
1158 fobj.write('print("hello")\n')
1159 self.invokeBlack([str(workspace)], exit_code=123)
1160 cache = black.read_cache(mode)
1161 self.assertNotIn(str(failing), cache)
1162 self.assertIn(str(clean), cache)
1164 def test_write_cache_write_fail(self) -> None:
1166 with cache_dir(), patch.object(Path, "open") as mock:
1167 mock.side_effect = OSError
1168 black.write_cache({}, [], mode)
1171 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1172 def test_works_in_mono_process_only_environment(self) -> None:
1173 with cache_dir() as workspace:
1175 (workspace / "one.py").resolve(),
1176 (workspace / "two.py").resolve(),
1178 f.write_text('print("hello")\n')
1179 self.invokeBlack([str(workspace)])
1182 def test_check_diff_use_together(self) -> None:
1184 # Files which will be reformatted.
1185 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
1186 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1187 # Files which will not be reformatted.
1188 src2 = (THIS_DIR / "data" / "composition.py").resolve()
1189 self.invokeBlack([str(src2), "--diff", "--check"])
1190 # Multi file command.
1191 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1193 def test_no_files(self) -> None:
1195 # Without an argument, black exits with error code 0.
1196 self.invokeBlack([])
1198 def test_broken_symlink(self) -> None:
1199 with cache_dir() as workspace:
1200 symlink = workspace / "broken_link.py"
1202 symlink.symlink_to("nonexistent.py")
1203 except OSError as e:
1204 self.skipTest(f"Can't create symlinks: {e}")
1205 self.invokeBlack([str(workspace.resolve())])
1207 def test_read_cache_line_lengths(self) -> None:
1209 short_mode = replace(DEFAULT_MODE, line_length=1)
1210 with cache_dir() as workspace:
1211 path = (workspace / "file.py").resolve()
1213 black.write_cache({}, [path], mode)
1214 one = black.read_cache(mode)
1215 self.assertIn(str(path), one)
1216 two = black.read_cache(short_mode)
1217 self.assertNotIn(str(path), two)
1219 def test_single_file_force_pyi(self) -> None:
1220 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1221 contents, expected = read_data("force_pyi")
1222 with cache_dir() as workspace:
1223 path = (workspace / "file.py").resolve()
1224 with open(path, "w") as fh:
1226 self.invokeBlack([str(path), "--pyi"])
1227 with open(path, "r") as fh:
1229 # verify cache with --pyi is separate
1230 pyi_cache = black.read_cache(pyi_mode)
1231 self.assertIn(str(path), pyi_cache)
1232 normal_cache = black.read_cache(DEFAULT_MODE)
1233 self.assertNotIn(str(path), normal_cache)
1234 self.assertFormatEqual(expected, actual)
1235 black.assert_equivalent(contents, actual)
1236 black.assert_stable(contents, actual, pyi_mode)
1239 def test_multi_file_force_pyi(self) -> None:
1240 reg_mode = DEFAULT_MODE
1241 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1242 contents, expected = read_data("force_pyi")
1243 with cache_dir() as workspace:
1245 (workspace / "file1.py").resolve(),
1246 (workspace / "file2.py").resolve(),
1249 with open(path, "w") as fh:
1251 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1253 with open(path, "r") as fh:
1255 self.assertEqual(actual, expected)
1256 # verify cache with --pyi is separate
1257 pyi_cache = black.read_cache(pyi_mode)
1258 normal_cache = black.read_cache(reg_mode)
1260 self.assertIn(str(path), pyi_cache)
1261 self.assertNotIn(str(path), normal_cache)
1263 def test_pipe_force_pyi(self) -> None:
1264 source, expected = read_data("force_pyi")
1265 result = CliRunner().invoke(
1266 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1268 self.assertEqual(result.exit_code, 0)
1269 actual = result.output
1270 self.assertFormatEqual(actual, expected)
1272 def test_single_file_force_py36(self) -> None:
1273 reg_mode = DEFAULT_MODE
1274 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1275 source, expected = read_data("force_py36")
1276 with cache_dir() as workspace:
1277 path = (workspace / "file.py").resolve()
1278 with open(path, "w") as fh:
1280 self.invokeBlack([str(path), *PY36_ARGS])
1281 with open(path, "r") as fh:
1283 # verify cache with --target-version is separate
1284 py36_cache = black.read_cache(py36_mode)
1285 self.assertIn(str(path), py36_cache)
1286 normal_cache = black.read_cache(reg_mode)
1287 self.assertNotIn(str(path), normal_cache)
1288 self.assertEqual(actual, expected)
1291 def test_multi_file_force_py36(self) -> None:
1292 reg_mode = DEFAULT_MODE
1293 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1294 source, expected = read_data("force_py36")
1295 with cache_dir() as workspace:
1297 (workspace / "file1.py").resolve(),
1298 (workspace / "file2.py").resolve(),
1301 with open(path, "w") as fh:
1303 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1305 with open(path, "r") as fh:
1307 self.assertEqual(actual, expected)
1308 # verify cache with --target-version is separate
1309 pyi_cache = black.read_cache(py36_mode)
1310 normal_cache = black.read_cache(reg_mode)
1312 self.assertIn(str(path), pyi_cache)
1313 self.assertNotIn(str(path), normal_cache)
1315 def test_pipe_force_py36(self) -> None:
1316 source, expected = read_data("force_py36")
1317 result = CliRunner().invoke(
1319 ["-", "-q", "--target-version=py36"],
1320 input=BytesIO(source.encode("utf8")),
1322 self.assertEqual(result.exit_code, 0)
1323 actual = result.output
1324 self.assertFormatEqual(actual, expected)
1326 def test_include_exclude(self) -> None:
1327 path = THIS_DIR / "data" / "include_exclude_tests"
1328 include = re.compile(r"\.pyi?$")
1329 exclude = re.compile(r"/exclude/|/\.definitely_exclude/")
1330 report = black.Report()
1331 gitignore = PathSpec.from_lines("gitwildmatch", [])
1332 sources: List[Path] = []
1334 Path(path / "b/dont_exclude/a.py"),
1335 Path(path / "b/dont_exclude/a.pyi"),
1337 this_abs = THIS_DIR.resolve()
1339 black.gen_python_files(
1340 path.iterdir(), this_abs, include, exclude, None, report, gitignore
1343 self.assertEqual(sorted(expected), sorted(sources))
1345 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1346 def test_exclude_for_issue_1572(self) -> None:
1347 # Exclude shouldn't touch files that were explicitly given to Black through the
1348 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1349 # https://github.com/psf/black/issues/1572
1350 path = THIS_DIR / "data" / "include_exclude_tests"
1352 exclude = r"/exclude/|a\.py"
1353 src = str(path / "b/exclude/a.py")
1354 report = black.Report()
1355 expected = [Path(path / "b/exclude/a.py")]
1366 stdin_filename=None,
1369 self.assertEqual(sorted(expected), sorted(sources))
1371 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1372 def test_get_sources_with_stdin(self) -> None:
1374 exclude = r"/exclude/|a\.py"
1376 report = black.Report()
1377 expected = [Path("-")]
1388 stdin_filename=None,
1391 self.assertEqual(sorted(expected), sorted(sources))
1393 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1394 def test_get_sources_with_stdin_filename(self) -> None:
1396 exclude = r"/exclude/|a\.py"
1398 report = black.Report()
1399 stdin_filename = str(THIS_DIR / "data/collections.py")
1400 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1411 stdin_filename=stdin_filename,
1414 self.assertEqual(sorted(expected), sorted(sources))
1416 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1417 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
1418 # Exclude shouldn't exclude stdin_filename since it is mimicing the
1419 # file being passed directly. This is the same as
1420 # test_exclude_for_issue_1572
1421 path = THIS_DIR / "data" / "include_exclude_tests"
1423 exclude = r"/exclude/|a\.py"
1425 report = black.Report()
1426 stdin_filename = str(path / "b/exclude/a.py")
1427 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1438 stdin_filename=stdin_filename,
1441 self.assertEqual(sorted(expected), sorted(sources))
1443 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1444 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
1445 # Force exclude should exclude the file when passing it through
1447 path = THIS_DIR / "data" / "include_exclude_tests"
1449 force_exclude = r"/exclude/|a\.py"
1451 report = black.Report()
1452 stdin_filename = str(path / "b/exclude/a.py")
1461 force_exclude=force_exclude,
1463 stdin_filename=stdin_filename,
1466 self.assertEqual([], sorted(sources))
1468 def test_reformat_one_with_stdin(self) -> None:
1470 "black.format_stdin_to_stdout",
1471 return_value=lambda *args, **kwargs: black.Changed.YES,
1473 report = MagicMock()
1478 write_back=black.WriteBack.YES,
1482 fsts.assert_called_once()
1483 report.done.assert_called_with(path, black.Changed.YES)
1485 def test_reformat_one_with_stdin_filename(self) -> None:
1487 "black.format_stdin_to_stdout",
1488 return_value=lambda *args, **kwargs: black.Changed.YES,
1490 report = MagicMock()
1492 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1497 write_back=black.WriteBack.YES,
1501 fsts.assert_called_once()
1502 # __BLACK_STDIN_FILENAME__ should have been striped
1503 report.done.assert_called_with(expected, black.Changed.YES)
1505 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1507 "black.format_stdin_to_stdout",
1508 return_value=lambda *args, **kwargs: black.Changed.YES,
1510 report = MagicMock()
1511 # Even with an existing file, since we are forcing stdin, black
1512 # should output to stdout and not modify the file inplace
1513 p = Path(str(THIS_DIR / "data/collections.py"))
1514 # Make sure is_file actually returns True
1515 self.assertTrue(p.is_file())
1516 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1521 write_back=black.WriteBack.YES,
1525 fsts.assert_called_once()
1526 # __BLACK_STDIN_FILENAME__ should have been striped
1527 report.done.assert_called_with(expected, black.Changed.YES)
1529 def test_gitignore_exclude(self) -> None:
1530 path = THIS_DIR / "data" / "include_exclude_tests"
1531 include = re.compile(r"\.pyi?$")
1532 exclude = re.compile(r"")
1533 report = black.Report()
1534 gitignore = PathSpec.from_lines(
1535 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1537 sources: List[Path] = []
1539 Path(path / "b/dont_exclude/a.py"),
1540 Path(path / "b/dont_exclude/a.pyi"),
1542 this_abs = THIS_DIR.resolve()
1544 black.gen_python_files(
1545 path.iterdir(), this_abs, include, exclude, None, report, gitignore
1548 self.assertEqual(sorted(expected), sorted(sources))
1550 def test_empty_include(self) -> None:
1551 path = THIS_DIR / "data" / "include_exclude_tests"
1552 report = black.Report()
1553 gitignore = PathSpec.from_lines("gitwildmatch", [])
1554 empty = re.compile(r"")
1555 sources: List[Path] = []
1557 Path(path / "b/exclude/a.pie"),
1558 Path(path / "b/exclude/a.py"),
1559 Path(path / "b/exclude/a.pyi"),
1560 Path(path / "b/dont_exclude/a.pie"),
1561 Path(path / "b/dont_exclude/a.py"),
1562 Path(path / "b/dont_exclude/a.pyi"),
1563 Path(path / "b/.definitely_exclude/a.pie"),
1564 Path(path / "b/.definitely_exclude/a.py"),
1565 Path(path / "b/.definitely_exclude/a.pyi"),
1567 this_abs = THIS_DIR.resolve()
1569 black.gen_python_files(
1573 re.compile(black.DEFAULT_EXCLUDES),
1579 self.assertEqual(sorted(expected), sorted(sources))
1581 def test_empty_exclude(self) -> None:
1582 path = THIS_DIR / "data" / "include_exclude_tests"
1583 report = black.Report()
1584 gitignore = PathSpec.from_lines("gitwildmatch", [])
1585 empty = re.compile(r"")
1586 sources: List[Path] = []
1588 Path(path / "b/dont_exclude/a.py"),
1589 Path(path / "b/dont_exclude/a.pyi"),
1590 Path(path / "b/exclude/a.py"),
1591 Path(path / "b/exclude/a.pyi"),
1592 Path(path / "b/.definitely_exclude/a.py"),
1593 Path(path / "b/.definitely_exclude/a.pyi"),
1595 this_abs = THIS_DIR.resolve()
1597 black.gen_python_files(
1600 re.compile(black.DEFAULT_INCLUDES),
1607 self.assertEqual(sorted(expected), sorted(sources))
1609 def test_invalid_include_exclude(self) -> None:
1610 for option in ["--include", "--exclude"]:
1611 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1613 def test_preserves_line_endings(self) -> None:
1614 with TemporaryDirectory() as workspace:
1615 test_file = Path(workspace) / "test.py"
1616 for nl in ["\n", "\r\n"]:
1617 contents = nl.join(["def f( ):", " pass"])
1618 test_file.write_bytes(contents.encode())
1619 ff(test_file, write_back=black.WriteBack.YES)
1620 updated_contents: bytes = test_file.read_bytes()
1621 self.assertIn(nl.encode(), updated_contents)
1623 self.assertNotIn(b"\r\n", updated_contents)
1625 def test_preserves_line_endings_via_stdin(self) -> None:
1626 for nl in ["\n", "\r\n"]:
1627 contents = nl.join(["def f( ):", " pass"])
1628 runner = BlackRunner()
1629 result = runner.invoke(
1630 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1632 self.assertEqual(result.exit_code, 0)
1633 output = runner.stdout_bytes
1634 self.assertIn(nl.encode("utf8"), output)
1636 self.assertNotIn(b"\r\n", output)
1638 def test_assert_equivalent_different_asts(self) -> None:
1639 with self.assertRaises(AssertionError):
1640 black.assert_equivalent("{}", "None")
1642 def test_symlink_out_of_root_directory(self) -> None:
1644 root = THIS_DIR.resolve()
1646 include = re.compile(black.DEFAULT_INCLUDES)
1647 exclude = re.compile(black.DEFAULT_EXCLUDES)
1648 report = black.Report()
1649 gitignore = PathSpec.from_lines("gitwildmatch", [])
1650 # `child` should behave like a symlink which resolved path is clearly
1651 # outside of the `root` directory.
1652 path.iterdir.return_value = [child]
1653 child.resolve.return_value = Path("/a/b/c")
1654 child.as_posix.return_value = "/a/b/c"
1655 child.is_symlink.return_value = True
1658 black.gen_python_files(
1659 path.iterdir(), root, include, exclude, None, report, gitignore
1662 except ValueError as ve:
1663 self.fail(f"`get_python_files_in_dir()` failed: {ve}")
1664 path.iterdir.assert_called_once()
1665 child.resolve.assert_called_once()
1666 child.is_symlink.assert_called_once()
1667 # `child` should behave like a strange file which resolved path is clearly
1668 # outside of the `root` directory.
1669 child.is_symlink.return_value = False
1670 with self.assertRaises(ValueError):
1672 black.gen_python_files(
1673 path.iterdir(), root, include, exclude, None, report, gitignore
1676 path.iterdir.assert_called()
1677 self.assertEqual(path.iterdir.call_count, 2)
1678 child.resolve.assert_called()
1679 self.assertEqual(child.resolve.call_count, 2)
1680 child.is_symlink.assert_called()
1681 self.assertEqual(child.is_symlink.call_count, 2)
1683 def test_shhh_click(self) -> None:
1685 from click import _unicodefun # type: ignore
1686 except ModuleNotFoundError:
1687 self.skipTest("Incompatible Click version")
1688 if not hasattr(_unicodefun, "_verify_python3_env"):
1689 self.skipTest("Incompatible Click version")
1690 # First, let's see if Click is crashing with a preferred ASCII charset.
1691 with patch("locale.getpreferredencoding") as gpe:
1692 gpe.return_value = "ASCII"
1693 with self.assertRaises(RuntimeError):
1694 _unicodefun._verify_python3_env()
1695 # Now, let's silence Click...
1697 # ...and confirm it's silent.
1698 with patch("locale.getpreferredencoding") as gpe:
1699 gpe.return_value = "ASCII"
1701 _unicodefun._verify_python3_env()
1702 except RuntimeError as re:
1703 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1705 def test_root_logger_not_used_directly(self) -> None:
1706 def fail(*args: Any, **kwargs: Any) -> None:
1707 self.fail("Record created with root logger")
1709 with patch.multiple(
1720 def test_invalid_config_return_code(self) -> None:
1721 tmp_file = Path(black.dump_to_file())
1723 tmp_config = Path(black.dump_to_file())
1725 args = ["--config", str(tmp_config), str(tmp_file)]
1726 self.invokeBlack(args, exit_code=2, ignore_config=False)
1730 def test_parse_pyproject_toml(self) -> None:
1731 test_toml_file = THIS_DIR / "test.toml"
1732 config = black.parse_pyproject_toml(str(test_toml_file))
1733 self.assertEqual(config["verbose"], 1)
1734 self.assertEqual(config["check"], "no")
1735 self.assertEqual(config["diff"], "y")
1736 self.assertEqual(config["color"], True)
1737 self.assertEqual(config["line_length"], 79)
1738 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1739 self.assertEqual(config["exclude"], r"\.pyi?$")
1740 self.assertEqual(config["include"], r"\.py?$")
1742 def test_read_pyproject_toml(self) -> None:
1743 test_toml_file = THIS_DIR / "test.toml"
1744 fake_ctx = FakeContext()
1745 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1746 config = fake_ctx.default_map
1747 self.assertEqual(config["verbose"], "1")
1748 self.assertEqual(config["check"], "no")
1749 self.assertEqual(config["diff"], "y")
1750 self.assertEqual(config["color"], "True")
1751 self.assertEqual(config["line_length"], "79")
1752 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1753 self.assertEqual(config["exclude"], r"\.pyi?$")
1754 self.assertEqual(config["include"], r"\.py?$")
1756 def test_find_project_root(self) -> None:
1757 with TemporaryDirectory() as workspace:
1758 root = Path(workspace)
1759 test_dir = root / "test"
1762 src_dir = root / "src"
1765 root_pyproject = root / "pyproject.toml"
1766 root_pyproject.touch()
1767 src_pyproject = src_dir / "pyproject.toml"
1768 src_pyproject.touch()
1769 src_python = src_dir / "foo.py"
1773 black.find_project_root((src_dir, test_dir)), root.resolve()
1775 self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1776 self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1778 def test_bpo_33660_workaround(self) -> None:
1779 if system() == "Windows":
1782 # https://bugs.python.org/issue33660
1784 old_cwd = Path.cwd()
1788 path = Path("workspace") / "project"
1789 report = black.Report(verbose=True)
1790 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1791 self.assertEqual(normalized_path, "workspace/project")
1793 os.chdir(str(old_cwd))
1795 def test_newline_comment_interaction(self) -> None:
1796 source = "class A:\\\r\n# type: ignore\n pass\n"
1797 output = black.format_str(source, mode=DEFAULT_MODE)
1798 black.assert_stable(source, output, mode=DEFAULT_MODE)
1800 def test_bpo_2142_workaround(self) -> None:
1802 # https://bugs.python.org/issue2142
1804 source, _ = read_data("missing_final_newline.py")
1805 # read_data adds a trailing newline
1806 source = source.rstrip()
1807 expected, _ = read_data("missing_final_newline.diff")
1808 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1809 diff_header = re.compile(
1810 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1811 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1814 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1815 self.assertEqual(result.exit_code, 0)
1818 actual = result.output
1819 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1820 self.assertEqual(actual, expected)
1823 with open(black.__file__, "r", encoding="utf-8") as _bf:
1824 black_source_lines = _bf.readlines()
1827 def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
1828 """Show function calls `from black/__init__.py` as they happen.
1830 Register this with `sys.settrace()` in a test you're debugging.
1835 stack = len(inspect.stack()) - 19
1837 filename = frame.f_code.co_filename
1838 lineno = frame.f_lineno
1839 func_sig_lineno = lineno - 1
1840 funcname = black_source_lines[func_sig_lineno].strip()
1841 while funcname.startswith("@"):
1842 func_sig_lineno += 1
1843 funcname = black_source_lines[func_sig_lineno].strip()
1844 if "black/__init__.py" in filename:
1845 print(f"{' ' * stack}{lineno}:{funcname}")
1849 if __name__ == "__main__":
1850 unittest.main(module="test_black")