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.
13 from concurrent.futures import ThreadPoolExecutor
14 from contextlib import contextmanager, redirect_stderr
15 from dataclasses import replace
16 from io import BytesIO
17 from pathlib import Path
18 from platform import system
19 from tempfile import TemporaryDirectory
32 from unittest.mock import MagicMock, patch
36 from click import unstyle
37 from click.testing import CliRunner
38 from pathspec import PathSpec
42 from black import Feature, TargetVersion
43 from black import re_compile_maybe_verbose as compile_pattern
44 from black.cache import get_cache_dir, get_cache_file
45 from black.debug import DebugVisitor
46 from black.output import color_diff, diff
47 from black.report import Report
49 # Import other test classes
50 from tests.util import (
68 THIS_FILE = Path(__file__)
69 EMPTY_CONFIG = THIS_DIR / "data" / "empty_pyproject.toml"
70 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
71 DEFAULT_EXCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_EXCLUDES)
72 DEFAULT_INCLUDE = black.re_compile_maybe_verbose(black.const.DEFAULT_INCLUDES)
76 # Match the time output in a diff, but nothing else
77 DIFF_TIME = re.compile(r"\t[\d\-:+\. ]+")
81 def cache_dir(exists: bool = True) -> Iterator[Path]:
82 with TemporaryDirectory() as workspace:
83 cache_dir = Path(workspace)
85 cache_dir = cache_dir / "new"
86 with patch("black.cache.CACHE_DIR", cache_dir):
91 def event_loop() -> Iterator[None]:
92 policy = asyncio.get_event_loop_policy()
93 loop = policy.new_event_loop()
94 asyncio.set_event_loop(loop)
102 class FakeContext(click.Context):
103 """A fake click Context for when calling functions that need it."""
105 def __init__(self) -> None:
106 self.default_map: Dict[str, Any] = {}
107 # Dummy root, since most of the tests don't care about it
108 self.obj: Dict[str, Any] = {"root": PROJECT_ROOT}
111 class FakeParameter(click.Parameter):
112 """A fake click Parameter for when calling functions that need it."""
114 def __init__(self) -> None:
118 class BlackRunner(CliRunner):
119 """Make sure STDOUT and STDERR are kept separate when testing Black via its CLI."""
121 def __init__(self) -> None:
122 super().__init__(mix_stderr=False)
126 args: List[str], exit_code: int = 0, ignore_config: bool = True
128 runner = BlackRunner()
130 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
131 result = runner.invoke(black.main, args, catch_exceptions=False)
132 assert result.stdout_bytes is not None
133 assert result.stderr_bytes is not None
135 f"Failed with args: {args}\n"
136 f"stdout: {result.stdout_bytes.decode()!r}\n"
137 f"stderr: {result.stderr_bytes.decode()!r}\n"
138 f"exception: {result.exception}"
140 assert result.exit_code == exit_code, msg
143 class BlackTestCase(BlackBaseTestCase):
144 invokeBlack = staticmethod(invokeBlack)
146 def test_empty_ff(self) -> None:
148 tmp_file = Path(black.dump_to_file())
150 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
151 with open(tmp_file, encoding="utf8") as f:
155 self.assertFormatEqual(expected, actual)
157 @patch("black.dump_to_file", dump_to_stderr)
158 def test_one_empty_line(self) -> None:
159 mode = black.Mode(preview=True)
160 for nl in ["\n", "\r\n"]:
161 source = expected = nl
162 assert_format(source, expected, mode=mode)
164 def test_one_empty_line_ff(self) -> None:
165 mode = black.Mode(preview=True)
166 for nl in ["\n", "\r\n"]:
168 tmp_file = Path(black.dump_to_file(nl))
169 if system() == "Windows":
170 # Writing files in text mode automatically uses the system newline,
171 # but in this case we don't want this for testing reasons. See:
172 # https://github.com/psf/black/pull/3348
173 with open(tmp_file, "wb") as f:
174 f.write(nl.encode("utf-8"))
177 ff(tmp_file, mode=mode, write_back=black.WriteBack.YES)
179 with open(tmp_file, "rb") as f:
180 actual = f.read().decode("utf8")
183 self.assertFormatEqual(expected, actual)
185 def test_experimental_string_processing_warns(self) -> None:
187 black.mode.Deprecated, black.Mode, experimental_string_processing=True
190 def test_piping(self) -> None:
191 source, expected = read_data_from_file(PROJECT_ROOT / "src/black/__init__.py")
192 result = BlackRunner().invoke(
197 f"--line-length={black.DEFAULT_LINE_LENGTH}",
198 f"--config={EMPTY_CONFIG}",
200 input=BytesIO(source.encode("utf8")),
202 self.assertEqual(result.exit_code, 0)
203 self.assertFormatEqual(expected, result.output)
204 if source != result.output:
205 black.assert_equivalent(source, result.output)
206 black.assert_stable(source, result.output, DEFAULT_MODE)
208 def test_piping_diff(self) -> None:
209 diff_header = re.compile(
210 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
213 source, _ = read_data("simple_cases", "expression.py")
214 expected, _ = read_data("simple_cases", "expression.diff")
218 f"--line-length={black.DEFAULT_LINE_LENGTH}",
220 f"--config={EMPTY_CONFIG}",
222 result = BlackRunner().invoke(
223 black.main, args, input=BytesIO(source.encode("utf8"))
225 self.assertEqual(result.exit_code, 0)
226 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
227 actual = actual.rstrip() + "\n" # the diff output has a trailing space
228 self.assertEqual(expected, actual)
230 def test_piping_diff_with_color(self) -> None:
231 source, _ = read_data("simple_cases", "expression.py")
235 f"--line-length={black.DEFAULT_LINE_LENGTH}",
238 f"--config={EMPTY_CONFIG}",
240 result = BlackRunner().invoke(
241 black.main, args, input=BytesIO(source.encode("utf8"))
243 actual = result.output
244 # Again, the contents are checked in a different test, so only look for colors.
245 self.assertIn("\033[1m", actual)
246 self.assertIn("\033[36m", actual)
247 self.assertIn("\033[32m", actual)
248 self.assertIn("\033[31m", actual)
249 self.assertIn("\033[0m", actual)
251 @patch("black.dump_to_file", dump_to_stderr)
252 def _test_wip(self) -> None:
253 source, expected = read_data("miscellaneous", "wip")
254 sys.settrace(tracefunc)
257 experimental_string_processing=False,
258 target_versions={black.TargetVersion.PY38},
260 actual = fs(source, mode=mode)
262 self.assertFormatEqual(expected, actual)
263 black.assert_equivalent(source, actual)
264 black.assert_stable(source, actual, black.FileMode())
266 def test_pep_572_version_detection(self) -> None:
267 source, _ = read_data("py_38", "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("simple_cases", "expression.py")
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("simple_cases", "expression.py")
290 expected, _ = read_data("simple_cases", "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(
298 black.main, ["--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
300 self.assertEqual(result.exit_code, 0)
303 actual = result.output
304 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
305 if expected != actual:
306 dump = black.dump_to_file(actual)
308 "Expected diff isn't equal to the actual. If you made changes to"
309 " expression.py and this is an anticipated difference, overwrite"
310 f" tests/data/expression.diff with {dump}"
312 self.assertEqual(expected, actual, msg)
314 def test_expression_diff_with_color(self) -> None:
315 source, _ = read_data("simple_cases", "expression.py")
316 expected, _ = read_data("simple_cases", "expression.diff")
317 tmp_file = Path(black.dump_to_file(source))
319 result = BlackRunner().invoke(
321 ["--diff", "--color", str(tmp_file), f"--config={EMPTY_CONFIG}"],
325 actual = result.output
326 # We check the contents of the diff in `test_expression_diff`. All
327 # we need to check here is that color codes exist in the result.
328 self.assertIn("\033[1m", actual)
329 self.assertIn("\033[36m", actual)
330 self.assertIn("\033[32m", actual)
331 self.assertIn("\033[31m", actual)
332 self.assertIn("\033[0m", actual)
334 def test_detect_pos_only_arguments(self) -> None:
335 source, _ = read_data("py_38", "pep_570")
336 root = black.lib2to3_parse(source)
337 features = black.get_features_used(root)
338 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
339 versions = black.detect_target_versions(root)
340 self.assertIn(black.TargetVersion.PY38, versions)
342 def test_detect_debug_f_strings(self) -> None:
343 root = black.lib2to3_parse("""f"{x=}" """)
344 features = black.get_features_used(root)
345 self.assertIn(black.Feature.DEBUG_F_STRINGS, features)
346 versions = black.detect_target_versions(root)
347 self.assertIn(black.TargetVersion.PY38, versions)
349 root = black.lib2to3_parse(
350 """f"{x}"\nf'{"="}'\nf'{(x:=5)}'\nf'{f(a="3=")}'\nf'{x:=10}'\n"""
352 features = black.get_features_used(root)
353 self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
355 # We don't yet support feature version detection in nested f-strings
356 root = black.lib2to3_parse(
357 """f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """
359 features = black.get_features_used(root)
360 self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
362 @patch("black.dump_to_file", dump_to_stderr)
363 def test_string_quotes(self) -> None:
364 source, expected = read_data("miscellaneous", "string_quotes")
365 mode = black.Mode(preview=True)
366 assert_format(source, expected, mode)
367 mode = replace(mode, string_normalization=False)
368 not_normalized = fs(source, mode=mode)
369 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
370 black.assert_equivalent(source, not_normalized)
371 black.assert_stable(source, not_normalized, mode=mode)
373 def test_skip_source_first_line(self) -> None:
374 source, _ = read_data("miscellaneous", "invalid_header")
375 tmp_file = Path(black.dump_to_file(source))
376 # Full source should fail (invalid syntax at header)
377 self.invokeBlack([str(tmp_file), "--diff", "--check"], exit_code=123)
378 # So, skipping the first line should work
379 result = BlackRunner().invoke(
380 black.main, [str(tmp_file), "-x", f"--config={EMPTY_CONFIG}"]
382 self.assertEqual(result.exit_code, 0)
383 with open(tmp_file, encoding="utf8") as f:
385 self.assertFormatEqual(source, actual)
387 def test_skip_source_first_line_when_mixing_newlines(self) -> None:
388 code_mixing_newlines = b"Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n"
389 expected = b"Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n"
390 with TemporaryDirectory() as workspace:
391 test_file = Path(workspace) / "skip_header.py"
392 test_file.write_bytes(code_mixing_newlines)
393 mode = replace(DEFAULT_MODE, skip_source_first_line=True)
394 ff(test_file, mode=mode, write_back=black.WriteBack.YES)
395 self.assertEqual(test_file.read_bytes(), expected)
397 def test_skip_magic_trailing_comma(self) -> None:
398 source, _ = read_data("simple_cases", "expression")
399 expected, _ = read_data(
400 "miscellaneous", "expression_skip_magic_trailing_comma.diff"
402 tmp_file = Path(black.dump_to_file(source))
403 diff_header = re.compile(
404 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
405 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
408 result = BlackRunner().invoke(
409 black.main, ["-C", "--diff", str(tmp_file), f"--config={EMPTY_CONFIG}"]
411 self.assertEqual(result.exit_code, 0)
414 actual = result.output
415 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
416 actual = actual.rstrip() + "\n" # the diff output has a trailing space
417 if expected != actual:
418 dump = black.dump_to_file(actual)
420 "Expected diff isn't equal to the actual. If you made changes to"
421 " expression.py and this is an anticipated difference, overwrite"
422 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
424 self.assertEqual(expected, actual, msg)
426 @patch("black.dump_to_file", dump_to_stderr)
427 def test_async_as_identifier(self) -> None:
428 source_path = get_case_path("miscellaneous", "async_as_identifier")
429 source, expected = read_data_from_file(source_path)
431 self.assertFormatEqual(expected, actual)
432 major, minor = sys.version_info[:2]
433 if major < 3 or (major <= 3 and minor < 7):
434 black.assert_equivalent(source, actual)
435 black.assert_stable(source, actual, DEFAULT_MODE)
436 # ensure black can parse this when the target is 3.6
437 self.invokeBlack([str(source_path), "--target-version", "py36"])
438 # but not on 3.7, because async/await is no longer an identifier
439 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
441 @patch("black.dump_to_file", dump_to_stderr)
442 def test_python37(self) -> None:
443 source_path = get_case_path("py_37", "python37")
444 source, expected = read_data_from_file(source_path)
446 self.assertFormatEqual(expected, actual)
447 major, minor = sys.version_info[:2]
448 if major > 3 or (major == 3 and minor >= 7):
449 black.assert_equivalent(source, actual)
450 black.assert_stable(source, actual, DEFAULT_MODE)
451 # ensure black can parse this when the target is 3.7
452 self.invokeBlack([str(source_path), "--target-version", "py37"])
453 # but not on 3.6, because we use async as a reserved keyword
454 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
456 def test_tab_comment_indentation(self) -> None:
457 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
458 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
459 self.assertFormatEqual(contents_spc, fs(contents_spc))
460 self.assertFormatEqual(contents_spc, fs(contents_tab))
462 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
463 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
464 self.assertFormatEqual(contents_spc, fs(contents_spc))
465 self.assertFormatEqual(contents_spc, fs(contents_tab))
467 # mixed tabs and spaces (valid Python 2 code)
468 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
469 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
470 self.assertFormatEqual(contents_spc, fs(contents_spc))
471 self.assertFormatEqual(contents_spc, fs(contents_tab))
473 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
474 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
475 self.assertFormatEqual(contents_spc, fs(contents_spc))
476 self.assertFormatEqual(contents_spc, fs(contents_tab))
478 def test_report_verbose(self) -> None:
479 report = Report(verbose=True)
483 def out(msg: str, **kwargs: Any) -> None:
484 out_lines.append(msg)
486 def err(msg: str, **kwargs: Any) -> None:
487 err_lines.append(msg)
489 with patch("black.output._out", out), patch("black.output._err", err):
490 report.done(Path("f1"), black.Changed.NO)
491 self.assertEqual(len(out_lines), 1)
492 self.assertEqual(len(err_lines), 0)
493 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
494 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
495 self.assertEqual(report.return_code, 0)
496 report.done(Path("f2"), black.Changed.YES)
497 self.assertEqual(len(out_lines), 2)
498 self.assertEqual(len(err_lines), 0)
499 self.assertEqual(out_lines[-1], "reformatted f2")
501 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
503 report.done(Path("f3"), black.Changed.CACHED)
504 self.assertEqual(len(out_lines), 3)
505 self.assertEqual(len(err_lines), 0)
507 out_lines[-1], "f3 wasn't modified on disk since last run."
510 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
512 self.assertEqual(report.return_code, 0)
514 self.assertEqual(report.return_code, 1)
516 report.failed(Path("e1"), "boom")
517 self.assertEqual(len(out_lines), 3)
518 self.assertEqual(len(err_lines), 1)
519 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
521 unstyle(str(report)),
523 "1 file reformatted, 2 files left unchanged, 1 file failed to"
527 self.assertEqual(report.return_code, 123)
528 report.done(Path("f3"), black.Changed.YES)
529 self.assertEqual(len(out_lines), 4)
530 self.assertEqual(len(err_lines), 1)
531 self.assertEqual(out_lines[-1], "reformatted f3")
533 unstyle(str(report)),
535 "2 files reformatted, 2 files left unchanged, 1 file failed to"
539 self.assertEqual(report.return_code, 123)
540 report.failed(Path("e2"), "boom")
541 self.assertEqual(len(out_lines), 4)
542 self.assertEqual(len(err_lines), 2)
543 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
545 unstyle(str(report)),
547 "2 files reformatted, 2 files left unchanged, 2 files failed to"
551 self.assertEqual(report.return_code, 123)
552 report.path_ignored(Path("wat"), "no match")
553 self.assertEqual(len(out_lines), 5)
554 self.assertEqual(len(err_lines), 2)
555 self.assertEqual(out_lines[-1], "wat ignored: no match")
557 unstyle(str(report)),
559 "2 files reformatted, 2 files left unchanged, 2 files failed to"
563 self.assertEqual(report.return_code, 123)
564 report.done(Path("f4"), black.Changed.NO)
565 self.assertEqual(len(out_lines), 6)
566 self.assertEqual(len(err_lines), 2)
567 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
569 unstyle(str(report)),
571 "2 files reformatted, 3 files left unchanged, 2 files failed to"
575 self.assertEqual(report.return_code, 123)
578 unstyle(str(report)),
580 "2 files would be reformatted, 3 files would be left unchanged, 2"
581 " files would fail to reformat."
587 unstyle(str(report)),
589 "2 files would be reformatted, 3 files would be left unchanged, 2"
590 " files would fail to reformat."
594 def test_report_quiet(self) -> None:
595 report = Report(quiet=True)
599 def out(msg: str, **kwargs: Any) -> None:
600 out_lines.append(msg)
602 def err(msg: str, **kwargs: Any) -> None:
603 err_lines.append(msg)
605 with patch("black.output._out", out), patch("black.output._err", err):
606 report.done(Path("f1"), black.Changed.NO)
607 self.assertEqual(len(out_lines), 0)
608 self.assertEqual(len(err_lines), 0)
609 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
610 self.assertEqual(report.return_code, 0)
611 report.done(Path("f2"), black.Changed.YES)
612 self.assertEqual(len(out_lines), 0)
613 self.assertEqual(len(err_lines), 0)
615 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
617 report.done(Path("f3"), black.Changed.CACHED)
618 self.assertEqual(len(out_lines), 0)
619 self.assertEqual(len(err_lines), 0)
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), 0)
629 self.assertEqual(len(err_lines), 1)
630 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
632 unstyle(str(report)),
634 "1 file reformatted, 2 files left unchanged, 1 file failed to"
638 self.assertEqual(report.return_code, 123)
639 report.done(Path("f3"), black.Changed.YES)
640 self.assertEqual(len(out_lines), 0)
641 self.assertEqual(len(err_lines), 1)
643 unstyle(str(report)),
645 "2 files reformatted, 2 files left unchanged, 1 file failed to"
649 self.assertEqual(report.return_code, 123)
650 report.failed(Path("e2"), "boom")
651 self.assertEqual(len(out_lines), 0)
652 self.assertEqual(len(err_lines), 2)
653 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
655 unstyle(str(report)),
657 "2 files reformatted, 2 files left unchanged, 2 files failed to"
661 self.assertEqual(report.return_code, 123)
662 report.path_ignored(Path("wat"), "no match")
663 self.assertEqual(len(out_lines), 0)
664 self.assertEqual(len(err_lines), 2)
666 unstyle(str(report)),
668 "2 files reformatted, 2 files left unchanged, 2 files failed to"
672 self.assertEqual(report.return_code, 123)
673 report.done(Path("f4"), black.Changed.NO)
674 self.assertEqual(len(out_lines), 0)
675 self.assertEqual(len(err_lines), 2)
677 unstyle(str(report)),
679 "2 files reformatted, 3 files left unchanged, 2 files failed to"
683 self.assertEqual(report.return_code, 123)
686 unstyle(str(report)),
688 "2 files would be reformatted, 3 files would be left unchanged, 2"
689 " files would fail to reformat."
695 unstyle(str(report)),
697 "2 files would be reformatted, 3 files would be left unchanged, 2"
698 " files would fail to reformat."
702 def test_report_normal(self) -> None:
703 report = black.Report()
707 def out(msg: str, **kwargs: Any) -> None:
708 out_lines.append(msg)
710 def err(msg: str, **kwargs: Any) -> None:
711 err_lines.append(msg)
713 with patch("black.output._out", out), patch("black.output._err", err):
714 report.done(Path("f1"), black.Changed.NO)
715 self.assertEqual(len(out_lines), 0)
716 self.assertEqual(len(err_lines), 0)
717 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
718 self.assertEqual(report.return_code, 0)
719 report.done(Path("f2"), black.Changed.YES)
720 self.assertEqual(len(out_lines), 1)
721 self.assertEqual(len(err_lines), 0)
722 self.assertEqual(out_lines[-1], "reformatted f2")
724 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
726 report.done(Path("f3"), black.Changed.CACHED)
727 self.assertEqual(len(out_lines), 1)
728 self.assertEqual(len(err_lines), 0)
729 self.assertEqual(out_lines[-1], "reformatted f2")
731 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
733 self.assertEqual(report.return_code, 0)
735 self.assertEqual(report.return_code, 1)
737 report.failed(Path("e1"), "boom")
738 self.assertEqual(len(out_lines), 1)
739 self.assertEqual(len(err_lines), 1)
740 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
742 unstyle(str(report)),
744 "1 file reformatted, 2 files left unchanged, 1 file failed to"
748 self.assertEqual(report.return_code, 123)
749 report.done(Path("f3"), black.Changed.YES)
750 self.assertEqual(len(out_lines), 2)
751 self.assertEqual(len(err_lines), 1)
752 self.assertEqual(out_lines[-1], "reformatted f3")
754 unstyle(str(report)),
756 "2 files reformatted, 2 files left unchanged, 1 file failed to"
760 self.assertEqual(report.return_code, 123)
761 report.failed(Path("e2"), "boom")
762 self.assertEqual(len(out_lines), 2)
763 self.assertEqual(len(err_lines), 2)
764 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
766 unstyle(str(report)),
768 "2 files reformatted, 2 files left unchanged, 2 files failed to"
772 self.assertEqual(report.return_code, 123)
773 report.path_ignored(Path("wat"), "no match")
774 self.assertEqual(len(out_lines), 2)
775 self.assertEqual(len(err_lines), 2)
777 unstyle(str(report)),
779 "2 files reformatted, 2 files left unchanged, 2 files failed to"
783 self.assertEqual(report.return_code, 123)
784 report.done(Path("f4"), black.Changed.NO)
785 self.assertEqual(len(out_lines), 2)
786 self.assertEqual(len(err_lines), 2)
788 unstyle(str(report)),
790 "2 files reformatted, 3 files left unchanged, 2 files failed to"
794 self.assertEqual(report.return_code, 123)
797 unstyle(str(report)),
799 "2 files would be reformatted, 3 files would be left unchanged, 2"
800 " files would fail to reformat."
806 unstyle(str(report)),
808 "2 files would be reformatted, 3 files would be left unchanged, 2"
809 " files would fail to reformat."
813 def test_lib2to3_parse(self) -> None:
814 with self.assertRaises(black.InvalidInput):
815 black.lib2to3_parse("invalid syntax")
818 black.lib2to3_parse(straddling)
819 black.lib2to3_parse(straddling, {TargetVersion.PY36})
822 with self.assertRaises(black.InvalidInput):
823 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
825 py3_only = "exec(x, end=y)"
826 black.lib2to3_parse(py3_only)
827 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
829 def test_get_features_used_decorator(self) -> None:
830 # Test the feature detection of new decorator syntax
831 # since this makes some test cases of test_get_features_used()
832 # fails if it fails, this is tested first so that a useful case
834 simples, relaxed = read_data("miscellaneous", "decorators")
835 # skip explanation comments at the top of the file
836 for simple_test in simples.split("##")[1:]:
837 node = black.lib2to3_parse(simple_test)
838 decorator = str(node.children[0].children[0]).strip()
840 Feature.RELAXED_DECORATORS,
841 black.get_features_used(node),
843 f"decorator '{decorator}' follows python<=3.8 syntax"
844 "but is detected as 3.9+"
845 # f"The full node is\n{node!r}"
848 # skip the '# output' comment at the top of the output part
849 for relaxed_test in relaxed.split("##")[1:]:
850 node = black.lib2to3_parse(relaxed_test)
851 decorator = str(node.children[0].children[0]).strip()
853 Feature.RELAXED_DECORATORS,
854 black.get_features_used(node),
856 f"decorator '{decorator}' uses python3.9+ syntax"
857 "but is detected as python<=3.8"
858 # f"The full node is\n{node!r}"
862 def test_get_features_used(self) -> None:
863 node = black.lib2to3_parse("def f(*, arg): ...\n")
864 self.assertEqual(black.get_features_used(node), set())
865 node = black.lib2to3_parse("def f(*, arg,): ...\n")
866 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
867 node = black.lib2to3_parse("f(*arg,)\n")
869 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
871 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
872 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
873 node = black.lib2to3_parse("123_456\n")
874 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
875 node = black.lib2to3_parse("123456\n")
876 self.assertEqual(black.get_features_used(node), set())
877 source, expected = read_data("simple_cases", "function")
878 node = black.lib2to3_parse(source)
879 expected_features = {
880 Feature.TRAILING_COMMA_IN_CALL,
881 Feature.TRAILING_COMMA_IN_DEF,
884 self.assertEqual(black.get_features_used(node), expected_features)
885 node = black.lib2to3_parse(expected)
886 self.assertEqual(black.get_features_used(node), expected_features)
887 source, expected = read_data("simple_cases", "expression")
888 node = black.lib2to3_parse(source)
889 self.assertEqual(black.get_features_used(node), set())
890 node = black.lib2to3_parse(expected)
891 self.assertEqual(black.get_features_used(node), set())
892 node = black.lib2to3_parse("lambda a, /, b: ...")
893 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
894 node = black.lib2to3_parse("def fn(a, /, b): ...")
895 self.assertEqual(black.get_features_used(node), {Feature.POS_ONLY_ARGUMENTS})
896 node = black.lib2to3_parse("def fn(): yield a, b")
897 self.assertEqual(black.get_features_used(node), set())
898 node = black.lib2to3_parse("def fn(): return a, b")
899 self.assertEqual(black.get_features_used(node), set())
900 node = black.lib2to3_parse("def fn(): yield *b, c")
901 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
902 node = black.lib2to3_parse("def fn(): return a, *b, c")
903 self.assertEqual(black.get_features_used(node), {Feature.UNPACKING_ON_FLOW})
904 node = black.lib2to3_parse("x = a, *b, c")
905 self.assertEqual(black.get_features_used(node), set())
906 node = black.lib2to3_parse("x: Any = regular")
907 self.assertEqual(black.get_features_used(node), set())
908 node = black.lib2to3_parse("x: Any = (regular, regular)")
909 self.assertEqual(black.get_features_used(node), set())
910 node = black.lib2to3_parse("x: Any = Complex(Type(1))[something]")
911 self.assertEqual(black.get_features_used(node), set())
912 node = black.lib2to3_parse("x: Tuple[int, ...] = a, b, c")
914 black.get_features_used(node), {Feature.ANN_ASSIGN_EXTENDED_RHS}
916 node = black.lib2to3_parse("try: pass\nexcept Something: pass")
917 self.assertEqual(black.get_features_used(node), set())
918 node = black.lib2to3_parse("try: pass\nexcept (*Something,): pass")
919 self.assertEqual(black.get_features_used(node), set())
920 node = black.lib2to3_parse("try: pass\nexcept *Group: pass")
921 self.assertEqual(black.get_features_used(node), {Feature.EXCEPT_STAR})
922 node = black.lib2to3_parse("a[*b]")
923 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
924 node = black.lib2to3_parse("a[x, *y(), z] = t")
925 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
926 node = black.lib2to3_parse("def fn(*args: *T): pass")
927 self.assertEqual(black.get_features_used(node), {Feature.VARIADIC_GENERICS})
929 def test_get_features_used_for_future_flags(self) -> None:
930 for src, features in [
931 ("from __future__ import annotations", {Feature.FUTURE_ANNOTATIONS}),
933 "from __future__ import (other, annotations)",
934 {Feature.FUTURE_ANNOTATIONS},
936 ("a = 1 + 2\nfrom something import annotations", set()),
937 ("from __future__ import x, y", set()),
939 with self.subTest(src=src, features=features):
940 node = black.lib2to3_parse(src)
941 future_imports = black.get_future_imports(node)
943 black.get_features_used(node, future_imports=future_imports),
947 def test_get_future_imports(self) -> None:
948 node = black.lib2to3_parse("\n")
949 self.assertEqual(set(), black.get_future_imports(node))
950 node = black.lib2to3_parse("from __future__ import black\n")
951 self.assertEqual({"black"}, black.get_future_imports(node))
952 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
953 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
954 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
955 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
956 node = black.lib2to3_parse(
957 "from __future__ import multiple\nfrom __future__ import imports\n"
959 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
960 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
961 self.assertEqual({"black"}, black.get_future_imports(node))
962 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
963 self.assertEqual({"black"}, black.get_future_imports(node))
964 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
965 self.assertEqual(set(), black.get_future_imports(node))
966 node = black.lib2to3_parse("from some.module import black\n")
967 self.assertEqual(set(), black.get_future_imports(node))
968 node = black.lib2to3_parse(
969 "from __future__ import unicode_literals as _unicode_literals"
971 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
972 node = black.lib2to3_parse(
973 "from __future__ import unicode_literals as _lol, print"
975 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
977 @pytest.mark.incompatible_with_mypyc
978 def test_debug_visitor(self) -> None:
979 source, _ = read_data("miscellaneous", "debug_visitor")
980 expected, _ = read_data("miscellaneous", "debug_visitor.out")
984 def out(msg: str, **kwargs: Any) -> None:
985 out_lines.append(msg)
987 def err(msg: str, **kwargs: Any) -> None:
988 err_lines.append(msg)
990 with patch("black.debug.out", out):
991 DebugVisitor.show(source)
992 actual = "\n".join(out_lines) + "\n"
994 if expected != actual:
995 log_name = black.dump_to_file(*out_lines)
999 f"AST print out is different. Actual version dumped to {log_name}",
1002 def test_format_file_contents(self) -> None:
1005 with self.assertRaises(black.NothingChanged):
1006 black.format_file_contents(empty, mode=mode, fast=False)
1008 with self.assertRaises(black.NothingChanged):
1009 black.format_file_contents(just_nl, mode=mode, fast=False)
1010 same = "j = [1, 2, 3]\n"
1011 with self.assertRaises(black.NothingChanged):
1012 black.format_file_contents(same, mode=mode, fast=False)
1013 different = "j = [1,2,3]"
1015 actual = black.format_file_contents(different, mode=mode, fast=False)
1016 self.assertEqual(expected, actual)
1017 invalid = "return if you can"
1018 with self.assertRaises(black.InvalidInput) as e:
1019 black.format_file_contents(invalid, mode=mode, fast=False)
1020 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
1022 mode = black.Mode(preview=True)
1024 with self.assertRaises(black.NothingChanged):
1025 black.format_file_contents(just_crlf, mode=mode, fast=False)
1026 just_whitespace_nl = "\n\t\n \n\t \n \t\n\n"
1027 actual = black.format_file_contents(just_whitespace_nl, mode=mode, fast=False)
1028 self.assertEqual("\n", actual)
1029 just_whitespace_crlf = "\r\n\t\r\n \r\n\t \r\n \t\r\n\r\n"
1030 actual = black.format_file_contents(just_whitespace_crlf, mode=mode, fast=False)
1031 self.assertEqual("\r\n", actual)
1033 def test_endmarker(self) -> None:
1034 n = black.lib2to3_parse("\n")
1035 self.assertEqual(n.type, black.syms.file_input)
1036 self.assertEqual(len(n.children), 1)
1037 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
1039 @pytest.mark.incompatible_with_mypyc
1040 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
1041 def test_assertFormatEqual(self) -> None:
1045 def out(msg: str, **kwargs: Any) -> None:
1046 out_lines.append(msg)
1048 def err(msg: str, **kwargs: Any) -> None:
1049 err_lines.append(msg)
1051 with patch("black.output._out", out), patch("black.output._err", err):
1052 with self.assertRaises(AssertionError):
1053 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
1055 out_str = "".join(out_lines)
1056 self.assertIn("Expected tree:", out_str)
1057 self.assertIn("Actual tree:", out_str)
1058 self.assertEqual("".join(err_lines), "")
1061 @patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1062 def test_works_in_mono_process_only_environment(self) -> None:
1063 with cache_dir() as workspace:
1065 (workspace / "one.py").resolve(),
1066 (workspace / "two.py").resolve(),
1068 f.write_text('print("hello")\n')
1069 self.invokeBlack([str(workspace)])
1072 def test_check_diff_use_together(self) -> None:
1074 # Files which will be reformatted.
1075 src1 = get_case_path("miscellaneous", "string_quotes")
1076 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1077 # Files which will not be reformatted.
1078 src2 = get_case_path("simple_cases", "composition")
1079 self.invokeBlack([str(src2), "--diff", "--check"])
1080 # Multi file command.
1081 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1083 def test_no_src_fails(self) -> None:
1085 self.invokeBlack([], exit_code=1)
1087 def test_src_and_code_fails(self) -> None:
1089 self.invokeBlack([".", "-c", "0"], exit_code=1)
1091 def test_broken_symlink(self) -> None:
1092 with cache_dir() as workspace:
1093 symlink = workspace / "broken_link.py"
1095 symlink.symlink_to("nonexistent.py")
1096 except (OSError, NotImplementedError) as e:
1097 self.skipTest(f"Can't create symlinks: {e}")
1098 self.invokeBlack([str(workspace.resolve())])
1100 def test_single_file_force_pyi(self) -> None:
1101 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1102 contents, expected = read_data("miscellaneous", "force_pyi")
1103 with cache_dir() as workspace:
1104 path = (workspace / "file.py").resolve()
1105 with open(path, "w") as fh:
1107 self.invokeBlack([str(path), "--pyi"])
1108 with open(path, "r") as fh:
1110 # verify cache with --pyi is separate
1111 pyi_cache = black.read_cache(pyi_mode)
1112 self.assertIn(str(path), pyi_cache)
1113 normal_cache = black.read_cache(DEFAULT_MODE)
1114 self.assertNotIn(str(path), normal_cache)
1115 self.assertFormatEqual(expected, actual)
1116 black.assert_equivalent(contents, actual)
1117 black.assert_stable(contents, actual, pyi_mode)
1120 def test_multi_file_force_pyi(self) -> None:
1121 reg_mode = DEFAULT_MODE
1122 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1123 contents, expected = read_data("miscellaneous", "force_pyi")
1124 with cache_dir() as workspace:
1126 (workspace / "file1.py").resolve(),
1127 (workspace / "file2.py").resolve(),
1130 with open(path, "w") as fh:
1132 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1134 with open(path, "r") as fh:
1136 self.assertEqual(actual, expected)
1137 # verify cache with --pyi is separate
1138 pyi_cache = black.read_cache(pyi_mode)
1139 normal_cache = black.read_cache(reg_mode)
1141 self.assertIn(str(path), pyi_cache)
1142 self.assertNotIn(str(path), normal_cache)
1144 def test_pipe_force_pyi(self) -> None:
1145 source, expected = read_data("miscellaneous", "force_pyi")
1146 result = CliRunner().invoke(
1147 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1149 self.assertEqual(result.exit_code, 0)
1150 actual = result.output
1151 self.assertFormatEqual(actual, expected)
1153 def test_single_file_force_py36(self) -> None:
1154 reg_mode = DEFAULT_MODE
1155 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1156 source, expected = read_data("miscellaneous", "force_py36")
1157 with cache_dir() as workspace:
1158 path = (workspace / "file.py").resolve()
1159 with open(path, "w") as fh:
1161 self.invokeBlack([str(path), *PY36_ARGS])
1162 with open(path, "r") as fh:
1164 # verify cache with --target-version is separate
1165 py36_cache = black.read_cache(py36_mode)
1166 self.assertIn(str(path), py36_cache)
1167 normal_cache = black.read_cache(reg_mode)
1168 self.assertNotIn(str(path), normal_cache)
1169 self.assertEqual(actual, expected)
1172 def test_multi_file_force_py36(self) -> None:
1173 reg_mode = DEFAULT_MODE
1174 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1175 source, expected = read_data("miscellaneous", "force_py36")
1176 with cache_dir() as workspace:
1178 (workspace / "file1.py").resolve(),
1179 (workspace / "file2.py").resolve(),
1182 with open(path, "w") as fh:
1184 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1186 with open(path, "r") as fh:
1188 self.assertEqual(actual, expected)
1189 # verify cache with --target-version is separate
1190 pyi_cache = black.read_cache(py36_mode)
1191 normal_cache = black.read_cache(reg_mode)
1193 self.assertIn(str(path), pyi_cache)
1194 self.assertNotIn(str(path), normal_cache)
1196 def test_pipe_force_py36(self) -> None:
1197 source, expected = read_data("miscellaneous", "force_py36")
1198 result = CliRunner().invoke(
1200 ["-", "-q", "--target-version=py36"],
1201 input=BytesIO(source.encode("utf8")),
1203 self.assertEqual(result.exit_code, 0)
1204 actual = result.output
1205 self.assertFormatEqual(actual, expected)
1207 @pytest.mark.incompatible_with_mypyc
1208 def test_reformat_one_with_stdin(self) -> None:
1210 "black.format_stdin_to_stdout",
1211 return_value=lambda *args, **kwargs: black.Changed.YES,
1213 report = MagicMock()
1218 write_back=black.WriteBack.YES,
1222 fsts.assert_called_once()
1223 report.done.assert_called_with(path, black.Changed.YES)
1225 @pytest.mark.incompatible_with_mypyc
1226 def test_reformat_one_with_stdin_filename(self) -> None:
1228 "black.format_stdin_to_stdout",
1229 return_value=lambda *args, **kwargs: black.Changed.YES,
1231 report = MagicMock()
1233 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1238 write_back=black.WriteBack.YES,
1242 fsts.assert_called_once_with(
1243 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1245 # __BLACK_STDIN_FILENAME__ should have been stripped
1246 report.done.assert_called_with(expected, black.Changed.YES)
1248 @pytest.mark.incompatible_with_mypyc
1249 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1251 "black.format_stdin_to_stdout",
1252 return_value=lambda *args, **kwargs: black.Changed.YES,
1254 report = MagicMock()
1256 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1261 write_back=black.WriteBack.YES,
1265 fsts.assert_called_once_with(
1267 write_back=black.WriteBack.YES,
1268 mode=replace(DEFAULT_MODE, is_pyi=True),
1270 # __BLACK_STDIN_FILENAME__ should have been stripped
1271 report.done.assert_called_with(expected, black.Changed.YES)
1273 @pytest.mark.incompatible_with_mypyc
1274 def test_reformat_one_with_stdin_filename_ipynb(self) -> None:
1276 "black.format_stdin_to_stdout",
1277 return_value=lambda *args, **kwargs: black.Changed.YES,
1279 report = MagicMock()
1281 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1286 write_back=black.WriteBack.YES,
1290 fsts.assert_called_once_with(
1292 write_back=black.WriteBack.YES,
1293 mode=replace(DEFAULT_MODE, is_ipynb=True),
1295 # __BLACK_STDIN_FILENAME__ should have been stripped
1296 report.done.assert_called_with(expected, black.Changed.YES)
1298 @pytest.mark.incompatible_with_mypyc
1299 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1301 "black.format_stdin_to_stdout",
1302 return_value=lambda *args, **kwargs: black.Changed.YES,
1304 report = MagicMock()
1305 # Even with an existing file, since we are forcing stdin, black
1306 # should output to stdout and not modify the file inplace
1307 p = THIS_DIR / "data" / "simple_cases" / "collections.py"
1308 # Make sure is_file actually returns True
1309 self.assertTrue(p.is_file())
1310 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1315 write_back=black.WriteBack.YES,
1319 fsts.assert_called_once()
1320 # __BLACK_STDIN_FILENAME__ should have been stripped
1321 report.done.assert_called_with(expected, black.Changed.YES)
1323 def test_reformat_one_with_stdin_empty(self) -> None:
1330 (" \t\r\n\t ", "\r\n"),
1334 output: io.StringIO, io_TextIOWrapper: Type[io.TextIOWrapper]
1335 ) -> Callable[[Any, Any], io.TextIOWrapper]:
1336 def get_output(*args: Any, **kwargs: Any) -> io.TextIOWrapper:
1337 if args == (sys.stdout.buffer,):
1338 # It's `format_stdin_to_stdout()` calling `io.TextIOWrapper()`,
1339 # return our mock object.
1341 # It's something else (i.e. `decode_bytes()`) calling
1342 # `io.TextIOWrapper()`, pass through to the original implementation.
1343 # See discussion in https://github.com/psf/black/pull/2489
1344 return io_TextIOWrapper(*args, **kwargs)
1348 mode = black.Mode(preview=True)
1349 for content, expected in cases:
1350 output = io.StringIO()
1351 io_TextIOWrapper = io.TextIOWrapper
1353 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1355 black.format_stdin_to_stdout(
1358 write_back=black.WriteBack.YES,
1361 except io.UnsupportedOperation:
1362 pass # StringIO does not support detach
1363 assert output.getvalue() == expected
1365 # An empty string is the only test case for `preview=False`
1366 output = io.StringIO()
1367 io_TextIOWrapper = io.TextIOWrapper
1368 with patch("io.TextIOWrapper", _new_wrapper(output, io_TextIOWrapper)):
1370 black.format_stdin_to_stdout(
1373 write_back=black.WriteBack.YES,
1376 except io.UnsupportedOperation:
1377 pass # StringIO does not support detach
1378 assert output.getvalue() == ""
1380 def test_invalid_cli_regex(self) -> None:
1381 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1382 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1384 def test_required_version_matches_version(self) -> None:
1386 ["--required-version", black.__version__, "-c", "0"],
1391 def test_required_version_matches_partial_version(self) -> None:
1393 ["--required-version", black.__version__.split(".")[0], "-c", "0"],
1398 def test_required_version_does_not_match_on_minor_version(self) -> None:
1400 ["--required-version", black.__version__.split(".")[0] + ".999", "-c", "0"],
1405 def test_required_version_does_not_match_version(self) -> None:
1406 result = BlackRunner().invoke(
1408 ["--required-version", "20.99b", "-c", "0"],
1410 self.assertEqual(result.exit_code, 1)
1411 self.assertIn("required version", result.stderr)
1413 def test_preserves_line_endings(self) -> None:
1414 with TemporaryDirectory() as workspace:
1415 test_file = Path(workspace) / "test.py"
1416 for nl in ["\n", "\r\n"]:
1417 contents = nl.join(["def f( ):", " pass"])
1418 test_file.write_bytes(contents.encode())
1419 ff(test_file, write_back=black.WriteBack.YES)
1420 updated_contents: bytes = test_file.read_bytes()
1421 self.assertIn(nl.encode(), updated_contents)
1423 self.assertNotIn(b"\r\n", updated_contents)
1425 def test_preserves_line_endings_via_stdin(self) -> None:
1426 for nl in ["\n", "\r\n"]:
1427 contents = nl.join(["def f( ):", " pass"])
1428 runner = BlackRunner()
1429 result = runner.invoke(
1430 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1432 self.assertEqual(result.exit_code, 0)
1433 output = result.stdout_bytes
1434 self.assertIn(nl.encode("utf8"), output)
1436 self.assertNotIn(b"\r\n", output)
1438 def test_normalize_line_endings(self) -> None:
1439 with TemporaryDirectory() as workspace:
1440 test_file = Path(workspace) / "test.py"
1441 for data, expected in (
1442 (b"c\r\nc\n ", b"c\r\nc\r\n"),
1443 (b"l\nl\r\n ", b"l\nl\n"),
1445 test_file.write_bytes(data)
1446 ff(test_file, write_back=black.WriteBack.YES)
1447 self.assertEqual(test_file.read_bytes(), expected)
1449 def test_assert_equivalent_different_asts(self) -> None:
1450 with self.assertRaises(AssertionError):
1451 black.assert_equivalent("{}", "None")
1453 def test_shhh_click(self) -> None:
1455 from click import _unicodefun # type: ignore
1457 self.skipTest("Incompatible Click version")
1459 if not hasattr(_unicodefun, "_verify_python_env"):
1460 self.skipTest("Incompatible Click version")
1462 # First, let's see if Click is crashing with a preferred ASCII charset.
1463 with patch("locale.getpreferredencoding") as gpe:
1464 gpe.return_value = "ASCII"
1465 with self.assertRaises(RuntimeError):
1466 _unicodefun._verify_python_env()
1467 # Now, let's silence Click...
1469 # ...and confirm it's silent.
1470 with patch("locale.getpreferredencoding") as gpe:
1471 gpe.return_value = "ASCII"
1473 _unicodefun._verify_python_env()
1474 except RuntimeError as re:
1475 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1477 def test_root_logger_not_used_directly(self) -> None:
1478 def fail(*args: Any, **kwargs: Any) -> None:
1479 self.fail("Record created with root logger")
1481 with patch.multiple(
1490 ff(THIS_DIR / "util.py")
1492 def test_invalid_config_return_code(self) -> None:
1493 tmp_file = Path(black.dump_to_file())
1495 tmp_config = Path(black.dump_to_file())
1497 args = ["--config", str(tmp_config), str(tmp_file)]
1498 self.invokeBlack(args, exit_code=2, ignore_config=False)
1502 def test_parse_pyproject_toml(self) -> None:
1503 test_toml_file = THIS_DIR / "test.toml"
1504 config = black.parse_pyproject_toml(str(test_toml_file))
1505 self.assertEqual(config["verbose"], 1)
1506 self.assertEqual(config["check"], "no")
1507 self.assertEqual(config["diff"], "y")
1508 self.assertEqual(config["color"], True)
1509 self.assertEqual(config["line_length"], 79)
1510 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1511 self.assertEqual(config["python_cell_magics"], ["custom1", "custom2"])
1512 self.assertEqual(config["exclude"], r"\.pyi?$")
1513 self.assertEqual(config["include"], r"\.py?$")
1515 def test_read_pyproject_toml(self) -> None:
1516 test_toml_file = THIS_DIR / "test.toml"
1517 fake_ctx = FakeContext()
1518 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1519 config = fake_ctx.default_map
1520 self.assertEqual(config["verbose"], "1")
1521 self.assertEqual(config["check"], "no")
1522 self.assertEqual(config["diff"], "y")
1523 self.assertEqual(config["color"], "True")
1524 self.assertEqual(config["line_length"], "79")
1525 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1526 self.assertEqual(config["exclude"], r"\.pyi?$")
1527 self.assertEqual(config["include"], r"\.py?$")
1529 @pytest.mark.incompatible_with_mypyc
1530 def test_find_project_root(self) -> None:
1531 with TemporaryDirectory() as workspace:
1532 root = Path(workspace)
1533 test_dir = root / "test"
1536 src_dir = root / "src"
1539 root_pyproject = root / "pyproject.toml"
1540 root_pyproject.touch()
1541 src_pyproject = src_dir / "pyproject.toml"
1542 src_pyproject.touch()
1543 src_python = src_dir / "foo.py"
1547 black.find_project_root((src_dir, test_dir)),
1548 (root.resolve(), "pyproject.toml"),
1551 black.find_project_root((src_dir,)),
1552 (src_dir.resolve(), "pyproject.toml"),
1555 black.find_project_root((src_python,)),
1556 (src_dir.resolve(), "pyproject.toml"),
1559 with change_directory(test_dir):
1561 black.find_project_root(("-",), stdin_filename="../src/a.py"),
1562 (src_dir.resolve(), "pyproject.toml"),
1566 "black.files.find_user_pyproject_toml",
1568 def test_find_pyproject_toml(self, find_user_pyproject_toml: MagicMock) -> None:
1569 find_user_pyproject_toml.side_effect = RuntimeError()
1571 with redirect_stderr(io.StringIO()) as stderr:
1572 result = black.files.find_pyproject_toml(
1573 path_search_start=(str(Path.cwd().root),)
1576 assert result is None
1577 err = stderr.getvalue()
1578 assert "Ignoring user configuration" in err
1581 "black.files.find_user_pyproject_toml",
1582 black.files.find_user_pyproject_toml.__wrapped__,
1584 def test_find_user_pyproject_toml_linux(self) -> None:
1585 if system() == "Windows":
1588 # Test if XDG_CONFIG_HOME is checked
1589 with TemporaryDirectory() as workspace:
1590 tmp_user_config = Path(workspace) / "black"
1591 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1593 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1596 # Test fallback for XDG_CONFIG_HOME
1597 with patch.dict("os.environ"):
1598 os.environ.pop("XDG_CONFIG_HOME", None)
1599 fallback_user_config = Path("~/.config").expanduser() / "black"
1601 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1604 def test_find_user_pyproject_toml_windows(self) -> None:
1605 if system() != "Windows":
1608 user_config_path = Path.home() / ".black"
1610 black.files.find_user_pyproject_toml(), user_config_path.resolve()
1613 def test_bpo_33660_workaround(self) -> None:
1614 if system() == "Windows":
1617 # https://bugs.python.org/issue33660
1619 with change_directory(root):
1620 path = Path("workspace") / "project"
1621 report = black.Report(verbose=True)
1622 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1623 self.assertEqual(normalized_path, "workspace/project")
1625 def test_normalize_path_ignore_windows_junctions_outside_of_root(self) -> None:
1626 if system() != "Windows":
1629 with TemporaryDirectory() as workspace:
1630 root = Path(workspace)
1631 junction_dir = root / "junction"
1632 junction_target_outside_of_root = root / ".."
1633 os.system(f"mklink /J {junction_dir} {junction_target_outside_of_root}")
1635 report = black.Report(verbose=True)
1636 normalized_path = black.normalize_path_maybe_ignore(
1637 junction_dir, root, report
1639 # Manually delete for Python < 3.8
1640 os.system(f"rmdir {junction_dir}")
1642 self.assertEqual(normalized_path, None)
1644 def test_newline_comment_interaction(self) -> None:
1645 source = "class A:\\\r\n# type: ignore\n pass\n"
1646 output = black.format_str(source, mode=DEFAULT_MODE)
1647 black.assert_stable(source, output, mode=DEFAULT_MODE)
1649 def test_bpo_2142_workaround(self) -> None:
1650 # https://bugs.python.org/issue2142
1652 source, _ = read_data("miscellaneous", "missing_final_newline")
1653 # read_data adds a trailing newline
1654 source = source.rstrip()
1655 expected, _ = read_data("miscellaneous", "missing_final_newline.diff")
1656 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1657 diff_header = re.compile(
1658 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1659 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1662 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1663 self.assertEqual(result.exit_code, 0)
1666 actual = result.output
1667 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1668 self.assertEqual(actual, expected)
1671 def compare_results(
1672 result: click.testing.Result, expected_value: str, expected_exit_code: int
1674 """Helper method to test the value and exit code of a click Result."""
1676 result.output == expected_value
1677 ), "The output did not match the expected value."
1678 assert result.exit_code == expected_exit_code, "The exit code is incorrect."
1680 def test_code_option(self) -> None:
1681 """Test the code option with no changes."""
1682 code = 'print("Hello world")\n'
1683 args = ["--code", code]
1684 result = CliRunner().invoke(black.main, args)
1686 self.compare_results(result, code, 0)
1688 def test_code_option_changed(self) -> None:
1689 """Test the code option when changes are required."""
1690 code = "print('hello world')"
1691 formatted = black.format_str(code, mode=DEFAULT_MODE)
1693 args = ["--code", code]
1694 result = CliRunner().invoke(black.main, args)
1696 self.compare_results(result, formatted, 0)
1698 def test_code_option_check(self) -> None:
1699 """Test the code option when check is passed."""
1700 args = ["--check", "--code", 'print("Hello world")\n']
1701 result = CliRunner().invoke(black.main, args)
1702 self.compare_results(result, "", 0)
1704 def test_code_option_check_changed(self) -> None:
1705 """Test the code option when changes are required, and check is passed."""
1706 args = ["--check", "--code", "print('hello world')"]
1707 result = CliRunner().invoke(black.main, args)
1708 self.compare_results(result, "", 1)
1710 def test_code_option_diff(self) -> None:
1711 """Test the code option when diff is passed."""
1712 code = "print('hello world')"
1713 formatted = black.format_str(code, mode=DEFAULT_MODE)
1714 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1716 args = ["--diff", "--code", code]
1717 result = CliRunner().invoke(black.main, args)
1719 # Remove time from diff
1720 output = DIFF_TIME.sub("", result.output)
1722 assert output == result_diff, "The output did not match the expected value."
1723 assert result.exit_code == 0, "The exit code is incorrect."
1725 def test_code_option_color_diff(self) -> None:
1726 """Test the code option when color and diff are passed."""
1727 code = "print('hello world')"
1728 formatted = black.format_str(code, mode=DEFAULT_MODE)
1730 result_diff = diff(code, formatted, "STDIN", "STDOUT")
1731 result_diff = color_diff(result_diff)
1733 args = ["--diff", "--color", "--code", code]
1734 result = CliRunner().invoke(black.main, args)
1736 # Remove time from diff
1737 output = DIFF_TIME.sub("", result.output)
1739 assert output == result_diff, "The output did not match the expected value."
1740 assert result.exit_code == 0, "The exit code is incorrect."
1742 @pytest.mark.incompatible_with_mypyc
1743 def test_code_option_safe(self) -> None:
1744 """Test that the code option throws an error when the sanity checks fail."""
1745 # Patch black.assert_equivalent to ensure the sanity checks fail
1746 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1747 code = 'print("Hello world")'
1748 error_msg = f"{code}\nerror: cannot format <string>: \n"
1750 args = ["--safe", "--code", code]
1751 result = CliRunner().invoke(black.main, args)
1753 self.compare_results(result, error_msg, 123)
1755 def test_code_option_fast(self) -> None:
1756 """Test that the code option ignores errors when the sanity checks fail."""
1757 # Patch black.assert_equivalent to ensure the sanity checks fail
1758 with patch.object(black, "assert_equivalent", side_effect=AssertionError):
1759 code = 'print("Hello world")'
1760 formatted = black.format_str(code, mode=DEFAULT_MODE)
1762 args = ["--fast", "--code", code]
1763 result = CliRunner().invoke(black.main, args)
1765 self.compare_results(result, formatted, 0)
1767 @pytest.mark.incompatible_with_mypyc
1768 def test_code_option_config(self) -> None:
1770 Test that the code option finds the pyproject.toml in the current directory.
1772 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1773 args = ["--code", "print"]
1774 # This is the only directory known to contain a pyproject.toml
1775 with change_directory(PROJECT_ROOT):
1776 CliRunner().invoke(black.main, args)
1777 pyproject_path = Path(Path.cwd(), "pyproject.toml").resolve()
1780 len(parse.mock_calls) >= 1
1781 ), "Expected config parse to be called with the current directory."
1783 _, call_args, _ = parse.mock_calls[0]
1785 call_args[0].lower() == str(pyproject_path).lower()
1786 ), "Incorrect config loaded."
1788 @pytest.mark.incompatible_with_mypyc
1789 def test_code_option_parent_config(self) -> None:
1791 Test that the code option finds the pyproject.toml in the parent directory.
1793 with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
1794 with change_directory(THIS_DIR):
1795 args = ["--code", "print"]
1796 CliRunner().invoke(black.main, args)
1798 pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
1800 len(parse.mock_calls) >= 1
1801 ), "Expected config parse to be called with the current directory."
1803 _, call_args, _ = parse.mock_calls[0]
1805 call_args[0].lower() == str(pyproject_path).lower()
1806 ), "Incorrect config loaded."
1808 def test_for_handled_unexpected_eof_error(self) -> None:
1810 Test that an unexpected EOF SyntaxError is nicely presented.
1812 with pytest.raises(black.parsing.InvalidInput) as exc_info:
1813 black.lib2to3_parse("print(", {})
1815 exc_info.match("Cannot parse: 2:0: EOF in multi-line statement")
1817 def test_equivalency_ast_parse_failure_includes_error(self) -> None:
1818 with pytest.raises(AssertionError) as err:
1819 black.assert_equivalent("a«»a = 1", "a«»a = 1")
1822 # Unfortunately the SyntaxError message has changed in newer versions so we
1823 # can't match it directly.
1824 err.match("invalid character")
1825 err.match(r"\(<unknown>, line 1\)")
1829 def test_get_cache_dir(
1832 monkeypatch: pytest.MonkeyPatch,
1834 # Create multiple cache directories
1835 workspace1 = tmp_path / "ws1"
1837 workspace2 = tmp_path / "ws2"
1840 # Force user_cache_dir to use the temporary directory for easier assertions
1841 patch_user_cache_dir = patch(
1842 target="black.cache.user_cache_dir",
1844 return_value=str(workspace1),
1847 # If BLACK_CACHE_DIR is not set, use user_cache_dir
1848 monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
1849 with patch_user_cache_dir:
1850 assert get_cache_dir() == workspace1
1852 # If it is set, use the path provided in the env var.
1853 monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
1854 assert get_cache_dir() == workspace2
1856 def test_cache_broken_file(self) -> None:
1858 with cache_dir() as workspace:
1859 cache_file = get_cache_file(mode)
1860 cache_file.write_text("this is not a pickle")
1861 assert black.read_cache(mode) == {}
1862 src = (workspace / "test.py").resolve()
1863 src.write_text("print('hello')")
1864 invokeBlack([str(src)])
1865 cache = black.read_cache(mode)
1866 assert str(src) in cache
1868 def test_cache_single_file_already_cached(self) -> None:
1870 with cache_dir() as workspace:
1871 src = (workspace / "test.py").resolve()
1872 src.write_text("print('hello')")
1873 black.write_cache({}, [src], mode)
1874 invokeBlack([str(src)])
1875 assert src.read_text() == "print('hello')"
1878 def test_cache_multiple_files(self) -> None:
1880 with cache_dir() as workspace, patch(
1881 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
1883 one = (workspace / "one.py").resolve()
1884 with one.open("w") as fobj:
1885 fobj.write("print('hello')")
1886 two = (workspace / "two.py").resolve()
1887 with two.open("w") as fobj:
1888 fobj.write("print('hello')")
1889 black.write_cache({}, [one], mode)
1890 invokeBlack([str(workspace)])
1891 with one.open("r") as fobj:
1892 assert fobj.read() == "print('hello')"
1893 with two.open("r") as fobj:
1894 assert fobj.read() == 'print("hello")\n'
1895 cache = black.read_cache(mode)
1896 assert str(one) in cache
1897 assert str(two) in cache
1899 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1900 def test_no_cache_when_writeback_diff(self, color: bool) -> None:
1902 with cache_dir() as workspace:
1903 src = (workspace / "test.py").resolve()
1904 with src.open("w") as fobj:
1905 fobj.write("print('hello')")
1906 with patch("black.read_cache") as read_cache, patch(
1909 cmd = [str(src), "--diff"]
1911 cmd.append("--color")
1913 cache_file = get_cache_file(mode)
1914 assert cache_file.exists() is False
1915 write_cache.assert_not_called()
1916 read_cache.assert_not_called()
1918 @pytest.mark.parametrize("color", [False, True], ids=["no-color", "with-color"])
1920 def test_output_locking_when_writeback_diff(self, color: bool) -> None:
1921 with cache_dir() as workspace:
1922 for tag in range(0, 4):
1923 src = (workspace / f"test{tag}.py").resolve()
1924 with src.open("w") as fobj:
1925 fobj.write("print('hello')")
1927 "black.concurrency.Manager", wraps=multiprocessing.Manager
1929 cmd = ["--diff", str(workspace)]
1931 cmd.append("--color")
1932 invokeBlack(cmd, exit_code=0)
1933 # this isn't quite doing what we want, but if it _isn't_
1934 # called then we cannot be using the lock it provides
1937 def test_no_cache_when_stdin(self) -> None:
1940 result = CliRunner().invoke(
1941 black.main, ["-"], input=BytesIO(b"print('hello')")
1943 assert not result.exit_code
1944 cache_file = get_cache_file(mode)
1945 assert not cache_file.exists()
1947 def test_read_cache_no_cachefile(self) -> None:
1950 assert black.read_cache(mode) == {}
1952 def test_write_cache_read_cache(self) -> None:
1954 with cache_dir() as workspace:
1955 src = (workspace / "test.py").resolve()
1957 black.write_cache({}, [src], mode)
1958 cache = black.read_cache(mode)
1959 assert str(src) in cache
1960 assert cache[str(src)] == black.get_cache_info(src)
1962 def test_filter_cached(self) -> None:
1963 with TemporaryDirectory() as workspace:
1964 path = Path(workspace)
1965 uncached = (path / "uncached").resolve()
1966 cached = (path / "cached").resolve()
1967 cached_but_changed = (path / "changed").resolve()
1970 cached_but_changed.touch()
1972 str(cached): black.get_cache_info(cached),
1973 str(cached_but_changed): (0.0, 0),
1975 todo, done = black.cache.filter_cached(
1976 cache, {uncached, cached, cached_but_changed}
1978 assert todo == {uncached, cached_but_changed}
1979 assert done == {cached}
1981 def test_write_cache_creates_directory_if_needed(self) -> None:
1983 with cache_dir(exists=False) as workspace:
1984 assert not workspace.exists()
1985 black.write_cache({}, [], mode)
1986 assert workspace.exists()
1989 def test_failed_formatting_does_not_get_cached(self) -> None:
1991 with cache_dir() as workspace, patch(
1992 "concurrent.futures.ProcessPoolExecutor", new=ThreadPoolExecutor
1994 failing = (workspace / "failing.py").resolve()
1995 with failing.open("w") as fobj:
1996 fobj.write("not actually python")
1997 clean = (workspace / "clean.py").resolve()
1998 with clean.open("w") as fobj:
1999 fobj.write('print("hello")\n')
2000 invokeBlack([str(workspace)], exit_code=123)
2001 cache = black.read_cache(mode)
2002 assert str(failing) not in cache
2003 assert str(clean) in cache
2005 def test_write_cache_write_fail(self) -> None:
2007 with cache_dir(), patch.object(Path, "open") as mock:
2008 mock.side_effect = OSError
2009 black.write_cache({}, [], mode)
2011 def test_read_cache_line_lengths(self) -> None:
2013 short_mode = replace(DEFAULT_MODE, line_length=1)
2014 with cache_dir() as workspace:
2015 path = (workspace / "file.py").resolve()
2017 black.write_cache({}, [path], mode)
2018 one = black.read_cache(mode)
2019 assert str(path) in one
2020 two = black.read_cache(short_mode)
2021 assert str(path) not in two
2024 def assert_collected_sources(
2025 src: Sequence[Union[str, Path]],
2026 expected: Sequence[Union[str, Path]],
2028 ctx: Optional[FakeContext] = None,
2029 exclude: Optional[str] = None,
2030 include: Optional[str] = None,
2031 extend_exclude: Optional[str] = None,
2032 force_exclude: Optional[str] = None,
2033 stdin_filename: Optional[str] = None,
2035 gs_src = tuple(str(Path(s)) for s in src)
2036 gs_expected = [Path(s) for s in expected]
2037 gs_exclude = None if exclude is None else compile_pattern(exclude)
2038 gs_include = DEFAULT_INCLUDE if include is None else compile_pattern(include)
2039 gs_extend_exclude = (
2040 None if extend_exclude is None else compile_pattern(extend_exclude)
2042 gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
2043 collected = black.get_sources(
2044 ctx=ctx or FakeContext(),
2050 extend_exclude=gs_extend_exclude,
2051 force_exclude=gs_force_exclude,
2052 report=black.Report(),
2053 stdin_filename=stdin_filename,
2055 assert sorted(collected) == sorted(gs_expected)
2058 class TestFileCollection:
2059 def test_include_exclude(self) -> None:
2060 path = THIS_DIR / "data" / "include_exclude_tests"
2063 Path(path / "b/dont_exclude/a.py"),
2064 Path(path / "b/dont_exclude/a.pyi"),
2066 assert_collected_sources(
2070 exclude=r"/exclude/|/\.definitely_exclude/",
2073 def test_gitignore_used_as_default(self) -> None:
2074 base = Path(DATA_DIR / "include_exclude_tests")
2076 base / "b/.definitely_exclude/a.py",
2077 base / "b/.definitely_exclude/a.pyi",
2081 ctx.obj["root"] = base
2082 assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")
2084 def test_gitignore_used_on_multiple_sources(self) -> None:
2085 root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
2087 root / "dir1" / "b.py",
2088 root / "dir2" / "b.py",
2091 ctx.obj["root"] = root
2092 src = [root / "dir1", root / "dir2"]
2093 assert_collected_sources(src, expected, ctx=ctx)
2095 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2096 def test_exclude_for_issue_1572(self) -> None:
2097 # Exclude shouldn't touch files that were explicitly given to Black through the
2098 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
2099 # https://github.com/psf/black/issues/1572
2100 path = DATA_DIR / "include_exclude_tests"
2101 src = [path / "b/exclude/a.py"]
2102 expected = [path / "b/exclude/a.py"]
2103 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2105 def test_gitignore_exclude(self) -> None:
2106 path = THIS_DIR / "data" / "include_exclude_tests"
2107 include = re.compile(r"\.pyi?$")
2108 exclude = re.compile(r"")
2109 report = black.Report()
2110 gitignore = PathSpec.from_lines(
2111 "gitwildmatch", ["exclude/", ".definitely_exclude"]
2113 sources: List[Path] = []
2115 Path(path / "b/dont_exclude/a.py"),
2116 Path(path / "b/dont_exclude/a.pyi"),
2118 this_abs = THIS_DIR.resolve()
2120 black.gen_python_files(
2133 assert sorted(expected) == sorted(sources)
2135 def test_nested_gitignore(self) -> None:
2136 path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
2137 include = re.compile(r"\.pyi?$")
2138 exclude = re.compile(r"")
2139 root_gitignore = black.files.get_gitignore(path)
2140 report = black.Report()
2141 expected: List[Path] = [
2142 Path(path / "x.py"),
2143 Path(path / "root/b.py"),
2144 Path(path / "root/c.py"),
2145 Path(path / "root/child/c.py"),
2147 this_abs = THIS_DIR.resolve()
2149 black.gen_python_files(
2157 {path: root_gitignore},
2162 assert sorted(expected) == sorted(sources)
2164 def test_nested_gitignore_directly_in_source_directory(self) -> None:
2165 # https://github.com/psf/black/issues/2598
2166 path = Path(DATA_DIR / "nested_gitignore_tests")
2167 src = Path(path / "root" / "child")
2168 expected = [src / "a.py", src / "c.py"]
2169 assert_collected_sources([src], expected)
2171 def test_invalid_gitignore(self) -> None:
2172 path = THIS_DIR / "data" / "invalid_gitignore_tests"
2173 empty_config = path / "pyproject.toml"
2174 result = BlackRunner().invoke(
2175 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2177 assert result.exit_code == 1
2178 assert result.stderr_bytes is not None
2180 gitignore = path / ".gitignore"
2181 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2183 def test_invalid_nested_gitignore(self) -> None:
2184 path = THIS_DIR / "data" / "invalid_nested_gitignore_tests"
2185 empty_config = path / "pyproject.toml"
2186 result = BlackRunner().invoke(
2187 black.main, ["--verbose", "--config", str(empty_config), str(path)]
2189 assert result.exit_code == 1
2190 assert result.stderr_bytes is not None
2192 gitignore = path / "a" / ".gitignore"
2193 assert f"Could not parse {gitignore}" in result.stderr_bytes.decode()
2195 def test_gitignore_that_ignores_subfolders(self) -> None:
2196 # If gitignore with */* is in root
2197 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir")
2198 expected = [root / "b.py"]
2200 ctx.obj["root"] = root
2201 assert_collected_sources([root], expected, ctx=ctx)
2203 # If .gitignore with */* is nested
2204 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2207 root / "subdir" / "b.py",
2210 ctx.obj["root"] = root
2211 assert_collected_sources([root], expected, ctx=ctx)
2213 # If command is executed from outer dir
2214 root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests")
2215 target = root / "subdir"
2216 expected = [target / "b.py"]
2218 ctx.obj["root"] = root
2219 assert_collected_sources([target], expected, ctx=ctx)
2221 def test_empty_include(self) -> None:
2222 path = DATA_DIR / "include_exclude_tests"
2225 Path(path / "b/exclude/a.pie"),
2226 Path(path / "b/exclude/a.py"),
2227 Path(path / "b/exclude/a.pyi"),
2228 Path(path / "b/dont_exclude/a.pie"),
2229 Path(path / "b/dont_exclude/a.py"),
2230 Path(path / "b/dont_exclude/a.pyi"),
2231 Path(path / "b/.definitely_exclude/a.pie"),
2232 Path(path / "b/.definitely_exclude/a.py"),
2233 Path(path / "b/.definitely_exclude/a.pyi"),
2234 Path(path / ".gitignore"),
2235 Path(path / "pyproject.toml"),
2237 # Setting exclude explicitly to an empty string to block .gitignore usage.
2238 assert_collected_sources(src, expected, include="", exclude="")
2240 def test_extend_exclude(self) -> None:
2241 path = DATA_DIR / "include_exclude_tests"
2244 Path(path / "b/exclude/a.py"),
2245 Path(path / "b/dont_exclude/a.py"),
2247 assert_collected_sources(
2248 src, expected, exclude=r"\.pyi$", extend_exclude=r"\.definitely_exclude"
2251 @pytest.mark.incompatible_with_mypyc
2252 def test_symlink_out_of_root_directory(self) -> None:
2254 root = THIS_DIR.resolve()
2256 include = re.compile(black.DEFAULT_INCLUDES)
2257 exclude = re.compile(black.DEFAULT_EXCLUDES)
2258 report = black.Report()
2259 gitignore = PathSpec.from_lines("gitwildmatch", [])
2260 # `child` should behave like a symlink which resolved path is clearly
2261 # outside of the `root` directory.
2262 path.iterdir.return_value = [child]
2263 child.resolve.return_value = Path("/a/b/c")
2264 child.as_posix.return_value = "/a/b/c"
2267 black.gen_python_files(
2280 except ValueError as ve:
2281 pytest.fail(f"`get_python_files_in_dir()` failed: {ve}")
2282 path.iterdir.assert_called_once()
2283 child.resolve.assert_called_once()
2285 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2286 def test_get_sources_with_stdin(self) -> None:
2289 assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")
2291 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2292 def test_get_sources_with_stdin_filename(self) -> None:
2294 stdin_filename = str(THIS_DIR / "data/collections.py")
2295 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2296 assert_collected_sources(
2299 exclude=r"/exclude/a\.py",
2300 stdin_filename=stdin_filename,
2303 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2304 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
2305 # Exclude shouldn't exclude stdin_filename since it is mimicking the
2306 # file being passed directly. This is the same as
2307 # test_exclude_for_issue_1572
2308 path = DATA_DIR / "include_exclude_tests"
2310 stdin_filename = str(path / "b/exclude/a.py")
2311 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2312 assert_collected_sources(
2315 exclude=r"/exclude/|a\.py",
2316 stdin_filename=stdin_filename,
2319 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2320 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
2321 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
2322 # file being passed directly. This is the same as
2323 # test_exclude_for_issue_1572
2325 path = THIS_DIR / "data" / "include_exclude_tests"
2326 stdin_filename = str(path / "b/exclude/a.py")
2327 expected = [f"__BLACK_STDIN_FILENAME__{stdin_filename}"]
2328 assert_collected_sources(
2331 extend_exclude=r"/exclude/|a\.py",
2332 stdin_filename=stdin_filename,
2335 @patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
2336 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
2337 # Force exclude should exclude the file when passing it through
2339 path = THIS_DIR / "data" / "include_exclude_tests"
2340 stdin_filename = str(path / "b/exclude/a.py")
2341 assert_collected_sources(
2344 force_exclude=r"/exclude/|a\.py",
2345 stdin_filename=stdin_filename,
2350 with open(black.__file__, "r", encoding="utf-8") as _bf:
2351 black_source_lines = _bf.readlines()
2352 except UnicodeDecodeError:
2353 if not black.COMPILED:
2358 frame: types.FrameType, event: str, arg: Any
2359 ) -> Callable[[types.FrameType, str, Any], Any]:
2360 """Show function calls `from black/__init__.py` as they happen.
2362 Register this with `sys.settrace()` in a test you're debugging.
2367 stack = len(inspect.stack()) - 19
2369 filename = frame.f_code.co_filename
2370 lineno = frame.f_lineno
2371 func_sig_lineno = lineno - 1
2372 funcname = black_source_lines[func_sig_lineno].strip()
2373 while funcname.startswith("@"):
2374 func_sig_lineno += 1
2375 funcname = black_source_lines[func_sig_lineno].strip()
2376 if "black/__init__.py" in filename:
2377 print(f"{' ' * stack}{lineno}:{funcname}")