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 actual = actual.rstrip() + "\n" # the diff output has a trailing space
304 if expected != actual:
305 dump = black.dump_to_file(actual)
307 "Expected diff isn't equal to the actual. If you made changes to"
308 " expression.py and this is an anticipated difference, overwrite"
309 f" tests/data/expression.diff with {dump}"
311 self.assertEqual(expected, actual, msg)
313 def test_expression_diff_with_color(self) -> None:
314 source, _ = read_data("expression.py")
315 expected, _ = read_data("expression.diff")
316 tmp_file = Path(black.dump_to_file(source))
318 result = BlackRunner().invoke(
319 black.main, ["--diff", "--color", str(tmp_file)]
323 actual = result.output
324 # We check the contents of the diff in `test_expression_diff`. All
325 # we need to check here is that color codes exist in the result.
326 self.assertIn("\033[1;37m", actual)
327 self.assertIn("\033[36m", actual)
328 self.assertIn("\033[32m", actual)
329 self.assertIn("\033[31m", actual)
330 self.assertIn("\033[0m", actual)
332 @patch("black.dump_to_file", dump_to_stderr)
333 def test_pep_570(self) -> None:
334 source, expected = read_data("pep_570")
336 self.assertFormatEqual(expected, actual)
337 black.assert_stable(source, actual, DEFAULT_MODE)
338 if sys.version_info >= (3, 8):
339 black.assert_equivalent(source, actual)
341 def test_detect_pos_only_arguments(self) -> None:
342 source, _ = read_data("pep_570")
343 root = black.lib2to3_parse(source)
344 features = black.get_features_used(root)
345 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
346 versions = black.detect_target_versions(root)
347 self.assertIn(black.TargetVersion.PY38, versions)
349 @patch("black.dump_to_file", dump_to_stderr)
350 def test_string_quotes(self) -> None:
351 source, expected = read_data("string_quotes")
353 self.assertFormatEqual(expected, actual)
354 black.assert_equivalent(source, actual)
355 black.assert_stable(source, actual, DEFAULT_MODE)
356 mode = replace(DEFAULT_MODE, string_normalization=False)
357 not_normalized = fs(source, mode=mode)
358 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
359 black.assert_equivalent(source, not_normalized)
360 black.assert_stable(source, not_normalized, mode=mode)
362 @patch("black.dump_to_file", dump_to_stderr)
363 def test_docstring_no_string_normalization(self) -> None:
364 """Like test_docstring but with string normalization off."""
365 source, expected = read_data("docstring_no_string_normalization")
366 mode = replace(DEFAULT_MODE, string_normalization=False)
367 actual = fs(source, mode=mode)
368 self.assertFormatEqual(expected, actual)
369 black.assert_equivalent(source, actual)
370 black.assert_stable(source, actual, mode)
372 def test_long_strings_flag_disabled(self) -> None:
373 """Tests for turning off the string processing logic."""
374 source, expected = read_data("long_strings_flag_disabled")
375 mode = replace(DEFAULT_MODE, experimental_string_processing=False)
376 actual = fs(source, mode=mode)
377 self.assertFormatEqual(expected, actual)
378 black.assert_stable(expected, actual, mode)
380 @patch("black.dump_to_file", dump_to_stderr)
381 def test_numeric_literals(self) -> None:
382 source, expected = read_data("numeric_literals")
383 mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
384 actual = fs(source, mode=mode)
385 self.assertFormatEqual(expected, actual)
386 black.assert_equivalent(source, actual)
387 black.assert_stable(source, actual, mode)
389 @patch("black.dump_to_file", dump_to_stderr)
390 def test_numeric_literals_ignoring_underscores(self) -> None:
391 source, expected = read_data("numeric_literals_skip_underscores")
392 mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
393 actual = fs(source, mode=mode)
394 self.assertFormatEqual(expected, actual)
395 black.assert_equivalent(source, actual)
396 black.assert_stable(source, actual, mode)
398 @patch("black.dump_to_file", dump_to_stderr)
399 def test_python2_print_function(self) -> None:
400 source, expected = read_data("python2_print_function")
401 mode = replace(DEFAULT_MODE, target_versions={TargetVersion.PY27})
402 actual = fs(source, mode=mode)
403 self.assertFormatEqual(expected, actual)
404 black.assert_equivalent(source, actual)
405 black.assert_stable(source, actual, mode)
407 @patch("black.dump_to_file", dump_to_stderr)
408 def test_stub(self) -> None:
409 mode = replace(DEFAULT_MODE, is_pyi=True)
410 source, expected = read_data("stub.pyi")
411 actual = fs(source, mode=mode)
412 self.assertFormatEqual(expected, actual)
413 black.assert_stable(source, actual, mode)
415 @patch("black.dump_to_file", dump_to_stderr)
416 def test_async_as_identifier(self) -> None:
417 source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
418 source, expected = read_data("async_as_identifier")
420 self.assertFormatEqual(expected, actual)
421 major, minor = sys.version_info[:2]
422 if major < 3 or (major <= 3 and minor < 7):
423 black.assert_equivalent(source, actual)
424 black.assert_stable(source, actual, DEFAULT_MODE)
425 # ensure black can parse this when the target is 3.6
426 self.invokeBlack([str(source_path), "--target-version", "py36"])
427 # but not on 3.7, because async/await is no longer an identifier
428 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
430 @patch("black.dump_to_file", dump_to_stderr)
431 def test_python37(self) -> None:
432 source_path = (THIS_DIR / "data" / "python37.py").resolve()
433 source, expected = read_data("python37")
435 self.assertFormatEqual(expected, actual)
436 major, minor = sys.version_info[:2]
437 if major > 3 or (major == 3 and minor >= 7):
438 black.assert_equivalent(source, actual)
439 black.assert_stable(source, actual, DEFAULT_MODE)
440 # ensure black can parse this when the target is 3.7
441 self.invokeBlack([str(source_path), "--target-version", "py37"])
442 # but not on 3.6, because we use async as a reserved keyword
443 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
445 @patch("black.dump_to_file", dump_to_stderr)
446 def test_python38(self) -> None:
447 source, expected = read_data("python38")
449 self.assertFormatEqual(expected, actual)
450 major, minor = sys.version_info[:2]
451 if major > 3 or (major == 3 and minor >= 8):
452 black.assert_equivalent(source, actual)
453 black.assert_stable(source, actual, DEFAULT_MODE)
455 @patch("black.dump_to_file", dump_to_stderr)
456 def test_python39(self) -> None:
457 source, expected = read_data("python39")
459 self.assertFormatEqual(expected, actual)
460 major, minor = sys.version_info[:2]
461 if major > 3 or (major == 3 and minor >= 9):
462 black.assert_equivalent(source, actual)
463 black.assert_stable(source, actual, DEFAULT_MODE)
465 def test_tab_comment_indentation(self) -> None:
466 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
467 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
468 self.assertFormatEqual(contents_spc, fs(contents_spc))
469 self.assertFormatEqual(contents_spc, fs(contents_tab))
471 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
472 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
473 self.assertFormatEqual(contents_spc, fs(contents_spc))
474 self.assertFormatEqual(contents_spc, fs(contents_tab))
476 # mixed tabs and spaces (valid Python 2 code)
477 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
478 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
479 self.assertFormatEqual(contents_spc, fs(contents_spc))
480 self.assertFormatEqual(contents_spc, fs(contents_tab))
482 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
483 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
484 self.assertFormatEqual(contents_spc, fs(contents_spc))
485 self.assertFormatEqual(contents_spc, fs(contents_tab))
487 def test_report_verbose(self) -> None:
488 report = black.Report(verbose=True)
492 def out(msg: str, **kwargs: Any) -> None:
493 out_lines.append(msg)
495 def err(msg: str, **kwargs: Any) -> None:
496 err_lines.append(msg)
498 with patch("black.out", out), patch("black.err", err):
499 report.done(Path("f1"), black.Changed.NO)
500 self.assertEqual(len(out_lines), 1)
501 self.assertEqual(len(err_lines), 0)
502 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
503 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
504 self.assertEqual(report.return_code, 0)
505 report.done(Path("f2"), black.Changed.YES)
506 self.assertEqual(len(out_lines), 2)
507 self.assertEqual(len(err_lines), 0)
508 self.assertEqual(out_lines[-1], "reformatted f2")
510 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
512 report.done(Path("f3"), black.Changed.CACHED)
513 self.assertEqual(len(out_lines), 3)
514 self.assertEqual(len(err_lines), 0)
516 out_lines[-1], "f3 wasn't modified on disk since last run."
519 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
521 self.assertEqual(report.return_code, 0)
523 self.assertEqual(report.return_code, 1)
525 report.failed(Path("e1"), "boom")
526 self.assertEqual(len(out_lines), 3)
527 self.assertEqual(len(err_lines), 1)
528 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
530 unstyle(str(report)),
531 "1 file reformatted, 2 files left unchanged, 1 file failed to"
534 self.assertEqual(report.return_code, 123)
535 report.done(Path("f3"), black.Changed.YES)
536 self.assertEqual(len(out_lines), 4)
537 self.assertEqual(len(err_lines), 1)
538 self.assertEqual(out_lines[-1], "reformatted f3")
540 unstyle(str(report)),
541 "2 files reformatted, 2 files left unchanged, 1 file failed to"
544 self.assertEqual(report.return_code, 123)
545 report.failed(Path("e2"), "boom")
546 self.assertEqual(len(out_lines), 4)
547 self.assertEqual(len(err_lines), 2)
548 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
550 unstyle(str(report)),
551 "2 files reformatted, 2 files left unchanged, 2 files failed to"
554 self.assertEqual(report.return_code, 123)
555 report.path_ignored(Path("wat"), "no match")
556 self.assertEqual(len(out_lines), 5)
557 self.assertEqual(len(err_lines), 2)
558 self.assertEqual(out_lines[-1], "wat ignored: no match")
560 unstyle(str(report)),
561 "2 files reformatted, 2 files left unchanged, 2 files failed to"
564 self.assertEqual(report.return_code, 123)
565 report.done(Path("f4"), black.Changed.NO)
566 self.assertEqual(len(out_lines), 6)
567 self.assertEqual(len(err_lines), 2)
568 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
570 unstyle(str(report)),
571 "2 files reformatted, 3 files left unchanged, 2 files failed to"
574 self.assertEqual(report.return_code, 123)
577 unstyle(str(report)),
578 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
579 " would fail to reformat.",
584 unstyle(str(report)),
585 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
586 " would fail to reformat.",
589 def test_report_quiet(self) -> None:
590 report = black.Report(quiet=True)
594 def out(msg: str, **kwargs: Any) -> None:
595 out_lines.append(msg)
597 def err(msg: str, **kwargs: Any) -> None:
598 err_lines.append(msg)
600 with patch("black.out", out), patch("black.err", err):
601 report.done(Path("f1"), black.Changed.NO)
602 self.assertEqual(len(out_lines), 0)
603 self.assertEqual(len(err_lines), 0)
604 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
605 self.assertEqual(report.return_code, 0)
606 report.done(Path("f2"), black.Changed.YES)
607 self.assertEqual(len(out_lines), 0)
608 self.assertEqual(len(err_lines), 0)
610 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
612 report.done(Path("f3"), black.Changed.CACHED)
613 self.assertEqual(len(out_lines), 0)
614 self.assertEqual(len(err_lines), 0)
616 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
618 self.assertEqual(report.return_code, 0)
620 self.assertEqual(report.return_code, 1)
622 report.failed(Path("e1"), "boom")
623 self.assertEqual(len(out_lines), 0)
624 self.assertEqual(len(err_lines), 1)
625 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
627 unstyle(str(report)),
628 "1 file reformatted, 2 files left unchanged, 1 file failed to"
631 self.assertEqual(report.return_code, 123)
632 report.done(Path("f3"), black.Changed.YES)
633 self.assertEqual(len(out_lines), 0)
634 self.assertEqual(len(err_lines), 1)
636 unstyle(str(report)),
637 "2 files reformatted, 2 files left unchanged, 1 file failed to"
640 self.assertEqual(report.return_code, 123)
641 report.failed(Path("e2"), "boom")
642 self.assertEqual(len(out_lines), 0)
643 self.assertEqual(len(err_lines), 2)
644 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
646 unstyle(str(report)),
647 "2 files reformatted, 2 files left unchanged, 2 files failed to"
650 self.assertEqual(report.return_code, 123)
651 report.path_ignored(Path("wat"), "no match")
652 self.assertEqual(len(out_lines), 0)
653 self.assertEqual(len(err_lines), 2)
655 unstyle(str(report)),
656 "2 files reformatted, 2 files left unchanged, 2 files failed to"
659 self.assertEqual(report.return_code, 123)
660 report.done(Path("f4"), black.Changed.NO)
661 self.assertEqual(len(out_lines), 0)
662 self.assertEqual(len(err_lines), 2)
664 unstyle(str(report)),
665 "2 files reformatted, 3 files left unchanged, 2 files failed to"
668 self.assertEqual(report.return_code, 123)
671 unstyle(str(report)),
672 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
673 " would fail to reformat.",
678 unstyle(str(report)),
679 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
680 " would fail to reformat.",
683 def test_report_normal(self) -> None:
684 report = black.Report()
688 def out(msg: str, **kwargs: Any) -> None:
689 out_lines.append(msg)
691 def err(msg: str, **kwargs: Any) -> None:
692 err_lines.append(msg)
694 with patch("black.out", out), patch("black.err", err):
695 report.done(Path("f1"), black.Changed.NO)
696 self.assertEqual(len(out_lines), 0)
697 self.assertEqual(len(err_lines), 0)
698 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
699 self.assertEqual(report.return_code, 0)
700 report.done(Path("f2"), black.Changed.YES)
701 self.assertEqual(len(out_lines), 1)
702 self.assertEqual(len(err_lines), 0)
703 self.assertEqual(out_lines[-1], "reformatted f2")
705 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
707 report.done(Path("f3"), black.Changed.CACHED)
708 self.assertEqual(len(out_lines), 1)
709 self.assertEqual(len(err_lines), 0)
710 self.assertEqual(out_lines[-1], "reformatted f2")
712 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
714 self.assertEqual(report.return_code, 0)
716 self.assertEqual(report.return_code, 1)
718 report.failed(Path("e1"), "boom")
719 self.assertEqual(len(out_lines), 1)
720 self.assertEqual(len(err_lines), 1)
721 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
723 unstyle(str(report)),
724 "1 file reformatted, 2 files left unchanged, 1 file failed to"
727 self.assertEqual(report.return_code, 123)
728 report.done(Path("f3"), black.Changed.YES)
729 self.assertEqual(len(out_lines), 2)
730 self.assertEqual(len(err_lines), 1)
731 self.assertEqual(out_lines[-1], "reformatted f3")
733 unstyle(str(report)),
734 "2 files reformatted, 2 files left unchanged, 1 file failed to"
737 self.assertEqual(report.return_code, 123)
738 report.failed(Path("e2"), "boom")
739 self.assertEqual(len(out_lines), 2)
740 self.assertEqual(len(err_lines), 2)
741 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
743 unstyle(str(report)),
744 "2 files reformatted, 2 files left unchanged, 2 files failed to"
747 self.assertEqual(report.return_code, 123)
748 report.path_ignored(Path("wat"), "no match")
749 self.assertEqual(len(out_lines), 2)
750 self.assertEqual(len(err_lines), 2)
752 unstyle(str(report)),
753 "2 files reformatted, 2 files left unchanged, 2 files failed to"
756 self.assertEqual(report.return_code, 123)
757 report.done(Path("f4"), black.Changed.NO)
758 self.assertEqual(len(out_lines), 2)
759 self.assertEqual(len(err_lines), 2)
761 unstyle(str(report)),
762 "2 files reformatted, 3 files left unchanged, 2 files failed to"
765 self.assertEqual(report.return_code, 123)
768 unstyle(str(report)),
769 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
770 " would fail to reformat.",
775 unstyle(str(report)),
776 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
777 " would fail to reformat.",
780 def test_lib2to3_parse(self) -> None:
781 with self.assertRaises(black.InvalidInput):
782 black.lib2to3_parse("invalid syntax")
785 black.lib2to3_parse(straddling)
786 black.lib2to3_parse(straddling, {TargetVersion.PY27})
787 black.lib2to3_parse(straddling, {TargetVersion.PY36})
788 black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36})
791 black.lib2to3_parse(py2_only)
792 black.lib2to3_parse(py2_only, {TargetVersion.PY27})
793 with self.assertRaises(black.InvalidInput):
794 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
795 with self.assertRaises(black.InvalidInput):
796 black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36})
798 py3_only = "exec(x, end=y)"
799 black.lib2to3_parse(py3_only)
800 with self.assertRaises(black.InvalidInput):
801 black.lib2to3_parse(py3_only, {TargetVersion.PY27})
802 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
803 black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36})
805 def test_get_features_used_decorator(self) -> None:
806 # Test the feature detection of new decorator syntax
807 # since this makes some test cases of test_get_features_used()
808 # fails if it fails, this is tested first so that a useful case
810 simples, relaxed = read_data("decorators")
811 # skip explanation comments at the top of the file
812 for simple_test in simples.split("##")[1:]:
813 node = black.lib2to3_parse(simple_test)
814 decorator = str(node.children[0].children[0]).strip()
816 Feature.RELAXED_DECORATORS,
817 black.get_features_used(node),
819 f"decorator '{decorator}' follows python<=3.8 syntax"
820 "but is detected as 3.9+"
821 # f"The full node is\n{node!r}"
824 # skip the '# output' comment at the top of the output part
825 for relaxed_test in relaxed.split("##")[1:]:
826 node = black.lib2to3_parse(relaxed_test)
827 decorator = str(node.children[0].children[0]).strip()
829 Feature.RELAXED_DECORATORS,
830 black.get_features_used(node),
832 f"decorator '{decorator}' uses python3.9+ syntax"
833 "but is detected as python<=3.8"
834 # f"The full node is\n{node!r}"
838 def test_get_features_used(self) -> None:
839 node = black.lib2to3_parse("def f(*, arg): ...\n")
840 self.assertEqual(black.get_features_used(node), set())
841 node = black.lib2to3_parse("def f(*, arg,): ...\n")
842 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
843 node = black.lib2to3_parse("f(*arg,)\n")
845 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
847 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
848 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
849 node = black.lib2to3_parse("123_456\n")
850 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
851 node = black.lib2to3_parse("123456\n")
852 self.assertEqual(black.get_features_used(node), set())
853 source, expected = read_data("function")
854 node = black.lib2to3_parse(source)
855 expected_features = {
856 Feature.TRAILING_COMMA_IN_CALL,
857 Feature.TRAILING_COMMA_IN_DEF,
860 self.assertEqual(black.get_features_used(node), expected_features)
861 node = black.lib2to3_parse(expected)
862 self.assertEqual(black.get_features_used(node), expected_features)
863 source, expected = read_data("expression")
864 node = black.lib2to3_parse(source)
865 self.assertEqual(black.get_features_used(node), set())
866 node = black.lib2to3_parse(expected)
867 self.assertEqual(black.get_features_used(node), set())
869 def test_get_future_imports(self) -> None:
870 node = black.lib2to3_parse("\n")
871 self.assertEqual(set(), black.get_future_imports(node))
872 node = black.lib2to3_parse("from __future__ import black\n")
873 self.assertEqual({"black"}, black.get_future_imports(node))
874 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
875 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
876 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
877 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
878 node = black.lib2to3_parse(
879 "from __future__ import multiple\nfrom __future__ import imports\n"
881 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
882 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
883 self.assertEqual({"black"}, black.get_future_imports(node))
884 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
885 self.assertEqual({"black"}, black.get_future_imports(node))
886 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
887 self.assertEqual(set(), black.get_future_imports(node))
888 node = black.lib2to3_parse("from some.module import black\n")
889 self.assertEqual(set(), black.get_future_imports(node))
890 node = black.lib2to3_parse(
891 "from __future__ import unicode_literals as _unicode_literals"
893 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
894 node = black.lib2to3_parse(
895 "from __future__ import unicode_literals as _lol, print"
897 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
899 def test_debug_visitor(self) -> None:
900 source, _ = read_data("debug_visitor.py")
901 expected, _ = read_data("debug_visitor.out")
905 def out(msg: str, **kwargs: Any) -> None:
906 out_lines.append(msg)
908 def err(msg: str, **kwargs: Any) -> None:
909 err_lines.append(msg)
911 with patch("black.out", out), patch("black.err", err):
912 black.DebugVisitor.show(source)
913 actual = "\n".join(out_lines) + "\n"
915 if expected != actual:
916 log_name = black.dump_to_file(*out_lines)
920 f"AST print out is different. Actual version dumped to {log_name}",
923 def test_format_file_contents(self) -> None:
926 with self.assertRaises(black.NothingChanged):
927 black.format_file_contents(empty, mode=mode, fast=False)
929 with self.assertRaises(black.NothingChanged):
930 black.format_file_contents(just_nl, mode=mode, fast=False)
931 same = "j = [1, 2, 3]\n"
932 with self.assertRaises(black.NothingChanged):
933 black.format_file_contents(same, mode=mode, fast=False)
934 different = "j = [1,2,3]"
936 actual = black.format_file_contents(different, mode=mode, fast=False)
937 self.assertEqual(expected, actual)
938 invalid = "return if you can"
939 with self.assertRaises(black.InvalidInput) as e:
940 black.format_file_contents(invalid, mode=mode, fast=False)
941 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
943 def test_endmarker(self) -> None:
944 n = black.lib2to3_parse("\n")
945 self.assertEqual(n.type, black.syms.file_input)
946 self.assertEqual(len(n.children), 1)
947 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
949 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
950 def test_assertFormatEqual(self) -> None:
954 def out(msg: str, **kwargs: Any) -> None:
955 out_lines.append(msg)
957 def err(msg: str, **kwargs: Any) -> None:
958 err_lines.append(msg)
960 with patch("black.out", out), patch("black.err", err):
961 with self.assertRaises(AssertionError):
962 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
964 out_str = "".join(out_lines)
965 self.assertTrue("Expected tree:" in out_str)
966 self.assertTrue("Actual tree:" in out_str)
967 self.assertEqual("".join(err_lines), "")
969 def test_cache_broken_file(self) -> None:
971 with cache_dir() as workspace:
972 cache_file = black.get_cache_file(mode)
973 with cache_file.open("w") as fobj:
974 fobj.write("this is not a pickle")
975 self.assertEqual(black.read_cache(mode), {})
976 src = (workspace / "test.py").resolve()
977 with src.open("w") as fobj:
978 fobj.write("print('hello')")
979 self.invokeBlack([str(src)])
980 cache = black.read_cache(mode)
981 self.assertIn(src, cache)
983 def test_cache_single_file_already_cached(self) -> None:
985 with cache_dir() as workspace:
986 src = (workspace / "test.py").resolve()
987 with src.open("w") as fobj:
988 fobj.write("print('hello')")
989 black.write_cache({}, [src], mode)
990 self.invokeBlack([str(src)])
991 with src.open("r") as fobj:
992 self.assertEqual(fobj.read(), "print('hello')")
995 def test_cache_multiple_files(self) -> None:
997 with cache_dir() as workspace, patch(
998 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1000 one = (workspace / "one.py").resolve()
1001 with one.open("w") as fobj:
1002 fobj.write("print('hello')")
1003 two = (workspace / "two.py").resolve()
1004 with two.open("w") as fobj:
1005 fobj.write("print('hello')")
1006 black.write_cache({}, [one], mode)
1007 self.invokeBlack([str(workspace)])
1008 with one.open("r") as fobj:
1009 self.assertEqual(fobj.read(), "print('hello')")
1010 with two.open("r") as fobj:
1011 self.assertEqual(fobj.read(), 'print("hello")\n')
1012 cache = black.read_cache(mode)
1013 self.assertIn(one, cache)
1014 self.assertIn(two, cache)
1016 def test_no_cache_when_writeback_diff(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 with patch("black.read_cache") as read_cache, patch(
1025 self.invokeBlack([str(src), "--diff"])
1026 cache_file = black.get_cache_file(mode)
1027 self.assertFalse(cache_file.exists())
1028 write_cache.assert_not_called()
1029 read_cache.assert_not_called()
1031 def test_no_cache_when_writeback_color_diff(self) -> None:
1033 with cache_dir() as workspace:
1034 src = (workspace / "test.py").resolve()
1035 with src.open("w") as fobj:
1036 fobj.write("print('hello')")
1037 with patch("black.read_cache") as read_cache, patch(
1040 self.invokeBlack([str(src), "--diff", "--color"])
1041 cache_file = black.get_cache_file(mode)
1042 self.assertFalse(cache_file.exists())
1043 write_cache.assert_not_called()
1044 read_cache.assert_not_called()
1047 def test_output_locking_when_writeback_diff(self) -> None:
1048 with cache_dir() as workspace:
1049 for tag in range(0, 4):
1050 src = (workspace / f"test{tag}.py").resolve()
1051 with src.open("w") as fobj:
1052 fobj.write("print('hello')")
1053 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1054 self.invokeBlack(["--diff", str(workspace)], exit_code=0)
1055 # this isn't quite doing what we want, but if it _isn't_
1056 # called then we cannot be using the lock it provides
1060 def test_output_locking_when_writeback_color_diff(self) -> None:
1061 with cache_dir() as workspace:
1062 for tag in range(0, 4):
1063 src = (workspace / f"test{tag}.py").resolve()
1064 with src.open("w") as fobj:
1065 fobj.write("print('hello')")
1066 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1067 self.invokeBlack(["--diff", "--color", str(workspace)], exit_code=0)
1068 # this isn't quite doing what we want, but if it _isn't_
1069 # called then we cannot be using the lock it provides
1072 def test_no_cache_when_stdin(self) -> None:
1075 result = CliRunner().invoke(
1076 black.main, ["-"], input=BytesIO(b"print('hello')")
1078 self.assertEqual(result.exit_code, 0)
1079 cache_file = black.get_cache_file(mode)
1080 self.assertFalse(cache_file.exists())
1082 def test_read_cache_no_cachefile(self) -> None:
1085 self.assertEqual(black.read_cache(mode), {})
1087 def test_write_cache_read_cache(self) -> None:
1089 with cache_dir() as workspace:
1090 src = (workspace / "test.py").resolve()
1092 black.write_cache({}, [src], mode)
1093 cache = black.read_cache(mode)
1094 self.assertIn(src, cache)
1095 self.assertEqual(cache[src], black.get_cache_info(src))
1097 def test_filter_cached(self) -> None:
1098 with TemporaryDirectory() as workspace:
1099 path = Path(workspace)
1100 uncached = (path / "uncached").resolve()
1101 cached = (path / "cached").resolve()
1102 cached_but_changed = (path / "changed").resolve()
1105 cached_but_changed.touch()
1106 cache = {cached: black.get_cache_info(cached), cached_but_changed: (0.0, 0)}
1107 todo, done = black.filter_cached(
1108 cache, {uncached, cached, cached_but_changed}
1110 self.assertEqual(todo, {uncached, cached_but_changed})
1111 self.assertEqual(done, {cached})
1113 def test_write_cache_creates_directory_if_needed(self) -> None:
1115 with cache_dir(exists=False) as workspace:
1116 self.assertFalse(workspace.exists())
1117 black.write_cache({}, [], mode)
1118 self.assertTrue(workspace.exists())
1121 def test_failed_formatting_does_not_get_cached(self) -> None:
1123 with cache_dir() as workspace, patch(
1124 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1126 failing = (workspace / "failing.py").resolve()
1127 with failing.open("w") as fobj:
1128 fobj.write("not actually python")
1129 clean = (workspace / "clean.py").resolve()
1130 with clean.open("w") as fobj:
1131 fobj.write('print("hello")\n')
1132 self.invokeBlack([str(workspace)], exit_code=123)
1133 cache = black.read_cache(mode)
1134 self.assertNotIn(failing, cache)
1135 self.assertIn(clean, cache)
1137 def test_write_cache_write_fail(self) -> None:
1139 with cache_dir(), patch.object(Path, "open") as mock:
1140 mock.side_effect = OSError
1141 black.write_cache({}, [], mode)
1144 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1145 def test_works_in_mono_process_only_environment(self) -> None:
1146 with cache_dir() as workspace:
1148 (workspace / "one.py").resolve(),
1149 (workspace / "two.py").resolve(),
1151 f.write_text('print("hello")\n')
1152 self.invokeBlack([str(workspace)])
1155 def test_check_diff_use_together(self) -> None:
1157 # Files which will be reformatted.
1158 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
1159 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1160 # Files which will not be reformatted.
1161 src2 = (THIS_DIR / "data" / "composition.py").resolve()
1162 self.invokeBlack([str(src2), "--diff", "--check"])
1163 # Multi file command.
1164 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1166 def test_no_files(self) -> None:
1168 # Without an argument, black exits with error code 0.
1169 self.invokeBlack([])
1171 def test_broken_symlink(self) -> None:
1172 with cache_dir() as workspace:
1173 symlink = workspace / "broken_link.py"
1175 symlink.symlink_to("nonexistent.py")
1176 except OSError as e:
1177 self.skipTest(f"Can't create symlinks: {e}")
1178 self.invokeBlack([str(workspace.resolve())])
1180 def test_read_cache_line_lengths(self) -> None:
1182 short_mode = replace(DEFAULT_MODE, line_length=1)
1183 with cache_dir() as workspace:
1184 path = (workspace / "file.py").resolve()
1186 black.write_cache({}, [path], mode)
1187 one = black.read_cache(mode)
1188 self.assertIn(path, one)
1189 two = black.read_cache(short_mode)
1190 self.assertNotIn(path, two)
1192 def test_single_file_force_pyi(self) -> None:
1193 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1194 contents, expected = read_data("force_pyi")
1195 with cache_dir() as workspace:
1196 path = (workspace / "file.py").resolve()
1197 with open(path, "w") as fh:
1199 self.invokeBlack([str(path), "--pyi"])
1200 with open(path, "r") as fh:
1202 # verify cache with --pyi is separate
1203 pyi_cache = black.read_cache(pyi_mode)
1204 self.assertIn(path, pyi_cache)
1205 normal_cache = black.read_cache(DEFAULT_MODE)
1206 self.assertNotIn(path, normal_cache)
1207 self.assertFormatEqual(expected, actual)
1208 black.assert_equivalent(contents, actual)
1209 black.assert_stable(contents, actual, pyi_mode)
1212 def test_multi_file_force_pyi(self) -> None:
1213 reg_mode = DEFAULT_MODE
1214 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1215 contents, expected = read_data("force_pyi")
1216 with cache_dir() as workspace:
1218 (workspace / "file1.py").resolve(),
1219 (workspace / "file2.py").resolve(),
1222 with open(path, "w") as fh:
1224 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1226 with open(path, "r") as fh:
1228 self.assertEqual(actual, expected)
1229 # verify cache with --pyi is separate
1230 pyi_cache = black.read_cache(pyi_mode)
1231 normal_cache = black.read_cache(reg_mode)
1233 self.assertIn(path, pyi_cache)
1234 self.assertNotIn(path, normal_cache)
1236 def test_pipe_force_pyi(self) -> None:
1237 source, expected = read_data("force_pyi")
1238 result = CliRunner().invoke(
1239 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1241 self.assertEqual(result.exit_code, 0)
1242 actual = result.output
1243 self.assertFormatEqual(actual, expected)
1245 def test_single_file_force_py36(self) -> None:
1246 reg_mode = DEFAULT_MODE
1247 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1248 source, expected = read_data("force_py36")
1249 with cache_dir() as workspace:
1250 path = (workspace / "file.py").resolve()
1251 with open(path, "w") as fh:
1253 self.invokeBlack([str(path), *PY36_ARGS])
1254 with open(path, "r") as fh:
1256 # verify cache with --target-version is separate
1257 py36_cache = black.read_cache(py36_mode)
1258 self.assertIn(path, py36_cache)
1259 normal_cache = black.read_cache(reg_mode)
1260 self.assertNotIn(path, normal_cache)
1261 self.assertEqual(actual, expected)
1264 def test_multi_file_force_py36(self) -> None:
1265 reg_mode = DEFAULT_MODE
1266 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1267 source, expected = read_data("force_py36")
1268 with cache_dir() as workspace:
1270 (workspace / "file1.py").resolve(),
1271 (workspace / "file2.py").resolve(),
1274 with open(path, "w") as fh:
1276 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1278 with open(path, "r") as fh:
1280 self.assertEqual(actual, expected)
1281 # verify cache with --target-version is separate
1282 pyi_cache = black.read_cache(py36_mode)
1283 normal_cache = black.read_cache(reg_mode)
1285 self.assertIn(path, pyi_cache)
1286 self.assertNotIn(path, normal_cache)
1288 def test_pipe_force_py36(self) -> None:
1289 source, expected = read_data("force_py36")
1290 result = CliRunner().invoke(
1292 ["-", "-q", "--target-version=py36"],
1293 input=BytesIO(source.encode("utf8")),
1295 self.assertEqual(result.exit_code, 0)
1296 actual = result.output
1297 self.assertFormatEqual(actual, expected)
1299 def test_include_exclude(self) -> None:
1300 path = THIS_DIR / "data" / "include_exclude_tests"
1301 include = re.compile(r"\.pyi?$")
1302 exclude = re.compile(r"/exclude/|/\.definitely_exclude/")
1303 report = black.Report()
1304 gitignore = PathSpec.from_lines("gitwildmatch", [])
1305 sources: List[Path] = []
1307 Path(path / "b/dont_exclude/a.py"),
1308 Path(path / "b/dont_exclude/a.pyi"),
1310 this_abs = THIS_DIR.resolve()
1312 black.gen_python_files(
1313 path.iterdir(), this_abs, include, exclude, None, report, gitignore
1316 self.assertEqual(sorted(expected), sorted(sources))
1318 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1319 def test_exclude_for_issue_1572(self) -> None:
1320 # Exclude shouldn't touch files that were explicitly given to Black through the
1321 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1322 # https://github.com/psf/black/issues/1572
1323 path = THIS_DIR / "data" / "include_exclude_tests"
1325 exclude = r"/exclude/|a\.py"
1326 src = str(path / "b/exclude/a.py")
1327 report = black.Report()
1328 expected = [Path(path / "b/exclude/a.py")]
1339 stdin_filename=None,
1342 self.assertEqual(sorted(expected), sorted(sources))
1344 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1345 def test_get_sources_with_stdin(self) -> None:
1347 exclude = r"/exclude/|a\.py"
1349 report = black.Report()
1350 expected = [Path("-")]
1361 stdin_filename=None,
1364 self.assertEqual(sorted(expected), sorted(sources))
1366 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1367 def test_get_sources_with_stdin_filename(self) -> None:
1369 exclude = r"/exclude/|a\.py"
1371 report = black.Report()
1372 stdin_filename = str(THIS_DIR / "data/collections.py")
1373 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1384 stdin_filename=stdin_filename,
1387 self.assertEqual(sorted(expected), sorted(sources))
1389 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1390 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
1391 # Exclude shouldn't exclude stdin_filename since it is mimicing the
1392 # file being passed directly. This is the same as
1393 # test_exclude_for_issue_1572
1394 path = THIS_DIR / "data" / "include_exclude_tests"
1396 exclude = r"/exclude/|a\.py"
1398 report = black.Report()
1399 stdin_filename = str(path / "b/exclude/a.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_force_exclude(self) -> None:
1418 # Force exclude should exclude the file when passing it through
1420 path = THIS_DIR / "data" / "include_exclude_tests"
1422 force_exclude = r"/exclude/|a\.py"
1424 report = black.Report()
1425 stdin_filename = str(path / "b/exclude/a.py")
1434 force_exclude=force_exclude,
1436 stdin_filename=stdin_filename,
1439 self.assertEqual([], sorted(sources))
1441 def test_reformat_one_with_stdin(self) -> None:
1443 "black.format_stdin_to_stdout",
1444 return_value=lambda *args, **kwargs: black.Changed.YES,
1446 report = MagicMock()
1451 write_back=black.WriteBack.YES,
1455 fsts.assert_called_once()
1456 report.done.assert_called_with(path, black.Changed.YES)
1458 def test_reformat_one_with_stdin_filename(self) -> None:
1460 "black.format_stdin_to_stdout",
1461 return_value=lambda *args, **kwargs: black.Changed.YES,
1463 report = MagicMock()
1465 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1470 write_back=black.WriteBack.YES,
1474 fsts.assert_called_once()
1475 # __BLACK_STDIN_FILENAME__ should have been striped
1476 report.done.assert_called_with(expected, black.Changed.YES)
1478 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1480 "black.format_stdin_to_stdout",
1481 return_value=lambda *args, **kwargs: black.Changed.YES,
1483 report = MagicMock()
1484 # Even with an existing file, since we are forcing stdin, black
1485 # should output to stdout and not modify the file inplace
1486 p = Path(str(THIS_DIR / "data/collections.py"))
1487 # Make sure is_file actually returns True
1488 self.assertTrue(p.is_file())
1489 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1494 write_back=black.WriteBack.YES,
1498 fsts.assert_called_once()
1499 # __BLACK_STDIN_FILENAME__ should have been striped
1500 report.done.assert_called_with(expected, black.Changed.YES)
1502 def test_gitignore_exclude(self) -> None:
1503 path = THIS_DIR / "data" / "include_exclude_tests"
1504 include = re.compile(r"\.pyi?$")
1505 exclude = re.compile(r"")
1506 report = black.Report()
1507 gitignore = PathSpec.from_lines(
1508 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1510 sources: List[Path] = []
1512 Path(path / "b/dont_exclude/a.py"),
1513 Path(path / "b/dont_exclude/a.pyi"),
1515 this_abs = THIS_DIR.resolve()
1517 black.gen_python_files(
1518 path.iterdir(), this_abs, include, exclude, None, report, gitignore
1521 self.assertEqual(sorted(expected), sorted(sources))
1523 def test_empty_include(self) -> None:
1524 path = THIS_DIR / "data" / "include_exclude_tests"
1525 report = black.Report()
1526 gitignore = PathSpec.from_lines("gitwildmatch", [])
1527 empty = re.compile(r"")
1528 sources: List[Path] = []
1530 Path(path / "b/exclude/a.pie"),
1531 Path(path / "b/exclude/a.py"),
1532 Path(path / "b/exclude/a.pyi"),
1533 Path(path / "b/dont_exclude/a.pie"),
1534 Path(path / "b/dont_exclude/a.py"),
1535 Path(path / "b/dont_exclude/a.pyi"),
1536 Path(path / "b/.definitely_exclude/a.pie"),
1537 Path(path / "b/.definitely_exclude/a.py"),
1538 Path(path / "b/.definitely_exclude/a.pyi"),
1540 this_abs = THIS_DIR.resolve()
1542 black.gen_python_files(
1546 re.compile(black.DEFAULT_EXCLUDES),
1552 self.assertEqual(sorted(expected), sorted(sources))
1554 def test_empty_exclude(self) -> None:
1555 path = THIS_DIR / "data" / "include_exclude_tests"
1556 report = black.Report()
1557 gitignore = PathSpec.from_lines("gitwildmatch", [])
1558 empty = re.compile(r"")
1559 sources: List[Path] = []
1561 Path(path / "b/dont_exclude/a.py"),
1562 Path(path / "b/dont_exclude/a.pyi"),
1563 Path(path / "b/exclude/a.py"),
1564 Path(path / "b/exclude/a.pyi"),
1565 Path(path / "b/.definitely_exclude/a.py"),
1566 Path(path / "b/.definitely_exclude/a.pyi"),
1568 this_abs = THIS_DIR.resolve()
1570 black.gen_python_files(
1573 re.compile(black.DEFAULT_INCLUDES),
1580 self.assertEqual(sorted(expected), sorted(sources))
1582 def test_invalid_include_exclude(self) -> None:
1583 for option in ["--include", "--exclude"]:
1584 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1586 def test_preserves_line_endings(self) -> None:
1587 with TemporaryDirectory() as workspace:
1588 test_file = Path(workspace) / "test.py"
1589 for nl in ["\n", "\r\n"]:
1590 contents = nl.join(["def f( ):", " pass"])
1591 test_file.write_bytes(contents.encode())
1592 ff(test_file, write_back=black.WriteBack.YES)
1593 updated_contents: bytes = test_file.read_bytes()
1594 self.assertIn(nl.encode(), updated_contents)
1596 self.assertNotIn(b"\r\n", updated_contents)
1598 def test_preserves_line_endings_via_stdin(self) -> None:
1599 for nl in ["\n", "\r\n"]:
1600 contents = nl.join(["def f( ):", " pass"])
1601 runner = BlackRunner()
1602 result = runner.invoke(
1603 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1605 self.assertEqual(result.exit_code, 0)
1606 output = runner.stdout_bytes
1607 self.assertIn(nl.encode("utf8"), output)
1609 self.assertNotIn(b"\r\n", output)
1611 def test_assert_equivalent_different_asts(self) -> None:
1612 with self.assertRaises(AssertionError):
1613 black.assert_equivalent("{}", "None")
1615 def test_symlink_out_of_root_directory(self) -> None:
1617 root = THIS_DIR.resolve()
1619 include = re.compile(black.DEFAULT_INCLUDES)
1620 exclude = re.compile(black.DEFAULT_EXCLUDES)
1621 report = black.Report()
1622 gitignore = PathSpec.from_lines("gitwildmatch", [])
1623 # `child` should behave like a symlink which resolved path is clearly
1624 # outside of the `root` directory.
1625 path.iterdir.return_value = [child]
1626 child.resolve.return_value = Path("/a/b/c")
1627 child.as_posix.return_value = "/a/b/c"
1628 child.is_symlink.return_value = True
1631 black.gen_python_files(
1632 path.iterdir(), root, include, exclude, None, report, gitignore
1635 except ValueError as ve:
1636 self.fail(f"`get_python_files_in_dir()` failed: {ve}")
1637 path.iterdir.assert_called_once()
1638 child.resolve.assert_called_once()
1639 child.is_symlink.assert_called_once()
1640 # `child` should behave like a strange file which resolved path is clearly
1641 # outside of the `root` directory.
1642 child.is_symlink.return_value = False
1643 with self.assertRaises(ValueError):
1645 black.gen_python_files(
1646 path.iterdir(), root, include, exclude, None, report, gitignore
1649 path.iterdir.assert_called()
1650 self.assertEqual(path.iterdir.call_count, 2)
1651 child.resolve.assert_called()
1652 self.assertEqual(child.resolve.call_count, 2)
1653 child.is_symlink.assert_called()
1654 self.assertEqual(child.is_symlink.call_count, 2)
1656 def test_shhh_click(self) -> None:
1658 from click import _unicodefun # type: ignore
1659 except ModuleNotFoundError:
1660 self.skipTest("Incompatible Click version")
1661 if not hasattr(_unicodefun, "_verify_python3_env"):
1662 self.skipTest("Incompatible Click version")
1663 # First, let's see if Click is crashing with a preferred ASCII charset.
1664 with patch("locale.getpreferredencoding") as gpe:
1665 gpe.return_value = "ASCII"
1666 with self.assertRaises(RuntimeError):
1667 _unicodefun._verify_python3_env()
1668 # Now, let's silence Click...
1670 # ...and confirm it's silent.
1671 with patch("locale.getpreferredencoding") as gpe:
1672 gpe.return_value = "ASCII"
1674 _unicodefun._verify_python3_env()
1675 except RuntimeError as re:
1676 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1678 def test_root_logger_not_used_directly(self) -> None:
1679 def fail(*args: Any, **kwargs: Any) -> None:
1680 self.fail("Record created with root logger")
1682 with patch.multiple(
1693 def test_invalid_config_return_code(self) -> None:
1694 tmp_file = Path(black.dump_to_file())
1696 tmp_config = Path(black.dump_to_file())
1698 args = ["--config", str(tmp_config), str(tmp_file)]
1699 self.invokeBlack(args, exit_code=2, ignore_config=False)
1703 def test_parse_pyproject_toml(self) -> None:
1704 test_toml_file = THIS_DIR / "test.toml"
1705 config = black.parse_pyproject_toml(str(test_toml_file))
1706 self.assertEqual(config["verbose"], 1)
1707 self.assertEqual(config["check"], "no")
1708 self.assertEqual(config["diff"], "y")
1709 self.assertEqual(config["color"], True)
1710 self.assertEqual(config["line_length"], 79)
1711 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1712 self.assertEqual(config["exclude"], r"\.pyi?$")
1713 self.assertEqual(config["include"], r"\.py?$")
1715 def test_read_pyproject_toml(self) -> None:
1716 test_toml_file = THIS_DIR / "test.toml"
1717 fake_ctx = FakeContext()
1718 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1719 config = fake_ctx.default_map
1720 self.assertEqual(config["verbose"], "1")
1721 self.assertEqual(config["check"], "no")
1722 self.assertEqual(config["diff"], "y")
1723 self.assertEqual(config["color"], "True")
1724 self.assertEqual(config["line_length"], "79")
1725 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1726 self.assertEqual(config["exclude"], r"\.pyi?$")
1727 self.assertEqual(config["include"], r"\.py?$")
1729 def test_find_project_root(self) -> None:
1730 with TemporaryDirectory() as workspace:
1731 root = Path(workspace)
1732 test_dir = root / "test"
1735 src_dir = root / "src"
1738 root_pyproject = root / "pyproject.toml"
1739 root_pyproject.touch()
1740 src_pyproject = src_dir / "pyproject.toml"
1741 src_pyproject.touch()
1742 src_python = src_dir / "foo.py"
1746 black.find_project_root((src_dir, test_dir)), root.resolve()
1748 self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1749 self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1751 def test_bpo_33660_workaround(self) -> None:
1752 if system() == "Windows":
1755 # https://bugs.python.org/issue33660
1757 old_cwd = Path.cwd()
1761 path = Path("workspace") / "project"
1762 report = black.Report(verbose=True)
1763 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1764 self.assertEqual(normalized_path, "workspace/project")
1766 os.chdir(str(old_cwd))
1769 with open(black.__file__, "r", encoding="utf-8") as _bf:
1770 black_source_lines = _bf.readlines()
1773 def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
1774 """Show function calls `from black/__init__.py` as they happen.
1776 Register this with `sys.settrace()` in a test you're debugging.
1781 stack = len(inspect.stack()) - 19
1783 filename = frame.f_code.co_filename
1784 lineno = frame.f_lineno
1785 func_sig_lineno = lineno - 1
1786 funcname = black_source_lines[func_sig_lineno].strip()
1787 while funcname.startswith("@"):
1788 func_sig_lineno += 1
1789 funcname = black_source_lines[func_sig_lineno].strip()
1790 if "black/__init__.py" in filename:
1791 print(f"{' ' * stack}{lineno}:{funcname}")
1795 if __name__ == "__main__":
1796 unittest.main(module="test_black")