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.
12 from concurrent.futures import ThreadPoolExecutor
13 from contextlib import contextmanager
14 from dataclasses import replace
15 from io import BytesIO
16 from pathlib import Path
17 from platform import system
18 from tempfile import TemporaryDirectory
30 from unittest.mock import MagicMock, patch
35 from click import unstyle
36 from click.testing import CliRunner
37 from pathspec import PathSpec
41 from black import Feature, TargetVersion
42 from black import re_compile_maybe_verbose as compile_pattern
43 from black.cache import get_cache_dir, get_cache_file
44 from black.debug import DebugVisitor
45 from black.output import color_diff, diff
46 from black.report import Report
48 # Import other test classes
49 from tests.util import (
65 THIS_FILE = Path(__file__)
66 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
67 DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
68 DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
72 # Match the time output in a diff, but nothing else
73 DIFF_TIME = re.compile(r"\t[\d\-:+\. ]+")
77 def cache_dir(exists: bool = True) -> Iterator[Path]:
78 with TemporaryDirectory() as workspace:
79 cache_dir = Path(workspace)
81 cache_dir = cache_dir / "new"
82 with patch("black.cache.CACHE_DIR", cache_dir):
87 def event_loop() -> Iterator[None]:
88 policy = asyncio.get_event_loop_policy()
89 loop = policy.new_event_loop()
90 asyncio.set_event_loop(loop)
98 class FakeContext(click.Context):
99 """A fake click Context for when calling functions that need it."""
101 def __init__(self) -> None:
102 self.default_map: Dict[str, Any] = {}
103 # Dummy root, since most of the tests don't care about it
104 self.obj: Dict[str, Any] = {"root": PROJECT_ROOT}
107 class FakeParameter(click.Parameter):
108 """A fake click Parameter for when calling functions that need it."""
110 def __init__(self) -> None:
114 class BlackRunner(CliRunner):
115 """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
117 def __init__(self) -> None:
118 super().__init__(mix_stderr=False)
122 args: List[str], exit_code: int = 0, ignore_config: bool = True
124 runner = BlackRunner()
126 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
127 result = runner.invoke(black.main, args, catch_exceptions=False)
128 assert result.stdout_bytes is not None
129 assert result.stderr_bytes is not None
131 f"Failed with args: {args}\n"
132 f"stdout: {result.stdout_bytes.decode()!r}\n"
133 f"stderr: {result.stderr_bytes.decode()!r}\n"
134 f"exception: {result.exception}"
136 assert result.exit_code == exit_code, msg
139 class BlackTestCase(BlackBaseTestCase):
140 invokeBlack = staticmethod(invokeBlack)
142 def test_empty_ff(self) -> None:
144 tmp_file = Path(black.dump_to_file())
146 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
147 with open(tmp_file, encoding="utf8") as f:
151 self.assertFormatEqual(expected, actual)
153 def test_experimental_string_processing_warns(self) -> None:
155 black.mode.Deprecated, black.Mode, experimental_string_processing=True
158 def test_piping(self) -> None:
159 source, expected = read_data("src/black/__init__", data=False)
160 result = BlackRunner().invoke(
162 ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
163 input=BytesIO(source.encode("utf8")),
165 self.assertEqual(result.exit_code, 0)
166 self.assertFormatEqual(expected, result.output)
167 if source != result.output:
168 black.assert_equivalent(source, result.output)
169 black.assert_stable(source, result.output, DEFAULT_MODE)
171 def test_piping_diff(self) -> None:
172 diff_header = re.compile(
173 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
176 source, _ = read_data("expression.py")
177 expected, _ = read_data("expression.diff")
178 config = THIS_DIR / "data" / "empty_pyproject.toml"
182 f"--line-length={black.DEFAULT_LINE_LENGTH}",
184 f"--config={config}",
186 result = BlackRunner().invoke(
187 black.main, args, input=BytesIO(source.encode("utf8"))
189 self.assertEqual(result.exit_code, 0)
190 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
191 actual = actual.rstrip() + "\n" # the diff output has a trailing space
192 self.assertEqual(expected, actual)
194 def test_piping_diff_with_color(self) -> None:
195 source, _ = read_data("expression.py")
196 config = THIS_DIR / "data" / "empty_pyproject.toml"
200 f"--line-length={black.DEFAULT_LINE_LENGTH}",
203 f"--config={config}",
205 result = BlackRunner().invoke(
206 black.main, args, input=BytesIO(source.encode("utf8"))
208 actual = result.output
209 # Again, the contents are checked in a different test, so only look for colors.
210 self.assertIn("\033[1m", actual)
211 self.assertIn("\033[36m", actual)
212 self.assertIn("\033[32m", actual)
213 self.assertIn("\033[31m", actual)
214 self.assertIn("\033[0m", actual)
216 @patch("black.dump_to_file", dump_to_stderr)
217 def _test_wip(self) -> None:
218 source, expected = read_data("wip")
219 sys.settrace(tracefunc)
222 experimental_string_processing=False,
223 target_versions={black.TargetVersion.PY38},
225 actual = fs(source, mode=mode)
227 self.assertFormatEqual(expected, actual)
228 black.assert_equivalent(source, actual)
229 black.assert_stable(source, actual, black.FileMode())
231 def test_pep_572_version_detection(self) -> None:
232 source, _ = read_data("pep_572")
233 root = black.lib2to3_parse(source)
234 features = black.get_features_used(root)
235 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
236 versions = black.detect_target_versions(root)
237 self.assertIn(black.TargetVersion.PY38, versions)
239 def test_expression_ff(self) -> None:
240 source, expected = read_data("expression")
241 tmp_file = Path(black.dump_to_file(source))
243 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
244 with open(tmp_file, encoding="utf8") as f:
248 self.assertFormatEqual(expected, actual)
249 with patch("black.dump_to_file", dump_to_stderr):
250 black.assert_equivalent(source, actual)
251 black.assert_stable(source, actual, DEFAULT_MODE)
253 def test_expression_diff(self) -> None:
254 source, _ = read_data("expression.py")
255 config = THIS_DIR / "data" / "empty_pyproject.toml"
256 expected, _ = read_data("expression.diff")
257 tmp_file = Path(black.dump_to_file(source))
258 diff_header = re.compile(
259 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
260 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
263 result = BlackRunner().invoke(
264 black.main, ["--diff", str(tmp_file), f"--config={config}"]
266 self.assertEqual(result.exit_code, 0)
269 actual = result.output
270 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
271 if expected != actual:
272 dump = black.dump_to_file(actual)
274 "Expected diff isn't equal to the actual. If you made changes to"
275 " expression.py and this is an anticipated difference, overwrite"
276 f" tests/data/expression.diff with {dump}"
278 self.assertEqual(expected, actual, msg)
280 def test_expression_diff_with_color(self) -> None:
281 source, _ = read_data("expression.py")
282 config = THIS_DIR / "data" / "empty_pyproject.toml"
283 expected, _ = read_data("expression.diff")
284 tmp_file = Path(black.dump_to_file(source))
286 result = BlackRunner().invoke(
287 black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
291 actual = result.output
292 # We check the contents of the diff in `test_expression_diff`. All
293 # we need to check here is that color codes exist in the result.
294 self.assertIn("\033[1m", actual)
295 self.assertIn("\033[36m", actual)
296 self.assertIn("\033[32m", actual)
297 self.assertIn("\033[31m", actual)
298 self.assertIn("\033[0m", actual)
300 def test_detect_pos_only_arguments(self) -> None:
301 source, _ = read_data("pep_570")
302 root = black.lib2to3_parse(source)
303 features = black.get_features_used(root)
304 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
305 versions = black.detect_target_versions(root)
306 self.assertIn(black.TargetVersion.PY38, versions)
308 @patch("black.dump_to_file", dump_to_stderr)
309 def test_string_quotes(self) -> None:
310 source, expected = read_data("string_quotes")
311 mode = black.Mode(preview=True)
312 assert_format(source, expected, mode)
313 mode = replace(mode, string_normalization=False)
314 not_normalized = fs(source, mode=mode)
315 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
316 black.assert_equivalent(source, not_normalized)
317 black.assert_stable(source, not_normalized, mode=mode)
319 def test_skip_magic_trailing_comma(self) -> None:
320 source, _ = read_data("expression.py")
321 expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
322 tmp_file = Path(black.dump_to_file(source))
323 diff_header = re.compile(
324 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
325 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
328 result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
329 self.assertEqual(result.exit_code, 0)
332 actual = result.output
333 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
334 actual = actual.rstrip() + "\n" # the diff output has a trailing space
335 if expected != actual:
336 dump = black.dump_to_file(actual)
338 "Expected diff isn't equal to the actual. If you made changes to"
339 " expression.py and this is an anticipated difference, overwrite"
340 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
342 self.assertEqual(expected, actual, msg)
344 @patch("black.dump_to_file", dump_to_stderr)
345 def test_async_as_identifier(self) -> None:
346 source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
347 source, expected = read_data("async_as_identifier")
349 self.assertFormatEqual(expected, actual)
350 major, minor = sys.version_info[:2]
351 if major < 3 or (major <= 3 and minor < 7):
352 black.assert_equivalent(source, actual)
353 black.assert_stable(source, actual, DEFAULT_MODE)
354 # ensure black can parse this when the target is 3.6
355 self.invokeBlack([str(source_path), "--target-version", "py36"])
356 # but not on 3.7, because async/await is no longer an identifier
357 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
359 @patch("black.dump_to_file", dump_to_stderr)
360 def test_python37(self) -> None:
361 source_path = (THIS_DIR / "data" / "python37.py").resolve()
362 source, expected = read_data("python37")
364 self.assertFormatEqual(expected, actual)
365 major, minor = sys.version_info[:2]
366 if major > 3 or (major == 3 and minor >= 7):
367 black.assert_equivalent(source, actual)
368 black.assert_stable(source, actual, DEFAULT_MODE)
369 # ensure black can parse this when the target is 3.7
370 self.invokeBlack([str(source_path), "--target-version", "py37"])
371 # but not on 3.6, because we use async as a reserved keyword
372 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
374 def test_tab_comment_indentation(self) -> None:
375 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
376 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
377 self.assertFormatEqual(contents_spc, fs(contents_spc))
378 self.assertFormatEqual(contents_spc, fs(contents_tab))
380 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
381 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
382 self.assertFormatEqual(contents_spc, fs(contents_spc))
383 self.assertFormatEqual(contents_spc, fs(contents_tab))
385 # mixed tabs and spaces (valid Python 2 code)
386 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
387 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
388 self.assertFormatEqual(contents_spc, fs(contents_spc))
389 self.assertFormatEqual(contents_spc, fs(contents_tab))
391 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
392 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
393 self.assertFormatEqual(contents_spc, fs(contents_spc))
394 self.assertFormatEqual(contents_spc, fs(contents_tab))
396 def test_report_verbose(self) -> None:
397 report = Report(verbose=True)
401 def out(msg: str, **kwargs: Any) -> None:
402 out_lines.append(msg)
404 def err(msg: str, **kwargs: Any) -> None:
405 err_lines.append(msg)
407 with patch("black.output._out", out), patch("black.output._err", err):
408 report.done(Path("f1"), black.Changed.NO)
409 self.assertEqual(len(out_lines), 1)
410 self.assertEqual(len(err_lines), 0)
411 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
412 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
413 self.assertEqual(report.return_code, 0)
414 report.done(Path("f2"), black.Changed.YES)
415 self.assertEqual(len(out_lines), 2)
416 self.assertEqual(len(err_lines), 0)
417 self.assertEqual(out_lines[-1], "reformatted f2")
419 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
421 report.done(Path("f3"), black.Changed.CACHED)
422 self.assertEqual(len(out_lines), 3)
423 self.assertEqual(len(err_lines), 0)
425 out_lines[-1], "f3 wasn't modified on disk since last run."
428 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
430 self.assertEqual(report.return_code, 0)
432 self.assertEqual(report.return_code, 1)
434 report.failed(Path("e1"), "boom")
435 self.assertEqual(len(out_lines), 3)
436 self.assertEqual(len(err_lines), 1)
437 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
439 unstyle(str(report)),
440 "1 file reformatted, 2 files left unchanged, 1 file failed to"
443 self.assertEqual(report.return_code, 123)
444 report.done(Path("f3"), black.Changed.YES)
445 self.assertEqual(len(out_lines), 4)
446 self.assertEqual(len(err_lines), 1)
447 self.assertEqual(out_lines[-1], "reformatted f3")
449 unstyle(str(report)),
450 "2 files reformatted, 2 files left unchanged, 1 file failed to"
453 self.assertEqual(report.return_code, 123)
454 report.failed(Path("e2"), "boom")
455 self.assertEqual(len(out_lines), 4)
456 self.assertEqual(len(err_lines), 2)
457 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
459 unstyle(str(report)),
460 "2 files reformatted, 2 files left unchanged, 2 files failed to"
463 self.assertEqual(report.return_code, 123)
464 report.path_ignored(Path("wat"), "no match")
465 self.assertEqual(len(out_lines), 5)
466 self.assertEqual(len(err_lines), 2)
467 self.assertEqual(out_lines[-1], "wat ignored: no match")
469 unstyle(str(report)),
470 "2 files reformatted, 2 files left unchanged, 2 files failed to"
473 self.assertEqual(report.return_code, 123)
474 report.done(Path("f4"), black.Changed.NO)
475 self.assertEqual(len(out_lines), 6)
476 self.assertEqual(len(err_lines), 2)
477 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
479 unstyle(str(report)),
480 "2 files reformatted, 3 files left unchanged, 2 files failed to"
483 self.assertEqual(report.return_code, 123)
486 unstyle(str(report)),
487 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
488 " would fail to reformat.",
493 unstyle(str(report)),
494 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
495 " would fail to reformat.",
498 def test_report_quiet(self) -> None:
499 report = Report(quiet=True)
503 def out(msg: str, **kwargs: Any) -> None:
504 out_lines.append(msg)
506 def err(msg: str, **kwargs: Any) -> None:
507 err_lines.append(msg)
509 with patch("black.output._out", out), patch("black.output._err", err):
510 report.done(Path("f1"), black.Changed.NO)
511 self.assertEqual(len(out_lines), 0)
512 self.assertEqual(len(err_lines), 0)
513 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
514 self.assertEqual(report.return_code, 0)
515 report.done(Path("f2"), black.Changed.YES)
516 self.assertEqual(len(out_lines), 0)
517 self.assertEqual(len(err_lines), 0)
519 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
521 report.done(Path("f3"), black.Changed.CACHED)
522 self.assertEqual(len(out_lines), 0)
523 self.assertEqual(len(err_lines), 0)
525 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
527 self.assertEqual(report.return_code, 0)
529 self.assertEqual(report.return_code, 1)
531 report.failed(Path("e1"), "boom")
532 self.assertEqual(len(out_lines), 0)
533 self.assertEqual(len(err_lines), 1)
534 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
536 unstyle(str(report)),
537 "1 file reformatted, 2 files left unchanged, 1 file failed to"
540 self.assertEqual(report.return_code, 123)
541 report.done(Path("f3"), black.Changed.YES)
542 self.assertEqual(len(out_lines), 0)
543 self.assertEqual(len(err_lines), 1)
545 unstyle(str(report)),
546 "2 files reformatted, 2 files left unchanged, 1 file failed to"
549 self.assertEqual(report.return_code, 123)
550 report.failed(Path("e2"), "boom")
551 self.assertEqual(len(out_lines), 0)
552 self.assertEqual(len(err_lines), 2)
553 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
555 unstyle(str(report)),
556 "2 files reformatted, 2 files left unchanged, 2 files failed to"
559 self.assertEqual(report.return_code, 123)
560 report.path_ignored(Path("wat"), "no match")
561 self.assertEqual(len(out_lines), 0)
562 self.assertEqual(len(err_lines), 2)
564 unstyle(str(report)),
565 "2 files reformatted, 2 files left unchanged, 2 files failed to"
568 self.assertEqual(report.return_code, 123)
569 report.done(Path("f4"), black.Changed.NO)
570 self.assertEqual(len(out_lines), 0)
571 self.assertEqual(len(err_lines), 2)
573 unstyle(str(report)),
574 "2 files reformatted, 3 files left unchanged, 2 files failed to"
577 self.assertEqual(report.return_code, 123)
580 unstyle(str(report)),
581 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
582 " would fail to reformat.",
587 unstyle(str(report)),
588 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
589 " would fail to reformat.",
592 def test_report_normal(self) -> None:
593 report = black.Report()
597 def out(msg: str, **kwargs: Any) -> None:
598 out_lines.append(msg)
600 def err(msg: str, **kwargs: Any) -> None:
601 err_lines.append(msg)
603 with patch("black.output._out", out), patch("black.output._err", err):
604 report.done(Path("f1"), black.Changed.NO)
605 self.assertEqual(len(out_lines), 0)
606 self.assertEqual(len(err_lines), 0)
607 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
608 self.assertEqual(report.return_code, 0)
609 report.done(Path("f2"), black.Changed.YES)
610 self.assertEqual(len(out_lines), 1)
611 self.assertEqual(len(err_lines), 0)
612 self.assertEqual(out_lines[-1], "reformatted f2")
614 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
616 report.done(Path("f3"), black.Changed.CACHED)
617 self.assertEqual(len(out_lines), 1)
618 self.assertEqual(len(err_lines), 0)
619 self.assertEqual(out_lines[-1], "reformatted f2")
621 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
623 self.assertEqual(report.return_code, 0)
625 self.assertEqual(report.return_code, 1)
627 report.failed(Path("e1"), "boom")
628 self.assertEqual(len(out_lines), 1)
629 self.assertEqual(len(err_lines), 1)
630 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
632 unstyle(str(report)),
633 "1 file reformatted, 2 files left unchanged, 1 file failed to"
636 self.assertEqual(report.return_code, 123)
637 report.done(Path("f3"), black.Changed.YES)
638 self.assertEqual(len(out_lines), 2)
639 self.assertEqual(len(err_lines), 1)
640 self.assertEqual(out_lines[-1], "reformatted f3")
642 unstyle(str(report)),
643 "2 files reformatted, 2 files left unchanged, 1 file failed to"
646 self.assertEqual(report.return_code, 123)
647 report.failed(Path("e2"), "boom")
648 self.assertEqual(len(out_lines), 2)
649 self.assertEqual(len(err_lines), 2)
650 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
652 unstyle(str(report)),
653 "2 files reformatted, 2 files left unchanged, 2 files failed to"
656 self.assertEqual(report.return_code, 123)
657 report.path_ignored(Path("wat"), "no match")
658 self.assertEqual(len(out_lines), 2)
659 self.assertEqual(len(err_lines), 2)
661 unstyle(str(report)),
662 "2 files reformatted, 2 files left unchanged, 2 files failed to"
665 self.assertEqual(report.return_code, 123)
666 report.done(Path("f4"), black.Changed.NO)
667 self.assertEqual(len(out_lines), 2)
668 self.assertEqual(len(err_lines), 2)
670 unstyle(str(report)),
671 "2 files reformatted, 3 files left unchanged, 2 files failed to"
674 self.assertEqual(report.return_code, 123)
677 unstyle(str(report)),
678 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
679 " would fail to reformat.",
684 unstyle(str(report)),
685 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
686 " would fail to reformat.",
689 def test_lib2to3_parse(self) -> None:
690 with self.assertRaises(black.InvalidInput):
691 black.lib2to3_parse("invalid syntax")
694 black.lib2to3_parse(straddling)
695 black.lib2to3_parse(straddling, {TargetVersion.PY36})
698 with self.assertRaises(black.InvalidInput):
699 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
701 py3_only = "exec(x, end=y)"
702 black.lib2to3_parse(py3_only)
703 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
705 def test_get_features_used_decorator(self) -> None:
706 # Test the feature detection of new decorator syntax
707 # since this makes some test cases of test_get_features_used()
708 # fails if it fails, this is tested first so that a useful case
710 simples, relaxed = read_data("decorators")
711 # skip explanation comments at the top of the file
712 for simple_test in simples.split("##")[1:]:
713 node = black.lib2to3_parse(simple_test)
714 decorator = str(node.children[0].children[0]).strip()
716 Feature.RELAXED_DECORATORS,
717 black.get_features_used(node),
719 f"decorator '{decorator}' follows python<=3.8 syntax"
720 "but is detected as 3.9+"
721 # f"The full node is\n{node!r}"
724 # skip the '# output' comment at the top of the output part
725 for relaxed_test in relaxed.split("##")[1:]:
726 node = black.lib2to3_parse(relaxed_test)
727 decorator = str(node.children[0].children[0]).strip()
729 Feature.RELAXED_DECORATORS,
730 black.get_features_used(node),
732 f"decorator '{decorator}' uses python3.9+ syntax"
733 "but is detected as python<=3.8"
734 # f"The full node is\n{node!r}"
738 def test_get_features_used(self) -> None:
739 node = black.lib2to3_parse("def f(*, arg): ...\n")
740 self.assertEqual(black.get_features_used(node), set())
741 node = black.lib2to3_parse("def f(*, arg,): ...\n")
742 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
743 node = black.lib2to3_parse("f(*arg,)\n")
745 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
747 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
748 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
749 node = black.lib2to3_parse("123_456\n")
750 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
751 node = black.lib2to3_parse("123456\n")
752 self.assertEqual(black.get_features_used(node), set())
753 source, expected = read_data("function")
754 node = black.lib2to3_parse(source)
755 expected_features = {
756 Feature.TRAILING_COMMA_IN_CALL,
757 Feature.TRAILING_COMMA_IN_DEF,
760 self.assertEqual(black.get_features_used(node), expected_features)
761 node = black.lib2to3_parse(expected)
762 self.assertEqual(black.get_features_used(node), expected_features)
763 source, expected = read_data("expression")
764 node = black.lib2to3_parse(source)
765 self.assertEqual(black.get_features_used(node), set())
766 node = black.lib2to3_parse(expected)
767 self.assertEqual(black.get_features_used(node), set())
768 node = black.lib2to3_parse("lambda a, /, b: ...")
769 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
770 node = black.lib2to3_parse("def fn(a, /, b): ...")
771 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
772 node = black.lib2to3_parse("def fn(): yield a, b")
773 self.assertEqual(black.get_features_used(node), set())
774 node = black.lib2to3_parse("def fn(): return a, b")
775 self.assertEqual(black.get_features_used(node), set())
776 node = black.lib2to3_parse("def fn(): yield *b, c")
777 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
778 node = black.lib2to3_parse("def fn(): return a, *b, c")
779 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
780 node = black.lib2to3_parse("x = a, *b, c")
781 self.assertEqual(black.get_features_used(node), set())
782 node = black.lib2to3_parse("x: Any = regular")
783 self.assertEqual(black.get_features_used(node), set())
784 node = black.lib2to3_parse("x: Any = (regular, regular)")
785 self.assertEqual(black.get_features_used(node), set())
786 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
787 self.assertEqual(black.get_features_used(node), set())
788 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
790 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
793 def test_get_features_used_for_future_flags(self) -> None:
794 for src, features in [
795 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
797 "from __future__ import (other, annotations)",
798 {Feature.FUTURE_ANNOTATIONS},
800 ("a = 1 + 2\nfrom something import annotations", set()),
801 ("from __future__ import x, y", set()),
803 with self.subTest(src=src, features=features):
804 node = black.lib2to3_parse(src)
805 future_imports = black.get_future_imports(node)
807 black.get_features_used(node, future_imports=future_imports),
811 def test_get_future_imports(self) -> None:
812 node = black.lib2to3_parse("\n")
813 self.assertEqual(set(), black.get_future_imports(node))
814 node = black.lib2to3_parse("from __future__ import black\n")
815 self.assertEqual({"black"}, black.get_future_imports(node))
816 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
817 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
818 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
819 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
820 node = black.lib2to3_parse(
821 "from __future__ import multiple\nfrom __future__ import imports\n"
823 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
824 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
825 self.assertEqual({"black"}, black.get_future_imports(node))
826 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
827 self.assertEqual({"black"}, black.get_future_imports(node))
828 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
829 self.assertEqual(set(), black.get_future_imports(node))
830 node = black.lib2to3_parse("from some.module import black\n")
831 self.assertEqual(set(), black.get_future_imports(node))
832 node = black.lib2to3_parse(
833 "from __future__ import unicode_literals as _unicode_literals"
835 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
836 node = black.lib2to3_parse(
837 "from __future__ import unicode_literals as _lol, print"
839 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
841 @pytest.mark.incompatible_with_mypyc
842 def test_debug_visitor(self) -> None:
843 source, _ = read_data("debug_visitor.py")
844 expected, _ = read_data("debug_visitor.out")
848 def out(msg: str, **kwargs: Any) -> None:
849 out_lines.append(msg)
851 def err(msg: str, **kwargs: Any) -> None:
852 err_lines.append(msg)
854 with patch("black.debug.out", out):
855 DebugVisitor.show(source)
856 actual = "\n".join(out_lines) + "\n"
858 if expected != actual:
859 log_name = black.dump_to_file(*out_lines)
863 f"AST print out is different. Actual version dumped to {log_name}",
866 def test_format_file_contents(self) -> None:
869 with self.assertRaises(black.NothingChanged):
870 black.format_file_contents(empty, mode=mode, fast=False)
872 with self.assertRaises(black.NothingChanged):
873 black.format_file_contents(just_nl, mode=mode, fast=False)
874 same = "j = [1, 2, 3]\n"
875 with self.assertRaises(black.NothingChanged):
876 black.format_file_contents(same, mode=mode, fast=False)
877 different = "j = [1,2,3]"
879 actual = black.format_file_contents(different, mode=mode, fast=False)
880 self.assertEqual(expected, actual)
881 invalid = "return if you can"
882 with self.assertRaises(black.InvalidInput) as e:
883 black.format_file_contents(invalid, mode=mode, fast=False)
884 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
886 def test_endmarker(self) -> None:
887 n = black.lib2to3_parse("\n")
888 self.assertEqual(n.type, black.syms.file_input)
889 self.assertEqual(len(n.children), 1)
890 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
892 @pytest.mark.incompatible_with_mypyc
893 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
894 def test_assertFormatEqual(self) -> None:
898 def out(msg: str, **kwargs: Any) -> None:
899 out_lines.append(msg)
901 def err(msg: str, **kwargs: Any) -> None:
902 err_lines.append(msg)
904 with patch("black.output._out", out), patch("black.output._err", err):
905 with self.assertRaises(AssertionError):
906 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
908 out_str = "".join(out_lines)
909 self.assertIn("Expected tree:", out_str)
910 self.assertIn("Actual tree:", out_str)
911 self.assertEqual("".join(err_lines), "")
914 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
915 def test_works_in_mono_process_only_environment(self) -> None:
916 with cache_dir() as workspace:
918 (workspace / "one.py").resolve(),
919 (workspace / "two.py").resolve(),
921 f.write_text('print("hello")\n')
922 self.invokeBlack([str(workspace)])
925 def test_check_diff_use_together(self) -> None:
927 # Files which will be reformatted.
928 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
929 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
930 # Files which will not be reformatted.
931 src2 = (THIS_DIR / "data" / "composition.py").resolve()
932 self.invokeBlack([str(src2), "--diff", "--check"])
933 # Multi file command.
934 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
936 def test_no_src_fails(self) -> None:
938 self.invokeBlack([], exit_code=1)
940 def test_src_and_code_fails(self) -> None:
942 self.invokeBlack([".", "-c", "0"], exit_code=1)
944 def test_broken_symlink(self) -> None:
945 with cache_dir() as workspace:
946 symlink = workspace / "broken_link.py"
948 symlink.symlink_to("nonexistent.py")
949 except (OSError, NotImplementedError) as e:
950 self.skipTest(f"Can't create symlinks: {e}")
951 self.invokeBlack([str(workspace.resolve())])
953 def test_single_file_force_pyi(self) -> None:
954 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
955 contents, expected = read_data("force_pyi")
956 with cache_dir() as workspace:
957 path = (workspace / "file.py").resolve()
958 with open(path, "w") as fh:
960 self.invokeBlack([str(path), "--pyi"])
961 with open(path, "r") as fh:
963 # verify cache with --pyi is separate
964 pyi_cache = black.read_cache(pyi_mode)
965 self.assertIn(str(path), pyi_cache)
966 normal_cache = black.read_cache(DEFAULT_MODE)
967 self.assertNotIn(str(path), normal_cache)
968 self.assertFormatEqual(expected, actual)
969 black.assert_equivalent(contents, actual)
970 black.assert_stable(contents, actual, pyi_mode)
973 def test_multi_file_force_pyi(self) -> None:
974 reg_mode = DEFAULT_MODE
975 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
976 contents, expected = read_data("force_pyi")
977 with cache_dir() as workspace:
979 (workspace / "file1.py").resolve(),
980 (workspace / "file2.py").resolve(),
983 with open(path, "w") as fh:
985 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
987 with open(path, "r") as fh:
989 self.assertEqual(actual, expected)
990 # verify cache with --pyi is separate
991 pyi_cache = black.read_cache(pyi_mode)
992 normal_cache = black.read_cache(reg_mode)
994 self.assertIn(str(path), pyi_cache)
995 self.assertNotIn(str(path), normal_cache)
997 def test_pipe_force_pyi(self) -> None:
998 source, expected = read_data("force_pyi")
999 result = CliRunner().invoke(
1000 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1002 self.assertEqual(result.exit_code, 0)
1003 actual = result.output
1004 self.assertFormatEqual(actual, expected)
1006 def test_single_file_force_py36(self) -> None:
1007 reg_mode = DEFAULT_MODE
1008 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1009 source, expected = read_data("force_py36")
1010 with cache_dir() as workspace:
1011 path = (workspace / "file.py").resolve()
1012 with open(path, "w") as fh:
1014 self.invokeBlack([str(path), *PY36_ARGS])
1015 with open(path, "r") as fh:
1017 # verify cache with --target-version is separate
1018 py36_cache = black.read_cache(py36_mode)
1019 self.assertIn(str(path), py36_cache)
1020 normal_cache = black.read_cache(reg_mode)
1021 self.assertNotIn(str(path), normal_cache)
1022 self.assertEqual(actual, expected)
1025 def test_multi_file_force_py36(self) -> None:
1026 reg_mode = DEFAULT_MODE
1027 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1028 source, expected = read_data("force_py36")
1029 with cache_dir() as workspace:
1031 (workspace / "file1.py").resolve(),
1032 (workspace / "file2.py").resolve(),
1035 with open(path, "w") as fh:
1037 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1039 with open(path, "r") as fh:
1041 self.assertEqual(actual, expected)
1042 # verify cache with --target-version is separate
1043 pyi_cache = black.read_cache(py36_mode)
1044 normal_cache = black.read_cache(reg_mode)
1046 self.assertIn(str(path), pyi_cache)
1047 self.assertNotIn(str(path), normal_cache)
1049 def test_pipe_force_py36(self) -> None:
1050 source, expected = read_data("force_py36")
1051 result = CliRunner().invoke(
1053 ["-", "-q", "--target-version=py36"],
1054 input=BytesIO(source.encode("utf8")),
1056 self.assertEqual(result.exit_code, 0)
1057 actual = result.output
1058 self.assertFormatEqual(actual, expected)
1060 @pytest.mark.incompatible_with_mypyc
1061 def test_reformat_one_with_stdin(self) -> None:
1063 "black.format_stdin_to_stdout",
1064 return_value=lambda *args, **kwargs: black.Changed.YES,
1066 report = MagicMock()
1071 write_back=black.WriteBack.YES,
1075 fsts.assert_called_once()
1076 report.done.assert_called_with(path, black.Changed.YES)
1078 @pytest.mark.incompatible_with_mypyc
1079 def test_reformat_one_with_stdin_filename(self) -> None:
1081 "black.format_stdin_to_stdout",
1082 return_value=lambda *args, **kwargs: black.Changed.YES,
1084 report = MagicMock()
1086 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1091 write_back=black.WriteBack.YES,
1095 fsts.assert_called_once_with(
1096 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1098 # __BLACK_STDIN_FILENAME__ should have been stripped
1099 report.done.assert_called_with(expected, black.Changed.YES)
1101 @pytest.mark.incompatible_with_mypyc
1102 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1104 "black.format_stdin_to_stdout",
1105 return_value=lambda *args, **kwargs: black.Changed.YES,
1107 report = MagicMock()
1109 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1114 write_back=black.WriteBack.YES,
1118 fsts.assert_called_once_with(
1120 write_back=black.WriteBack.YES,
1121 mode=replace(DEFAULT_MODE, is_pyi=True),
1123 # __BLACK_STDIN_FILENAME__ should have been stripped
1124 report.done.assert_called_with(expected, black.Changed.YES)
1126 @pytest.mark.incompatible_with_mypyc
1127 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1129 "black.format_stdin_to_stdout",
1130 return_value=lambda *args, **kwargs: black.Changed.YES,
1132 report = MagicMock()
1134 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1139 write_back=black.WriteBack.YES,
1143 fsts.assert_called_once_with(
1145 write_back=black.WriteBack.YES,
1146 mode=replace(DEFAULT_MODE, is_ipynb=True),
1148 # __BLACK_STDIN_FILENAME__ should have been stripped
1149 report.done.assert_called_with(expected, black.Changed.YES)
1151 @pytest.mark.incompatible_with_mypyc
1152 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1154 "black.format_stdin_to_stdout",
1155 return_value=lambda *args, **kwargs: black.Changed.YES,
1157 report = MagicMock()
1158 # Even with an existing file, since we are forcing stdin, black
1159 # should output to stdout and not modify the file inplace
1160 p = Path(str(THIS_DIR / "data/collections.py"))
1161 # Make sure is_file actually returns True
1162 self.assertTrue(p.is_file())
1163 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1168 write_back=black.WriteBack.YES,
1172 fsts.assert_called_once()
1173 # __BLACK_STDIN_FILENAME__ should have been stripped
1174 report.done.assert_called_with(expected, black.Changed.YES)
1176 def test_reformat_one_with_stdin_empty(self) -> None:
1177 output = io.StringIO()
1178 with patch("io.TextIOWrapper", lambda *args, **kwargs: output):
1180 black.format_stdin_to_stdout(
1183 write_back=black.WriteBack.YES,
1186 except io.UnsupportedOperation:
1187 pass # StringIO does not support detach
1188 assert output.getvalue() == ""
1190 def test_invalid_cli_regex(self) -> None:
1191 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1192 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1194 def test_required_version_matches_version(self) -> None:
1196 ["--required-version", black.__version__, "-c", "0"],
1201 def test_required_version_does_not_match_version(self) -> None:
1202 result = BlackRunner().invoke(
1204 ["--required-version", "20.99b", "-c", "0"],
1206 self.assertEqual(result.exit_code, 1)
1207 self.assertIn("required version", result.stderr)
1209 def test_preserves_line_endings(self) -> None:
1210 with TemporaryDirectory() as workspace:
1211 test_file = Path(workspace) / "test.py"
1212 for nl in ["\n", "\r\n"]:
1213 contents = nl.join(["def f( ):", " pass"])
1214 test_file.write_bytes(contents.encode())
1215 ff(test_file, write_back=black.WriteBack.YES)
1216 updated_contents: bytes = test_file.read_bytes()
1217 self.assertIn(nl.encode(), updated_contents)
1219 self.assertNotIn(b"\r\n", updated_contents)
1221 def test_preserves_line_endings_via_stdin(self) -> None:
1222 for nl in ["\n", "\r\n"]:
1223 contents = nl.join(["def f( ):", " pass"])
1224 runner = BlackRunner()
1225 result = runner.invoke(
1226 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1228 self.assertEqual(result.exit_code, 0)
1229 output = result.stdout_bytes
1230 self.assertIn(nl.encode("utf8"), output)
1232 self.assertNotIn(b"\r\n", output)
1234 def test_assert_equivalent_different_asts(self) -> None:
1235 with self.assertRaises(AssertionError):
1236 black.assert_equivalent("{}", "None")
1238 def test_shhh_click(self) -> None:
1240 from click import _unicodefun
1241 except ModuleNotFoundError:
1242 self.skipTest("Incompatible Click version")
1243 if not hasattr(_unicodefun, "_verify_python3_env"):
1244 self.skipTest("Incompatible Click version")
1245 # First, let's see if Click is crashing with a preferred ASCII charset.
1246 with patch("locale.getpreferredencoding") as gpe:
1247 gpe.return_value = "ASCII"
1248 with self.assertRaises(RuntimeError):
1249 _unicodefun._verify_python3_env() # type: ignore
1250 # Now, let's silence Click...
1252 # ...and confirm it's silent.
1253 with patch("locale.getpreferredencoding") as gpe:
1254 gpe.return_value = "ASCII"
1256 _unicodefun._verify_python3_env() # type: ignore
1257 except RuntimeError as re:
1258 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1260 def test_root_logger_not_used_directly(self) -> None:
1261 def fail(*args: Any, **kwargs: Any) -> None:
1262 self.fail("Record created with root logger")
1264 with patch.multiple(
1273 ff(THIS_DIR / "util.py")
1275 def test_invalid_config_return_code(self) -> None:
1276 tmp_file = Path(black.dump_to_file())
1278 tmp_config = Path(black.dump_to_file())
1280 args = ["--config", str(tmp_config), str(tmp_file)]
1281 self.invokeBlack(args, exit_code=2, ignore_config=False)
1285 def test_parse_pyproject_toml(self) -> None:
1286 test_toml_file = THIS_DIR / "test.toml"
1287 config = black.parse_pyproject_toml(str(test_toml_file))
1288 self.assertEqual(config["verbose"], 1)
1289 self.assertEqual(config["check"], "no")
1290 self.assertEqual(config["diff"], "y")
1291 self.assertEqual(config["color"], True)
1292 self.assertEqual(config["line_length"], 79)
1293 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1294 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1295 self.assertEqual(config["exclude"], r"\.pyi?$")
1296 self.assertEqual(config["include"], r"\.py?$")
1298 def test_read_pyproject_toml(self) -> None:
1299 test_toml_file = THIS_DIR / "test.toml"
1300 fake_ctx = FakeContext()
1301 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1302 config = fake_ctx.default_map
1303 self.assertEqual(config["verbose"], "1")
1304 self.assertEqual(config["check"], "no")
1305 self.assertEqual(config["diff"], "y")
1306 self.assertEqual(config["color"], "True")
1307 self.assertEqual(config["line_length"], "79")
1308 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1309 self.assertEqual(config["exclude"], r"\.pyi?$")
1310 self.assertEqual(config["include"], r"\.py?$")
1312 @pytest.mark.incompatible_with_mypyc
1313 def test_find_project_root(self) -> None:
1314 with TemporaryDirectory() as workspace:
1315 root = Path(workspace)
1316 test_dir = root / "test"
1319 src_dir = root / "src"
1322 root_pyproject = root / "pyproject.toml"
1323 root_pyproject.touch()
1324 src_pyproject = src_dir / "pyproject.toml"
1325 src_pyproject.touch()
1326 src_python = src_dir / "foo.py"
1330 black.find_project_root((src_dir, test_dir)),
1331 (root.resolve(), "pyproject.toml"),
1334 black.find_project_root((src_dir,)),
1335 (src_dir.resolve(), "pyproject.toml"),
1338 black.find_project_root((src_python,)),
1339 (src_dir.resolve(), "pyproject.toml"),
1343 "black.files.find_user_pyproject_toml",
1344 black.files.find_user_pyproject_toml.__wrapped__,
1346 def test_find_user_pyproject_toml_linux(self) -> None:
1347 if system() == "Windows":
1350 # Test if XDG_CONFIG_HOME is checked
1351 with TemporaryDirectory() as workspace:
1352 tmp_user_config = Path(workspace) / "black"
1353 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1355 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1358 # Test fallback for XDG_CONFIG_HOME
1359 with patch.dict("os.environ"):
1360 os.environ.pop("XDG_CONFIG_HOME", None)
1361 fallback_user_config = Path("~/.config").expanduser() / "black"
1363 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1366 def test_find_user_pyproject_toml_windows(self) -> None:
1367 if system() != "Windows":
1370 user_config_path = Path.home() / ".black"
1372 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1375 def test_bpo_33660_workaround(self) -> None:
1376 if system() == "Windows":
1379 # https://bugs.python.org/issue33660
1381 with change_directory(root):
1382 path = Path("workspace") / "project"
1383 report = black.Report(verbose=True)
1384 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1385 self.assertEqual(normalized_path, "workspace/project")
1387 def test_newline_comment_interaction(self) -> None:
1388 source = "class A:\\\r\n# type: ignore\n pass\n"
1389 output = black.format_str(source, mode=DEFAULT_MODE)
1390 black.assert_stable(source, output, mode=DEFAULT_MODE)
1392 def test_bpo_2142_workaround(self) -> None:
1394 # https://bugs.python.org/issue2142
1396 source, _ = read_data("missing_final_newline.py")
1397 # read_data adds a trailing newline
1398 source = source.rstrip()
1399 expected, _ = read_data("missing_final_newline.diff")
1400 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1401 diff_header = re.compile(
1402 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1403 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1406 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1407 self.assertEqual(result.exit_code, 0)
1410 actual = result.output
1411 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1412 self.assertEqual(actual, expected)
1415 def compare_results(
1416 result: click.testing.Result, expected_value: str, expected_exit_code: int
1418 """Helper method to test the value and exit code of a click Result."""
1420 result.output == expected_value
1421 ), "The output did not match the expected value."
1422 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1424 def test_code_option(self) -> None:
1425 """Test the code option with no changes."""
1426 code = 'print("Hello world")\n'
1427 args = ["--code", code]
1428 result = CliRunner().invoke(black.main, args)
1430 self.compare_results(result, code, 0)
1432 def test_code_option_changed(self) -> None:
1433 """Test the code option when changes are required."""
1434 code = "print('hello world')"
1435 formatted = black.format_str(code, mode=DEFAULT_MODE)
1437 args = ["--code", code]
1438 result = CliRunner().invoke(black.main, args)
1440 self.compare_results(result, formatted, 0)
1442 def test_code_option_check(self) -> None:
1443 """Test the code option when check is passed."""
1444 args = ["--check", "--code", 'print("Hello world")\n']
1445 result = CliRunner().invoke(black.main, args)
1446 self.compare_results(result, "", 0)
1448 def test_code_option_check_changed(self) -> None:
1449 """Test the code option when changes are required, and check is passed."""
1450 args = ["--check", "--code", "print('hello world')"]
1451 result = CliRunner().invoke(black.main, args)
1452 self.compare_results(result, "", 1)
1454 def test_code_option_diff(self) -> None:
1455 """Test the code option when diff is passed."""
1456 code = "print('hello world')"
1457 formatted = black.format_str(code, mode=DEFAULT_MODE)
1458 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1460 args = ["--diff", "--code", code]
1461 result = CliRunner().invoke(black.main, args)
1463 # Remove time from diff
1464 output = DIFF_TIME.sub("", result.output)
1466 assert output == result_diff, "The output did not match the expected value."
1467 assert result.exit_code == 0, "The exit code is incorrect."
1469 def test_code_option_color_diff(self) -> None:
1470 """Test the code option when color and diff are passed."""
1471 code = "print('hello world')"
1472 formatted = black.format_str(code, mode=DEFAULT_MODE)
1474 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1475 result_diff = color_diff(result_diff)
1477 args = ["--diff", "--color", "--code", code]
1478 result = CliRunner().invoke(black.main, args)
1480 # Remove time from diff
1481 output = DIFF_TIME.sub("", result.output)
1483 assert output == result_diff, "The output did not match the expected value."
1484 assert result.exit_code == 0, "The exit code is incorrect."
1486 @pytest.mark.incompatible_with_mypyc
1487 def test_code_option_safe(self) -> None:
1488 """Test that the code option throws an error when the sanity checks fail."""
1489 # Patch black.assert_equivalent to ensure the sanity checks fail
1490 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1491 code = 'print("Hello world")'
1492 error_msg = f"{code}\nerror: cannot format <string>: \n"
1494 args = ["--safe", "--code", code]
1495 result = CliRunner().invoke(black.main, args)
1497 self.compare_results(result, error_msg, 123)
1499 def test_code_option_fast(self) -> None:
1500 """Test that the code option ignores errors when the sanity checks fail."""
1501 # Patch black.assert_equivalent to ensure the sanity checks fail
1502 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1503 code = 'print("Hello world")'
1504 formatted = black.format_str(code, mode=DEFAULT_MODE)
1506 args = ["--fast", "--code", code]
1507 result = CliRunner().invoke(black.main, args)
1509 self.compare_results(result, formatted, 0)
1511 @pytest.mark.incompatible_with_mypyc
1512 def test_code_option_config(self) -> None:
1514 Test that the code option finds the pyproject.toml in the current directory.
1516 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1517 args = ["--code", "print"]
1518 # This is the only directory known to contain a pyproject.toml
1519 with change_directory(PROJECT_ROOT):
1520 CliRunner().invoke(black.main, args)
1521 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1524 len(parse.mock_calls) >= 1
1525 ), "Expected config parse to be called with the current directory."
1527 _, call_args, _ = parse.mock_calls[0]
1529 call_args[0].lower() == str(pyproject_path).lower()
1530 ), "Incorrect config loaded."
1532 @pytest.mark.incompatible_with_mypyc
1533 def test_code_option_parent_config(self) -> None:
1535 Test that the code option finds the pyproject.toml in the parent directory.
1537 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1538 with change_directory(THIS_DIR):
1539 args = ["--code", "print"]
1540 CliRunner().invoke(black.main, args)
1542 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1544 len(parse.mock_calls) >= 1
1545 ), "Expected config parse to be called with the current directory."
1547 _, call_args, _ = parse.mock_calls[0]
1549 call_args[0].lower() == str(pyproject_path).lower()
1550 ), "Incorrect config loaded."
1552 def test_for_handled_unexpected_eof_error(self) -> None:
1554 Test that an unexpected EOF SyntaxError is nicely presented.
1556 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1557 black.lib2to3_parse("print(", {})
1559 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1561 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1562 with pytest.raises(AssertionError) as err:
1563 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1566 # Unfortunately the SyntaxError message has changed in newer versions so we
1567 # can't match it directly.
1568 err.match("invalid character")
1569 err.match(r"\(<unknown>, line 1\)")
1573 def test_get_cache_dir(
1576 monkeypatch: pytest.MonkeyPatch,
1578 # Create multiple cache directories
1579 workspace1 = tmp_path / "ws1"
1581 workspace2 = tmp_path / "ws2"
1584 # Force user_cache_dir to use the temporary directory for easier assertions
1585 patch_user_cache_dir = patch(
1586 target="black.cache.user_cache_dir",
1588 return_value=str(workspace1),
1591 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1592 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1593 with patch_user_cache_dir:
1594 assert get_cache_dir() == workspace1
1596 # If it is set, use the path provided in the env var.
1597 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1598 assert get_cache_dir() == workspace2
1600 def test_cache_broken_file(self) -> None:
1602 with cache_dir() as workspace:
1603 cache_file = get_cache_file(mode)
1604 cache_file.write_text("this is not a pickle")
1605 assert black.read_cache(mode) == {}
1606 src = (workspace / "test.py").resolve()
1607 src.write_text("print('hello')")
1608 invokeBlack([str(src)])
1609 cache = black.read_cache(mode)
1610 assert str(src) in cache
1612 def test_cache_single_file_already_cached(self) -> None:
1614 with cache_dir() as workspace:
1615 src = (workspace / "test.py").resolve()
1616 src.write_text("print('hello')")
1617 black.write_cache({}, [src], mode)
1618 invokeBlack([str(src)])
1619 assert src.read_text() == "print('hello')"
1622 def test_cache_multiple_files(self) -> None:
1624 with cache_dir() as workspace, patch(
1625 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1627 one = (workspace / "one.py").resolve()
1628 with one.open("w") as fobj:
1629 fobj.write("print('hello')")
1630 two = (workspace / "two.py").resolve()
1631 with two.open("w") as fobj:
1632 fobj.write("print('hello')")
1633 black.write_cache({}, [one], mode)
1634 invokeBlack([str(workspace)])
1635 with one.open("r") as fobj:
1636 assert fobj.read() == "print('hello')"
1637 with two.open("r") as fobj:
1638 assert fobj.read() == 'print("hello")\n'
1639 cache = black.read_cache(mode)
1640 assert str(one) in cache
1641 assert str(two) in cache
1643 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1644 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1646 with cache_dir() as workspace:
1647 src = (workspace / "test.py").resolve()
1648 with src.open("w") as fobj:
1649 fobj.write("print('hello')")
1650 with patch("black.read_cache") as read_cache, patch(
1653 cmd = [str(src), "--diff"]
1655 cmd.append("--color")
1657 cache_file = get_cache_file(mode)
1658 assert cache_file.exists() is False
1659 write_cache.assert_not_called()
1660 read_cache.assert_not_called()
1662 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1664 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1665 with cache_dir() as workspace:
1666 for tag in range(0, 4):
1667 src = (workspace / f"test{tag}.py").resolve()
1668 with src.open("w") as fobj:
1669 fobj.write("print('hello')")
1670 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1671 cmd = ["--diff", str(workspace)]
1673 cmd.append("--color")
1674 invokeBlack(cmd, exit_code=0)
1675 # this isn't quite doing what we want, but if it _isn't_
1676 # called then we cannot be using the lock it provides
1679 def test_no_cache_when_stdin(self) -> None:
1682 result = CliRunner().invoke(
1683 black.main, ["-"], input=BytesIO(b"print('hello')")
1685 assert not result.exit_code
1686 cache_file = get_cache_file(mode)
1687 assert not cache_file.exists()
1689 def test_read_cache_no_cachefile(self) -> None:
1692 assert black.read_cache(mode) == {}
1694 def test_write_cache_read_cache(self) -> None:
1696 with cache_dir() as workspace:
1697 src = (workspace / "test.py").resolve()
1699 black.write_cache({}, [src], mode)
1700 cache = black.read_cache(mode)
1701 assert str(src) in cache
1702 assert cache[str(src)] == black.get_cache_info(src)
1704 def test_filter_cached(self) -> None:
1705 with TemporaryDirectory() as workspace:
1706 path = Path(workspace)
1707 uncached = (path / "uncached").resolve()
1708 cached = (path / "cached").resolve()
1709 cached_but_changed = (path / "changed").resolve()
1712 cached_but_changed.touch()
1714 str(cached): black.get_cache_info(cached),
1715 str(cached_but_changed): (0.0, 0),
1717 todo, done = black.filter_cached(
1718 cache, {uncached, cached, cached_but_changed}
1720 assert todo == {uncached, cached_but_changed}
1721 assert done == {cached}
1723 def test_write_cache_creates_directory_if_needed(self) -> None:
1725 with cache_dir(exists=False) as workspace:
1726 assert not workspace.exists()
1727 black.write_cache({}, [], mode)
1728 assert workspace.exists()
1731 def test_failed_formatting_does_not_get_cached(self) -> None:
1733 with cache_dir() as workspace, patch(
1734 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1736 failing = (workspace / "failing.py").resolve()
1737 with failing.open("w") as fobj:
1738 fobj.write("not actually python")
1739 clean = (workspace / "clean.py").resolve()
1740 with clean.open("w") as fobj:
1741 fobj.write('print("hello")\n')
1742 invokeBlack([str(workspace)], exit_code=123)
1743 cache = black.read_cache(mode)
1744 assert str(failing) not in cache
1745 assert str(clean) in cache
1747 def test_write_cache_write_fail(self) -> None:
1749 with cache_dir(), patch.object(Path, "open") as mock:
1750 mock.side_effect = OSError
1751 black.write_cache({}, [], mode)
1753 def test_read_cache_line_lengths(self) -> None:
1755 short_mode = replace(DEFAULT_MODE, line_length=1)
1756 with cache_dir() as workspace:
1757 path = (workspace / "file.py").resolve()
1759 black.write_cache({}, [path], mode)
1760 one = black.read_cache(mode)
1761 assert str(path) in one
1762 two = black.read_cache(short_mode)
1763 assert str(path) not in two
1766 def assert_collected_sources(
1767 src: Sequence[Union[str, Path]],
1768 expected: Sequence[Union[str, Path]],
1770 ctx: Optional[FakeContext] = None,
1771 exclude: Optional[str] = None,
1772 include: Optional[str] = None,
1773 extend_exclude: Optional[str] = None,
1774 force_exclude: Optional[str] = None,
1775 stdin_filename: Optional[str] = None,
1777 gs_src = tuple(str(Path(s)) for s in src)
1778 gs_expected = [Path(s) for s in expected]
1779 gs_exclude = None if exclude is None else compile_pattern(exclude)
1780 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
1781 gs_extend_exclude = (
1782 None if extend_exclude is None else compile_pattern(extend_exclude)
1784 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
1785 collected = black.get_sources(
1786 ctx=ctx or FakeContext(),
1792 extend_exclude=gs_extend_exclude,
1793 force_exclude=gs_force_exclude,
1794 report=black.Report(),
1795 stdin_filename=stdin_filename,
1797 assert sorted(collected) == sorted(gs_expected)
1800 class TestFileCollection:
1801 def test_include_exclude(self) -> None:
1802 path = THIS_DIR / "data" / "include_exclude_tests"
1805 Path(path / "b/dont_exclude/a.py"),
1806 Path(path / "b/dont_exclude/a.pyi"),
1808 assert_collected_sources(
1812 exclude=r"/exclude/|/\.definitely_exclude/",
1815 def test_gitignore_used_as_default(self) -> None:
1816 base = Path(DATA_DIR / "include_exclude_tests")
1818 base / "b/.definitely_exclude/a.py",
1819 base / "b/.definitely_exclude/a.pyi",
1823 ctx.obj["root"] = base
1824 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
1826 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
1827 def test_exclude_for_issue_1572(self) -> None:
1828 # Exclude shouldn't touch files that were explicitly given to Black through the
1829 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1830 # https://github.com/psf/black/issues/1572
1831 path = DATA_DIR / "include_exclude_tests"
1832 src = [path / "b/exclude/a.py"]
1833 expected = [path / "b/exclude/a.py"]
1834 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
1836 def test_gitignore_exclude(self) -> None:
1837 path = THIS_DIR / "data" / "include_exclude_tests"
1838 include = re.compile(r"\.pyi?$")
1839 exclude = re.compile(r"")
1840 report = black.Report()
1841 gitignore = PathSpec.from_lines(
1842 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1844 sources: List[Path] = []
1846 Path(path / "b/dont_exclude/a.py"),
1847 Path(path / "b/dont_exclude/a.pyi"),
1849 this_abs = THIS_DIR.resolve()
1851 black.gen_python_files(
1864 assert sorted(expected) == sorted(sources)
1866 def test_nested_gitignore(self) -> None:
1867 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
1868 include = re.compile(r"\.pyi?$")
1869 exclude = re.compile(r"")
1870 root_gitignore = black.files.get_gitignore(path)
1871 report = black.Report()
1872 expected: List[Path] = [
1873 Path(path / "x.py"),
1874 Path(path / "root/b.py"),
1875 Path(path / "root/c.py"),
1876 Path(path / "root/child/c.py"),
1878 this_abs = THIS_DIR.resolve()
1880 black.gen_python_files(
1893 assert sorted(expected) == sorted(sources)
1895 def test_invalid_gitignore(self) -> None:
1896 path = THIS_DIR / "data" / "invalid_gitignore_tests"
1897 empty_config = path / "pyproject.toml"
1898 result = BlackRunner().invoke(
1899 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1901 assert result.exit_code == 1
1902 assert result.stderr_bytes is not None
1904 gitignore = path / ".gitignore"
1905 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1907 def test_invalid_nested_gitignore(self) -> None:
1908 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
1909 empty_config = path / "pyproject.toml"
1910 result = BlackRunner().invoke(
1911 black.main, ["--verbose", "--config", str(empty_config), str(path)]
1913 assert result.exit_code == 1
1914 assert result.stderr_bytes is not None
1916 gitignore = path / "a" / ".gitignore"
1917 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
1919 def test_empty_include(self) -> None:
1920 path = DATA_DIR / "include_exclude_tests"
1923 Path(path / "b/exclude/a.pie"),
1924 Path(path / "b/exclude/a.py"),
1925 Path(path / "b/exclude/a.pyi"),
1926 Path(path / "b/dont_exclude/a.pie"),
1927 Path(path / "b/dont_exclude/a.py"),
1928 Path(path / "b/dont_exclude/a.pyi"),
1929 Path(path / "b/.definitely_exclude/a.pie"),
1930 Path(path / "b/.definitely_exclude/a.py"),
1931 Path(path / "b/.definitely_exclude/a.pyi"),
1932 Path(path / ".gitignore"),
1933 Path(path / "pyproject.toml"),
1935 # Setting exclude explicitly to an empty string to block .gitignore usage.
1936 assert_collected_sources(src, expected, include="", exclude="")
1938 def test_extend_exclude(self) -> None:
1939 path = DATA_DIR / "include_exclude_tests"
1942 Path(path / "b/exclude/a.py"),
1943 Path(path / "b/dont_exclude/a.py"),
1945 assert_collected_sources(
1946 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
1949 @pytest.mark.incompatible_with_mypyc
1950 def test_symlink_out_of_root_directory(self) -> None:
1952 root = THIS_DIR.resolve()
1954 include = re.compile(black.DEFAULT_INCLUDES)
1955 exclude = re.compile(black.DEFAULT_EXCLUDES)
1956 report = black.Report()
1957 gitignore = PathSpec.from_lines("gitwildmatch", [])
1958 # `child` should behave like a symlink which resolved path is clearly
1959 # outside of the `root` directory.
1960 path.iterdir.return_value = [child]
1961 child.resolve.return_value = Path("/a/b/c")
1962 child.as_posix.return_value = "/a/b/c"
1963 child.is_symlink.return_value = True
1966 black.gen_python_files(
1979 except ValueError as ve:
1980 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
1981 path.iterdir.assert_called_once()
1982 child.resolve.assert_called_once()
1983 child.is_symlink.assert_called_once()
1984 # `child` should behave like a strange file which resolved path is clearly
1985 # outside of the `root` directory.
1986 child.is_symlink.return_value = False
1987 with pytest.raises(ValueError):
1989 black.gen_python_files(
2002 path.iterdir.assert_called()
2003 assert path.iterdir.call_count == 2
2004 child.resolve.assert_called()
2005 assert child.resolve.call_count == 2
2006 child.is_symlink.assert_called()
2007 assert child.is_symlink.call_count == 2
2009 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2010 def test_get_sources_with_stdin(self) -> None:
2013 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2015 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2016 def test_get_sources_with_stdin_filename(self) -> None:
2018 stdin_filename = str(THIS_DIR / "data/collections.py")
2019 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2020 assert_collected_sources(
2023 exclude=r"/exclude/a\.py",
2024 stdin_filename=stdin_filename,
2027 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2028 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2029 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2030 # file being passed directly. This is the same as
2031 # test_exclude_for_issue_1572
2032 path = DATA_DIR / "include_exclude_tests"
2034 stdin_filename = str(path / "b/exclude/a.py")
2035 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2036 assert_collected_sources(
2039 exclude=r"/exclude/|a\.py",
2040 stdin_filename=stdin_filename,
2043 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2044 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2045 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2046 # file being passed directly. This is the same as
2047 # test_exclude_for_issue_1572
2049 path = THIS_DIR / "data" / "include_exclude_tests"
2050 stdin_filename = str(path / "b/exclude/a.py")
2051 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2052 assert_collected_sources(
2055 extend_exclude=r"/exclude/|a\.py",
2056 stdin_filename=stdin_filename,
2059 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2060 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2061 # Force exclude should exclude the file when passing it through
2063 path = THIS_DIR / "data" / "include_exclude_tests"
2064 stdin_filename = str(path / "b/exclude/a.py")
2065 assert_collected_sources(
2068 force_exclude=r"/exclude/|a\.py",
2069 stdin_filename=stdin_filename,
2074 with open(black.__file__, "r", encoding="utf-8") as _bf:
2075 black_source_lines = _bf.readlines()
2076 except UnicodeDecodeError:
2077 if not black.COMPILED:
2082 frame: types.FrameType, event: str, arg: Any
2083 ) -> Callable[[types.FrameType, str, Any], Any]:
2084 """Show function calls `from black/__init__.py` as they happen.
2086 Register this with `sys.settrace()` in a test you're debugging.
2091 stack = len(inspect.stack()) - 19
2093 filename = frame.f_code.co_filename
2094 lineno = frame.f_lineno
2095 func_sig_lineno = lineno - 1
2096 funcname = black_source_lines[func_sig_lineno].strip()
2097 while funcname.startswith("@"):
2098 func_sig_lineno += 1
2099 funcname = black_source_lines[func_sig_lineno].strip()
2100 if "black/__init__.py" in filename:
2101 print(f"{' ' * stack}{lineno}:{funcname}")