All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
5 from concurrent.futures import ThreadPoolExecutor
6 from contextlib import contextmanager
7 from dataclasses import replace
9 from io import BytesIO, TextIOWrapper
11 from pathlib import Path
12 from platform import system
15 from tempfile import TemporaryDirectory
29 from unittest.mock import patch, MagicMock
32 from click import unstyle
33 from click.testing import CliRunner
36 from black import Feature, TargetVersion
37 from black.cache import get_cache_file
38 from black.debug import DebugVisitor
39 from black.report import Report
42 from pathspec import PathSpec
44 # Import other test classes
45 from tests.util import (
55 from .test_primer import PrimerCLITests # noqa: F401
58 THIS_FILE = Path(__file__)
65 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
71 def cache_dir(exists: bool = True) -> Iterator[Path]:
72 with TemporaryDirectory() as workspace:
73 cache_dir = Path(workspace)
75 cache_dir = cache_dir / "new"
76 with patch("black.cache.CACHE_DIR", cache_dir):
81 def event_loop() -> Iterator[None]:
82 policy = asyncio.get_event_loop_policy()
83 loop = policy.new_event_loop()
84 asyncio.set_event_loop(loop)
92 class FakeContext(click.Context):
93 """A fake click Context for when calling functions that need it."""
95 def __init__(self) -> None:
96 self.default_map: Dict[str, Any] = {}
99 class FakeParameter(click.Parameter):
100 """A fake click Parameter for when calling functions that need it."""
102 def __init__(self) -> None:
106 class BlackRunner(CliRunner):
107 """Modify CliRunner so that stderr is not merged with stdout.
109 This is a hack that can be removed once we depend on Click 7.x"""
111 def __init__(self) -> None:
112 self.stderrbuf = BytesIO()
113 self.stdoutbuf = BytesIO()
114 self.stdout_bytes = b""
115 self.stderr_bytes = b""
119 def isolation(self, *args: Any, **kwargs: Any) -> Generator[BinaryIO, None, None]:
120 with super().isolation(*args, **kwargs) as output:
122 hold_stderr = sys.stderr
123 sys.stderr = TextIOWrapper(self.stderrbuf, encoding=self.charset)
126 self.stdout_bytes = sys.stdout.buffer.getvalue() # type: ignore
127 self.stderr_bytes = sys.stderr.buffer.getvalue() # type: ignore
128 sys.stderr = hold_stderr
131 class BlackTestCase(BlackBaseTestCase):
133 self, args: List[str], exit_code: int = 0, ignore_config: bool = True
135 runner = BlackRunner()
137 args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
138 result = runner.invoke(black.main, args)
143 f"Failed with args: {args}\n"
144 f"stdout: {runner.stdout_bytes.decode()!r}\n"
145 f"stderr: {runner.stderr_bytes.decode()!r}\n"
146 f"exception: {result.exception}"
150 @patch("black.dump_to_file", dump_to_stderr)
151 def test_empty(self) -> None:
152 source = expected = ""
154 self.assertFormatEqual(expected, actual)
155 black.assert_equivalent(source, actual)
156 black.assert_stable(source, actual, DEFAULT_MODE)
158 def test_empty_ff(self) -> None:
160 tmp_file = Path(black.dump_to_file())
162 self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
163 with open(tmp_file, encoding="utf8") as f:
167 self.assertFormatEqual(expected, actual)
169 def test_piping(self) -> None:
170 source, expected = read_data("src/black/__init__", data=False)
171 result = BlackRunner().invoke(
173 ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
174 input=BytesIO(source.encode("utf8")),
176 self.assertEqual(result.exit_code, 0)
177 self.assertFormatEqual(expected, result.output)
178 if source != result.output:
179 black.assert_equivalent(source, result.output)
180 black.assert_stable(source, result.output, DEFAULT_MODE)
182 def test_piping_diff(self) -> None:
183 diff_header = re.compile(
184 r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
187 source, _ = read_data("expression.py")
188 expected, _ = read_data("expression.diff")
189 config = THIS_DIR / "data" / "empty_pyproject.toml"
193 f"--line-length={black.DEFAULT_LINE_LENGTH}",
195 f"--config={config}",
197 result = BlackRunner().invoke(
198 black.main, args, input=BytesIO(source.encode("utf8"))
200 self.assertEqual(result.exit_code, 0)
201 actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
202 actual = actual.rstrip() + "\n" # the diff output has a trailing space
203 self.assertEqual(expected, actual)
205 def test_piping_diff_with_color(self) -> None:
206 source, _ = read_data("expression.py")
207 config = THIS_DIR / "data" / "empty_pyproject.toml"
211 f"--line-length={black.DEFAULT_LINE_LENGTH}",
214 f"--config={config}",
216 result = BlackRunner().invoke(
217 black.main, args, input=BytesIO(source.encode("utf8"))
219 actual = result.output
220 # Again, the contents are checked in a different test, so only look for colors.
221 self.assertIn("\033[1;37m", actual)
222 self.assertIn("\033[36m", actual)
223 self.assertIn("\033[32m", actual)
224 self.assertIn("\033[31m", actual)
225 self.assertIn("\033[0m", actual)
227 @patch("black.dump_to_file", dump_to_stderr)
228 def _test_wip(self) -> None:
229 source, expected = read_data("wip")
230 sys.settrace(tracefunc)
233 experimental_string_processing=False,
234 target_versions={black.TargetVersion.PY38},
236 actual = fs(source, mode=mode)
238 self.assertFormatEqual(expected, actual)
239 black.assert_equivalent(source, actual)
240 black.assert_stable(source, actual, black.FileMode())
242 @unittest.expectedFailure
243 @patch("black.dump_to_file", dump_to_stderr)
244 def test_trailing_comma_optional_parens_stability1(self) -> None:
245 source, _expected = read_data("trailing_comma_optional_parens1")
247 black.assert_stable(source, actual, DEFAULT_MODE)
249 @unittest.expectedFailure
250 @patch("black.dump_to_file", dump_to_stderr)
251 def test_trailing_comma_optional_parens_stability2(self) -> None:
252 source, _expected = read_data("trailing_comma_optional_parens2")
254 black.assert_stable(source, actual, DEFAULT_MODE)
256 @unittest.expectedFailure
257 @patch("black.dump_to_file", dump_to_stderr)
258 def test_trailing_comma_optional_parens_stability3(self) -> None:
259 source, _expected = read_data("trailing_comma_optional_parens3")
261 black.assert_stable(source, actual, DEFAULT_MODE)
263 @patch("black.dump_to_file", dump_to_stderr)
264 def test_trailing_comma_optional_parens_stability1_pass2(self) -> None:
265 source, _expected = read_data("trailing_comma_optional_parens1")
266 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
267 black.assert_stable(source, actual, DEFAULT_MODE)
269 @patch("black.dump_to_file", dump_to_stderr)
270 def test_trailing_comma_optional_parens_stability2_pass2(self) -> None:
271 source, _expected = read_data("trailing_comma_optional_parens2")
272 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
273 black.assert_stable(source, actual, DEFAULT_MODE)
275 @patch("black.dump_to_file", dump_to_stderr)
276 def test_trailing_comma_optional_parens_stability3_pass2(self) -> None:
277 source, _expected = read_data("trailing_comma_optional_parens3")
278 actual = fs(fs(source)) # this is what `format_file_contents` does with --safe
279 black.assert_stable(source, actual, DEFAULT_MODE)
281 @patch("black.dump_to_file", dump_to_stderr)
282 def test_pep_572(self) -> None:
283 source, expected = read_data("pep_572")
285 self.assertFormatEqual(expected, actual)
286 black.assert_stable(source, actual, DEFAULT_MODE)
287 if sys.version_info >= (3, 8):
288 black.assert_equivalent(source, actual)
290 @patch("black.dump_to_file", dump_to_stderr)
291 def test_pep_572_remove_parens(self) -> None:
292 source, expected = read_data("pep_572_remove_parens")
294 self.assertFormatEqual(expected, actual)
295 black.assert_stable(source, actual, DEFAULT_MODE)
296 if sys.version_info >= (3, 8):
297 black.assert_equivalent(source, actual)
299 @patch("black.dump_to_file", dump_to_stderr)
300 def test_pep_572_do_not_remove_parens(self) -> None:
301 source, expected = read_data("pep_572_do_not_remove_parens")
302 # the AST safety checks will fail, but that's expected, just make sure no
303 # parentheses are touched
304 actual = black.format_str(source, mode=DEFAULT_MODE)
305 self.assertFormatEqual(expected, actual)
307 def test_pep_572_version_detection(self) -> None:
308 source, _ = read_data("pep_572")
309 root = black.lib2to3_parse(source)
310 features = black.get_features_used(root)
311 self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
312 versions = black.detect_target_versions(root)
313 self.assertIn(black.TargetVersion.PY38, versions)
315 def test_expression_ff(self) -> None:
316 source, expected = read_data("expression")
317 tmp_file = Path(black.dump_to_file(source))
319 self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
320 with open(tmp_file, encoding="utf8") as f:
324 self.assertFormatEqual(expected, actual)
325 with patch("black.dump_to_file", dump_to_stderr):
326 black.assert_equivalent(source, actual)
327 black.assert_stable(source, actual, DEFAULT_MODE)
329 def test_expression_diff(self) -> None:
330 source, _ = read_data("expression.py")
331 config = THIS_DIR / "data" / "empty_pyproject.toml"
332 expected, _ = read_data("expression.diff")
333 tmp_file = Path(black.dump_to_file(source))
334 diff_header = re.compile(
335 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
336 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
339 result = BlackRunner().invoke(
340 black.main, ["--diff", str(tmp_file), f"--config={config}"]
342 self.assertEqual(result.exit_code, 0)
345 actual = result.output
346 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
347 if expected != actual:
348 dump = black.dump_to_file(actual)
350 "Expected diff isn't equal to the actual. If you made changes to"
351 " expression.py and this is an anticipated difference, overwrite"
352 f" tests/data/expression.diff with {dump}"
354 self.assertEqual(expected, actual, msg)
356 def test_expression_diff_with_color(self) -> None:
357 source, _ = read_data("expression.py")
358 config = THIS_DIR / "data" / "empty_pyproject.toml"
359 expected, _ = read_data("expression.diff")
360 tmp_file = Path(black.dump_to_file(source))
362 result = BlackRunner().invoke(
363 black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
367 actual = result.output
368 # We check the contents of the diff in `test_expression_diff`. All
369 # we need to check here is that color codes exist in the result.
370 self.assertIn("\033[1;37m", actual)
371 self.assertIn("\033[36m", actual)
372 self.assertIn("\033[32m", actual)
373 self.assertIn("\033[31m", actual)
374 self.assertIn("\033[0m", actual)
376 @patch("black.dump_to_file", dump_to_stderr)
377 def test_pep_570(self) -> None:
378 source, expected = read_data("pep_570")
380 self.assertFormatEqual(expected, actual)
381 black.assert_stable(source, actual, DEFAULT_MODE)
382 if sys.version_info >= (3, 8):
383 black.assert_equivalent(source, actual)
385 def test_detect_pos_only_arguments(self) -> None:
386 source, _ = read_data("pep_570")
387 root = black.lib2to3_parse(source)
388 features = black.get_features_used(root)
389 self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
390 versions = black.detect_target_versions(root)
391 self.assertIn(black.TargetVersion.PY38, versions)
393 @patch("black.dump_to_file", dump_to_stderr)
394 def test_string_quotes(self) -> None:
395 source, expected = read_data("string_quotes")
396 mode = black.Mode(experimental_string_processing=True)
397 actual = fs(source, mode=mode)
398 self.assertFormatEqual(expected, actual)
399 black.assert_equivalent(source, actual)
400 black.assert_stable(source, actual, mode)
401 mode = replace(mode, string_normalization=False)
402 not_normalized = fs(source, mode=mode)
403 self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
404 black.assert_equivalent(source, not_normalized)
405 black.assert_stable(source, not_normalized, mode=mode)
407 @patch("black.dump_to_file", dump_to_stderr)
408 def test_docstring_no_string_normalization(self) -> None:
409 """Like test_docstring but with string normalization off."""
410 source, expected = read_data("docstring_no_string_normalization")
411 mode = replace(DEFAULT_MODE, string_normalization=False)
412 actual = fs(source, mode=mode)
413 self.assertFormatEqual(expected, actual)
414 black.assert_equivalent(source, actual)
415 black.assert_stable(source, actual, mode)
417 def test_long_strings_flag_disabled(self) -> None:
418 """Tests for turning off the string processing logic."""
419 source, expected = read_data("long_strings_flag_disabled")
420 mode = replace(DEFAULT_MODE, experimental_string_processing=False)
421 actual = fs(source, mode=mode)
422 self.assertFormatEqual(expected, actual)
423 black.assert_stable(expected, actual, mode)
425 @patch("black.dump_to_file", dump_to_stderr)
426 def test_numeric_literals(self) -> None:
427 source, expected = read_data("numeric_literals")
428 mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
429 actual = fs(source, mode=mode)
430 self.assertFormatEqual(expected, actual)
431 black.assert_equivalent(source, actual)
432 black.assert_stable(source, actual, mode)
434 @patch("black.dump_to_file", dump_to_stderr)
435 def test_numeric_literals_ignoring_underscores(self) -> None:
436 source, expected = read_data("numeric_literals_skip_underscores")
437 mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
438 actual = fs(source, mode=mode)
439 self.assertFormatEqual(expected, actual)
440 black.assert_equivalent(source, actual)
441 black.assert_stable(source, actual, mode)
443 def test_skip_magic_trailing_comma(self) -> None:
444 source, _ = read_data("expression.py")
445 expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
446 tmp_file = Path(black.dump_to_file(source))
447 diff_header = re.compile(
448 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
449 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
452 result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
453 self.assertEqual(result.exit_code, 0)
456 actual = result.output
457 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
458 actual = actual.rstrip() + "\n" # the diff output has a trailing space
459 if expected != actual:
460 dump = black.dump_to_file(actual)
462 "Expected diff isn't equal to the actual. If you made changes to"
463 " expression.py and this is an anticipated difference, overwrite"
464 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
466 self.assertEqual(expected, actual, msg)
468 @pytest.mark.no_python2
469 def test_python2_should_fail_without_optional_install(self) -> None:
470 if sys.version_info < (3, 8):
472 "Python 3.6 and 3.7 will install typed-ast to work and as such will be"
473 " able to parse Python 2 syntax without explicitly specifying the"
478 tmp_file = Path(black.dump_to_file(source))
480 runner = BlackRunner()
481 result = runner.invoke(black.main, [str(tmp_file)])
482 self.assertEqual(result.exit_code, 123)
486 runner.stderr_bytes.decode()
493 "The requested source code has invalid Python 3 syntax."
494 "If you are trying to format Python 2 files please reinstall Black"
495 " with the 'python2' extra: `python3 -m pip install black[python2]`."
497 self.assertIn(msg, actual)
500 @patch("black.dump_to_file", dump_to_stderr)
501 def test_python2_print_function(self) -> None:
502 source, expected = read_data("python2_print_function")
503 mode = replace(DEFAULT_MODE, target_versions={TargetVersion.PY27})
504 actual = fs(source, mode=mode)
505 self.assertFormatEqual(expected, actual)
506 black.assert_equivalent(source, actual)
507 black.assert_stable(source, actual, mode)
509 @patch("black.dump_to_file", dump_to_stderr)
510 def test_stub(self) -> None:
511 mode = replace(DEFAULT_MODE, is_pyi=True)
512 source, expected = read_data("stub.pyi")
513 actual = fs(source, mode=mode)
514 self.assertFormatEqual(expected, actual)
515 black.assert_stable(source, actual, mode)
517 @patch("black.dump_to_file", dump_to_stderr)
518 def test_async_as_identifier(self) -> None:
519 source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
520 source, expected = read_data("async_as_identifier")
522 self.assertFormatEqual(expected, actual)
523 major, minor = sys.version_info[:2]
524 if major < 3 or (major <= 3 and minor < 7):
525 black.assert_equivalent(source, actual)
526 black.assert_stable(source, actual, DEFAULT_MODE)
527 # ensure black can parse this when the target is 3.6
528 self.invokeBlack([str(source_path), "--target-version", "py36"])
529 # but not on 3.7, because async/await is no longer an identifier
530 self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
532 @patch("black.dump_to_file", dump_to_stderr)
533 def test_python37(self) -> None:
534 source_path = (THIS_DIR / "data" / "python37.py").resolve()
535 source, expected = read_data("python37")
537 self.assertFormatEqual(expected, actual)
538 major, minor = sys.version_info[:2]
539 if major > 3 or (major == 3 and minor >= 7):
540 black.assert_equivalent(source, actual)
541 black.assert_stable(source, actual, DEFAULT_MODE)
542 # ensure black can parse this when the target is 3.7
543 self.invokeBlack([str(source_path), "--target-version", "py37"])
544 # but not on 3.6, because we use async as a reserved keyword
545 self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
547 @patch("black.dump_to_file", dump_to_stderr)
548 def test_python38(self) -> None:
549 source, expected = read_data("python38")
551 self.assertFormatEqual(expected, actual)
552 major, minor = sys.version_info[:2]
553 if major > 3 or (major == 3 and minor >= 8):
554 black.assert_equivalent(source, actual)
555 black.assert_stable(source, actual, DEFAULT_MODE)
557 @patch("black.dump_to_file", dump_to_stderr)
558 def test_python39(self) -> None:
559 source, expected = read_data("python39")
561 self.assertFormatEqual(expected, actual)
562 major, minor = sys.version_info[:2]
563 if major > 3 or (major == 3 and minor >= 9):
564 black.assert_equivalent(source, actual)
565 black.assert_stable(source, actual, DEFAULT_MODE)
567 def test_tab_comment_indentation(self) -> None:
568 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
569 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
570 self.assertFormatEqual(contents_spc, fs(contents_spc))
571 self.assertFormatEqual(contents_spc, fs(contents_tab))
573 contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
574 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
575 self.assertFormatEqual(contents_spc, fs(contents_spc))
576 self.assertFormatEqual(contents_spc, fs(contents_tab))
578 # mixed tabs and spaces (valid Python 2 code)
579 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t# comment\n pass\n"
580 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
581 self.assertFormatEqual(contents_spc, fs(contents_spc))
582 self.assertFormatEqual(contents_spc, fs(contents_tab))
584 contents_tab = "if 1:\n if 2:\n\t\tpass\n\t\t# comment\n pass\n"
585 contents_spc = "if 1:\n if 2:\n pass\n # comment\n pass\n"
586 self.assertFormatEqual(contents_spc, fs(contents_spc))
587 self.assertFormatEqual(contents_spc, fs(contents_tab))
589 def test_report_verbose(self) -> None:
590 report = Report(verbose=True)
594 def out(msg: str, **kwargs: Any) -> None:
595 out_lines.append(msg)
597 def err(msg: str, **kwargs: Any) -> None:
598 err_lines.append(msg)
600 with patch("black.output._out", out), patch("black.output._err", err):
601 report.done(Path("f1"), black.Changed.NO)
602 self.assertEqual(len(out_lines), 1)
603 self.assertEqual(len(err_lines), 0)
604 self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
605 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
606 self.assertEqual(report.return_code, 0)
607 report.done(Path("f2"), black.Changed.YES)
608 self.assertEqual(len(out_lines), 2)
609 self.assertEqual(len(err_lines), 0)
610 self.assertEqual(out_lines[-1], "reformatted f2")
612 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
614 report.done(Path("f3"), black.Changed.CACHED)
615 self.assertEqual(len(out_lines), 3)
616 self.assertEqual(len(err_lines), 0)
618 out_lines[-1], "f3 wasn't modified on disk since last run."
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), 3)
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), 4)
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), 4)
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), 5)
659 self.assertEqual(len(err_lines), 2)
660 self.assertEqual(out_lines[-1], "wat ignored: no match")
662 unstyle(str(report)),
663 "2 files reformatted, 2 files left unchanged, 2 files failed to"
666 self.assertEqual(report.return_code, 123)
667 report.done(Path("f4"), black.Changed.NO)
668 self.assertEqual(len(out_lines), 6)
669 self.assertEqual(len(err_lines), 2)
670 self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
672 unstyle(str(report)),
673 "2 files reformatted, 3 files left unchanged, 2 files failed to"
676 self.assertEqual(report.return_code, 123)
679 unstyle(str(report)),
680 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
681 " would fail to reformat.",
686 unstyle(str(report)),
687 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
688 " would fail to reformat.",
691 def test_report_quiet(self) -> None:
692 report = Report(quiet=True)
696 def out(msg: str, **kwargs: Any) -> None:
697 out_lines.append(msg)
699 def err(msg: str, **kwargs: Any) -> None:
700 err_lines.append(msg)
702 with patch("black.output._out", out), patch("black.output._err", err):
703 report.done(Path("f1"), black.Changed.NO)
704 self.assertEqual(len(out_lines), 0)
705 self.assertEqual(len(err_lines), 0)
706 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
707 self.assertEqual(report.return_code, 0)
708 report.done(Path("f2"), black.Changed.YES)
709 self.assertEqual(len(out_lines), 0)
710 self.assertEqual(len(err_lines), 0)
712 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
714 report.done(Path("f3"), black.Changed.CACHED)
715 self.assertEqual(len(out_lines), 0)
716 self.assertEqual(len(err_lines), 0)
718 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
720 self.assertEqual(report.return_code, 0)
722 self.assertEqual(report.return_code, 1)
724 report.failed(Path("e1"), "boom")
725 self.assertEqual(len(out_lines), 0)
726 self.assertEqual(len(err_lines), 1)
727 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
729 unstyle(str(report)),
730 "1 file reformatted, 2 files left unchanged, 1 file failed to"
733 self.assertEqual(report.return_code, 123)
734 report.done(Path("f3"), black.Changed.YES)
735 self.assertEqual(len(out_lines), 0)
736 self.assertEqual(len(err_lines), 1)
738 unstyle(str(report)),
739 "2 files reformatted, 2 files left unchanged, 1 file failed to"
742 self.assertEqual(report.return_code, 123)
743 report.failed(Path("e2"), "boom")
744 self.assertEqual(len(out_lines), 0)
745 self.assertEqual(len(err_lines), 2)
746 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
748 unstyle(str(report)),
749 "2 files reformatted, 2 files left unchanged, 2 files failed to"
752 self.assertEqual(report.return_code, 123)
753 report.path_ignored(Path("wat"), "no match")
754 self.assertEqual(len(out_lines), 0)
755 self.assertEqual(len(err_lines), 2)
757 unstyle(str(report)),
758 "2 files reformatted, 2 files left unchanged, 2 files failed to"
761 self.assertEqual(report.return_code, 123)
762 report.done(Path("f4"), black.Changed.NO)
763 self.assertEqual(len(out_lines), 0)
764 self.assertEqual(len(err_lines), 2)
766 unstyle(str(report)),
767 "2 files reformatted, 3 files left unchanged, 2 files failed to"
770 self.assertEqual(report.return_code, 123)
773 unstyle(str(report)),
774 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
775 " would fail to reformat.",
780 unstyle(str(report)),
781 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
782 " would fail to reformat.",
785 def test_report_normal(self) -> None:
786 report = black.Report()
790 def out(msg: str, **kwargs: Any) -> None:
791 out_lines.append(msg)
793 def err(msg: str, **kwargs: Any) -> None:
794 err_lines.append(msg)
796 with patch("black.output._out", out), patch("black.output._err", err):
797 report.done(Path("f1"), black.Changed.NO)
798 self.assertEqual(len(out_lines), 0)
799 self.assertEqual(len(err_lines), 0)
800 self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
801 self.assertEqual(report.return_code, 0)
802 report.done(Path("f2"), black.Changed.YES)
803 self.assertEqual(len(out_lines), 1)
804 self.assertEqual(len(err_lines), 0)
805 self.assertEqual(out_lines[-1], "reformatted f2")
807 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
809 report.done(Path("f3"), black.Changed.CACHED)
810 self.assertEqual(len(out_lines), 1)
811 self.assertEqual(len(err_lines), 0)
812 self.assertEqual(out_lines[-1], "reformatted f2")
814 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
816 self.assertEqual(report.return_code, 0)
818 self.assertEqual(report.return_code, 1)
820 report.failed(Path("e1"), "boom")
821 self.assertEqual(len(out_lines), 1)
822 self.assertEqual(len(err_lines), 1)
823 self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
825 unstyle(str(report)),
826 "1 file reformatted, 2 files left unchanged, 1 file failed to"
829 self.assertEqual(report.return_code, 123)
830 report.done(Path("f3"), black.Changed.YES)
831 self.assertEqual(len(out_lines), 2)
832 self.assertEqual(len(err_lines), 1)
833 self.assertEqual(out_lines[-1], "reformatted f3")
835 unstyle(str(report)),
836 "2 files reformatted, 2 files left unchanged, 1 file failed to"
839 self.assertEqual(report.return_code, 123)
840 report.failed(Path("e2"), "boom")
841 self.assertEqual(len(out_lines), 2)
842 self.assertEqual(len(err_lines), 2)
843 self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
845 unstyle(str(report)),
846 "2 files reformatted, 2 files left unchanged, 2 files failed to"
849 self.assertEqual(report.return_code, 123)
850 report.path_ignored(Path("wat"), "no match")
851 self.assertEqual(len(out_lines), 2)
852 self.assertEqual(len(err_lines), 2)
854 unstyle(str(report)),
855 "2 files reformatted, 2 files left unchanged, 2 files failed to"
858 self.assertEqual(report.return_code, 123)
859 report.done(Path("f4"), black.Changed.NO)
860 self.assertEqual(len(out_lines), 2)
861 self.assertEqual(len(err_lines), 2)
863 unstyle(str(report)),
864 "2 files reformatted, 3 files left unchanged, 2 files failed to"
867 self.assertEqual(report.return_code, 123)
870 unstyle(str(report)),
871 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
872 " would fail to reformat.",
877 unstyle(str(report)),
878 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
879 " would fail to reformat.",
882 def test_lib2to3_parse(self) -> None:
883 with self.assertRaises(black.InvalidInput):
884 black.lib2to3_parse("invalid syntax")
887 black.lib2to3_parse(straddling)
888 black.lib2to3_parse(straddling, {TargetVersion.PY27})
889 black.lib2to3_parse(straddling, {TargetVersion.PY36})
890 black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36})
893 black.lib2to3_parse(py2_only)
894 black.lib2to3_parse(py2_only, {TargetVersion.PY27})
895 with self.assertRaises(black.InvalidInput):
896 black.lib2to3_parse(py2_only, {TargetVersion.PY36})
897 with self.assertRaises(black.InvalidInput):
898 black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36})
900 py3_only = "exec(x, end=y)"
901 black.lib2to3_parse(py3_only)
902 with self.assertRaises(black.InvalidInput):
903 black.lib2to3_parse(py3_only, {TargetVersion.PY27})
904 black.lib2to3_parse(py3_only, {TargetVersion.PY36})
905 black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36})
907 def test_get_features_used_decorator(self) -> None:
908 # Test the feature detection of new decorator syntax
909 # since this makes some test cases of test_get_features_used()
910 # fails if it fails, this is tested first so that a useful case
912 simples, relaxed = read_data("decorators")
913 # skip explanation comments at the top of the file
914 for simple_test in simples.split("##")[1:]:
915 node = black.lib2to3_parse(simple_test)
916 decorator = str(node.children[0].children[0]).strip()
918 Feature.RELAXED_DECORATORS,
919 black.get_features_used(node),
921 f"decorator '{decorator}' follows python<=3.8 syntax"
922 "but is detected as 3.9+"
923 # f"The full node is\n{node!r}"
926 # skip the '# output' comment at the top of the output part
927 for relaxed_test in relaxed.split("##")[1:]:
928 node = black.lib2to3_parse(relaxed_test)
929 decorator = str(node.children[0].children[0]).strip()
931 Feature.RELAXED_DECORATORS,
932 black.get_features_used(node),
934 f"decorator '{decorator}' uses python3.9+ syntax"
935 "but is detected as python<=3.8"
936 # f"The full node is\n{node!r}"
940 def test_get_features_used(self) -> None:
941 node = black.lib2to3_parse("def f(*, arg): ...\n")
942 self.assertEqual(black.get_features_used(node), set())
943 node = black.lib2to3_parse("def f(*, arg,): ...\n")
944 self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
945 node = black.lib2to3_parse("f(*arg,)\n")
947 black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
949 node = black.lib2to3_parse("def f(*, arg): f'string'\n")
950 self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
951 node = black.lib2to3_parse("123_456\n")
952 self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
953 node = black.lib2to3_parse("123456\n")
954 self.assertEqual(black.get_features_used(node), set())
955 source, expected = read_data("function")
956 node = black.lib2to3_parse(source)
957 expected_features = {
958 Feature.TRAILING_COMMA_IN_CALL,
959 Feature.TRAILING_COMMA_IN_DEF,
962 self.assertEqual(black.get_features_used(node), expected_features)
963 node = black.lib2to3_parse(expected)
964 self.assertEqual(black.get_features_used(node), expected_features)
965 source, expected = read_data("expression")
966 node = black.lib2to3_parse(source)
967 self.assertEqual(black.get_features_used(node), set())
968 node = black.lib2to3_parse(expected)
969 self.assertEqual(black.get_features_used(node), set())
971 def test_get_future_imports(self) -> None:
972 node = black.lib2to3_parse("\n")
973 self.assertEqual(set(), black.get_future_imports(node))
974 node = black.lib2to3_parse("from __future__ import black\n")
975 self.assertEqual({"black"}, black.get_future_imports(node))
976 node = black.lib2to3_parse("from __future__ import multiple, imports\n")
977 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
978 node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
979 self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
980 node = black.lib2to3_parse(
981 "from __future__ import multiple\nfrom __future__ import imports\n"
983 self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
984 node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
985 self.assertEqual({"black"}, black.get_future_imports(node))
986 node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
987 self.assertEqual({"black"}, black.get_future_imports(node))
988 node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
989 self.assertEqual(set(), black.get_future_imports(node))
990 node = black.lib2to3_parse("from some.module import black\n")
991 self.assertEqual(set(), black.get_future_imports(node))
992 node = black.lib2to3_parse(
993 "from __future__ import unicode_literals as _unicode_literals"
995 self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
996 node = black.lib2to3_parse(
997 "from __future__ import unicode_literals as _lol, print"
999 self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
1001 def test_debug_visitor(self) -> None:
1002 source, _ = read_data("debug_visitor.py")
1003 expected, _ = read_data("debug_visitor.out")
1007 def out(msg: str, **kwargs: Any) -> None:
1008 out_lines.append(msg)
1010 def err(msg: str, **kwargs: Any) -> None:
1011 err_lines.append(msg)
1013 with patch("black.debug.out", out):
1014 DebugVisitor.show(source)
1015 actual = "\n".join(out_lines) + "\n"
1017 if expected != actual:
1018 log_name = black.dump_to_file(*out_lines)
1022 f"AST print out is different. Actual version dumped to {log_name}",
1025 def test_format_file_contents(self) -> None:
1028 with self.assertRaises(black.NothingChanged):
1029 black.format_file_contents(empty, mode=mode, fast=False)
1031 with self.assertRaises(black.NothingChanged):
1032 black.format_file_contents(just_nl, mode=mode, fast=False)
1033 same = "j = [1, 2, 3]\n"
1034 with self.assertRaises(black.NothingChanged):
1035 black.format_file_contents(same, mode=mode, fast=False)
1036 different = "j = [1,2,3]"
1038 actual = black.format_file_contents(different, mode=mode, fast=False)
1039 self.assertEqual(expected, actual)
1040 invalid = "return if you can"
1041 with self.assertRaises(black.InvalidInput) as e:
1042 black.format_file_contents(invalid, mode=mode, fast=False)
1043 self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
1045 def test_endmarker(self) -> None:
1046 n = black.lib2to3_parse("\n")
1047 self.assertEqual(n.type, black.syms.file_input)
1048 self.assertEqual(len(n.children), 1)
1049 self.assertEqual(n.children[0].type, black.token.ENDMARKER)
1051 @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
1052 def test_assertFormatEqual(self) -> None:
1056 def out(msg: str, **kwargs: Any) -> None:
1057 out_lines.append(msg)
1059 def err(msg: str, **kwargs: Any) -> None:
1060 err_lines.append(msg)
1062 with patch("black.output._out", out), patch("black.output._err", err):
1063 with self.assertRaises(AssertionError):
1064 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
1066 out_str = "".join(out_lines)
1067 self.assertTrue("Expected tree:" in out_str)
1068 self.assertTrue("Actual tree:" in out_str)
1069 self.assertEqual("".join(err_lines), "")
1071 def test_cache_broken_file(self) -> None:
1073 with cache_dir() as workspace:
1074 cache_file = get_cache_file(mode)
1075 with cache_file.open("w") as fobj:
1076 fobj.write("this is not a pickle")
1077 self.assertEqual(black.read_cache(mode), {})
1078 src = (workspace / "test.py").resolve()
1079 with src.open("w") as fobj:
1080 fobj.write("print('hello')")
1081 self.invokeBlack([str(src)])
1082 cache = black.read_cache(mode)
1083 self.assertIn(str(src), cache)
1085 def test_cache_single_file_already_cached(self) -> None:
1087 with cache_dir() as workspace:
1088 src = (workspace / "test.py").resolve()
1089 with src.open("w") as fobj:
1090 fobj.write("print('hello')")
1091 black.write_cache({}, [src], mode)
1092 self.invokeBlack([str(src)])
1093 with src.open("r") as fobj:
1094 self.assertEqual(fobj.read(), "print('hello')")
1097 def test_cache_multiple_files(self) -> None:
1099 with cache_dir() as workspace, patch(
1100 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1102 one = (workspace / "one.py").resolve()
1103 with one.open("w") as fobj:
1104 fobj.write("print('hello')")
1105 two = (workspace / "two.py").resolve()
1106 with two.open("w") as fobj:
1107 fobj.write("print('hello')")
1108 black.write_cache({}, [one], mode)
1109 self.invokeBlack([str(workspace)])
1110 with one.open("r") as fobj:
1111 self.assertEqual(fobj.read(), "print('hello')")
1112 with two.open("r") as fobj:
1113 self.assertEqual(fobj.read(), 'print("hello")\n')
1114 cache = black.read_cache(mode)
1115 self.assertIn(str(one), cache)
1116 self.assertIn(str(two), cache)
1118 def test_no_cache_when_writeback_diff(self) -> None:
1120 with cache_dir() as workspace:
1121 src = (workspace / "test.py").resolve()
1122 with src.open("w") as fobj:
1123 fobj.write("print('hello')")
1124 with patch("black.read_cache") as read_cache, patch(
1127 self.invokeBlack([str(src), "--diff"])
1128 cache_file = get_cache_file(mode)
1129 self.assertFalse(cache_file.exists())
1130 write_cache.assert_not_called()
1131 read_cache.assert_not_called()
1133 def test_no_cache_when_writeback_color_diff(self) -> None:
1135 with cache_dir() as workspace:
1136 src = (workspace / "test.py").resolve()
1137 with src.open("w") as fobj:
1138 fobj.write("print('hello')")
1139 with patch("black.read_cache") as read_cache, patch(
1142 self.invokeBlack([str(src), "--diff", "--color"])
1143 cache_file = get_cache_file(mode)
1144 self.assertFalse(cache_file.exists())
1145 write_cache.assert_not_called()
1146 read_cache.assert_not_called()
1149 def test_output_locking_when_writeback_diff(self) -> None:
1150 with cache_dir() as workspace:
1151 for tag in range(0, 4):
1152 src = (workspace / f"test{tag}.py").resolve()
1153 with src.open("w") as fobj:
1154 fobj.write("print('hello')")
1155 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1156 self.invokeBlack(["--diff", str(workspace)], exit_code=0)
1157 # this isn't quite doing what we want, but if it _isn't_
1158 # called then we cannot be using the lock it provides
1162 def test_output_locking_when_writeback_color_diff(self) -> None:
1163 with cache_dir() as workspace:
1164 for tag in range(0, 4):
1165 src = (workspace / f"test{tag}.py").resolve()
1166 with src.open("w") as fobj:
1167 fobj.write("print('hello')")
1168 with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1169 self.invokeBlack(["--diff", "--color", str(workspace)], exit_code=0)
1170 # this isn't quite doing what we want, but if it _isn't_
1171 # called then we cannot be using the lock it provides
1174 def test_no_cache_when_stdin(self) -> None:
1177 result = CliRunner().invoke(
1178 black.main, ["-"], input=BytesIO(b"print('hello')")
1180 self.assertEqual(result.exit_code, 0)
1181 cache_file = get_cache_file(mode)
1182 self.assertFalse(cache_file.exists())
1184 def test_read_cache_no_cachefile(self) -> None:
1187 self.assertEqual(black.read_cache(mode), {})
1189 def test_write_cache_read_cache(self) -> None:
1191 with cache_dir() as workspace:
1192 src = (workspace / "test.py").resolve()
1194 black.write_cache({}, [src], mode)
1195 cache = black.read_cache(mode)
1196 self.assertIn(str(src), cache)
1197 self.assertEqual(cache[str(src)], black.get_cache_info(src))
1199 def test_filter_cached(self) -> None:
1200 with TemporaryDirectory() as workspace:
1201 path = Path(workspace)
1202 uncached = (path / "uncached").resolve()
1203 cached = (path / "cached").resolve()
1204 cached_but_changed = (path / "changed").resolve()
1207 cached_but_changed.touch()
1209 str(cached): black.get_cache_info(cached),
1210 str(cached_but_changed): (0.0, 0),
1212 todo, done = black.filter_cached(
1213 cache, {uncached, cached, cached_but_changed}
1215 self.assertEqual(todo, {uncached, cached_but_changed})
1216 self.assertEqual(done, {cached})
1218 def test_write_cache_creates_directory_if_needed(self) -> None:
1220 with cache_dir(exists=False) as workspace:
1221 self.assertFalse(workspace.exists())
1222 black.write_cache({}, [], mode)
1223 self.assertTrue(workspace.exists())
1226 def test_failed_formatting_does_not_get_cached(self) -> None:
1228 with cache_dir() as workspace, patch(
1229 "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1231 failing = (workspace / "failing.py").resolve()
1232 with failing.open("w") as fobj:
1233 fobj.write("not actually python")
1234 clean = (workspace / "clean.py").resolve()
1235 with clean.open("w") as fobj:
1236 fobj.write('print("hello")\n')
1237 self.invokeBlack([str(workspace)], exit_code=123)
1238 cache = black.read_cache(mode)
1239 self.assertNotIn(str(failing), cache)
1240 self.assertIn(str(clean), cache)
1242 def test_write_cache_write_fail(self) -> None:
1244 with cache_dir(), patch.object(Path, "open") as mock:
1245 mock.side_effect = OSError
1246 black.write_cache({}, [], mode)
1249 @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1250 def test_works_in_mono_process_only_environment(self) -> None:
1251 with cache_dir() as workspace:
1253 (workspace / "one.py").resolve(),
1254 (workspace / "two.py").resolve(),
1256 f.write_text('print("hello")\n')
1257 self.invokeBlack([str(workspace)])
1260 def test_check_diff_use_together(self) -> None:
1262 # Files which will be reformatted.
1263 src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
1264 self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1265 # Files which will not be reformatted.
1266 src2 = (THIS_DIR / "data" / "composition.py").resolve()
1267 self.invokeBlack([str(src2), "--diff", "--check"])
1268 # Multi file command.
1269 self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1271 def test_no_files(self) -> None:
1273 # Without an argument, black exits with error code 0.
1274 self.invokeBlack([])
1276 def test_broken_symlink(self) -> None:
1277 with cache_dir() as workspace:
1278 symlink = workspace / "broken_link.py"
1280 symlink.symlink_to("nonexistent.py")
1281 except OSError as e:
1282 self.skipTest(f"Can't create symlinks: {e}")
1283 self.invokeBlack([str(workspace.resolve())])
1285 def test_read_cache_line_lengths(self) -> None:
1287 short_mode = replace(DEFAULT_MODE, line_length=1)
1288 with cache_dir() as workspace:
1289 path = (workspace / "file.py").resolve()
1291 black.write_cache({}, [path], mode)
1292 one = black.read_cache(mode)
1293 self.assertIn(str(path), one)
1294 two = black.read_cache(short_mode)
1295 self.assertNotIn(str(path), two)
1297 def test_single_file_force_pyi(self) -> None:
1298 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1299 contents, expected = read_data("force_pyi")
1300 with cache_dir() as workspace:
1301 path = (workspace / "file.py").resolve()
1302 with open(path, "w") as fh:
1304 self.invokeBlack([str(path), "--pyi"])
1305 with open(path, "r") as fh:
1307 # verify cache with --pyi is separate
1308 pyi_cache = black.read_cache(pyi_mode)
1309 self.assertIn(str(path), pyi_cache)
1310 normal_cache = black.read_cache(DEFAULT_MODE)
1311 self.assertNotIn(str(path), normal_cache)
1312 self.assertFormatEqual(expected, actual)
1313 black.assert_equivalent(contents, actual)
1314 black.assert_stable(contents, actual, pyi_mode)
1317 def test_multi_file_force_pyi(self) -> None:
1318 reg_mode = DEFAULT_MODE
1319 pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1320 contents, expected = read_data("force_pyi")
1321 with cache_dir() as workspace:
1323 (workspace / "file1.py").resolve(),
1324 (workspace / "file2.py").resolve(),
1327 with open(path, "w") as fh:
1329 self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1331 with open(path, "r") as fh:
1333 self.assertEqual(actual, expected)
1334 # verify cache with --pyi is separate
1335 pyi_cache = black.read_cache(pyi_mode)
1336 normal_cache = black.read_cache(reg_mode)
1338 self.assertIn(str(path), pyi_cache)
1339 self.assertNotIn(str(path), normal_cache)
1341 def test_pipe_force_pyi(self) -> None:
1342 source, expected = read_data("force_pyi")
1343 result = CliRunner().invoke(
1344 black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1346 self.assertEqual(result.exit_code, 0)
1347 actual = result.output
1348 self.assertFormatEqual(actual, expected)
1350 def test_single_file_force_py36(self) -> None:
1351 reg_mode = DEFAULT_MODE
1352 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1353 source, expected = read_data("force_py36")
1354 with cache_dir() as workspace:
1355 path = (workspace / "file.py").resolve()
1356 with open(path, "w") as fh:
1358 self.invokeBlack([str(path), *PY36_ARGS])
1359 with open(path, "r") as fh:
1361 # verify cache with --target-version is separate
1362 py36_cache = black.read_cache(py36_mode)
1363 self.assertIn(str(path), py36_cache)
1364 normal_cache = black.read_cache(reg_mode)
1365 self.assertNotIn(str(path), normal_cache)
1366 self.assertEqual(actual, expected)
1369 def test_multi_file_force_py36(self) -> None:
1370 reg_mode = DEFAULT_MODE
1371 py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1372 source, expected = read_data("force_py36")
1373 with cache_dir() as workspace:
1375 (workspace / "file1.py").resolve(),
1376 (workspace / "file2.py").resolve(),
1379 with open(path, "w") as fh:
1381 self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1383 with open(path, "r") as fh:
1385 self.assertEqual(actual, expected)
1386 # verify cache with --target-version is separate
1387 pyi_cache = black.read_cache(py36_mode)
1388 normal_cache = black.read_cache(reg_mode)
1390 self.assertIn(str(path), pyi_cache)
1391 self.assertNotIn(str(path), normal_cache)
1393 def test_pipe_force_py36(self) -> None:
1394 source, expected = read_data("force_py36")
1395 result = CliRunner().invoke(
1397 ["-", "-q", "--target-version=py36"],
1398 input=BytesIO(source.encode("utf8")),
1400 self.assertEqual(result.exit_code, 0)
1401 actual = result.output
1402 self.assertFormatEqual(actual, expected)
1404 def test_include_exclude(self) -> None:
1405 path = THIS_DIR / "data" / "include_exclude_tests"
1406 include = re.compile(r"\.pyi?$")
1407 exclude = re.compile(r"/exclude/|/\.definitely_exclude/")
1408 report = black.Report()
1409 gitignore = PathSpec.from_lines("gitwildmatch", [])
1410 sources: List[Path] = []
1412 Path(path / "b/dont_exclude/a.py"),
1413 Path(path / "b/dont_exclude/a.pyi"),
1415 this_abs = THIS_DIR.resolve()
1417 black.gen_python_files(
1428 self.assertEqual(sorted(expected), sorted(sources))
1430 def test_gitingore_used_as_default(self) -> None:
1431 path = Path(THIS_DIR / "data" / "include_exclude_tests")
1432 include = re.compile(r"\.pyi?$")
1433 extend_exclude = re.compile(r"/exclude/")
1434 src = str(path / "b/")
1435 report = black.Report()
1436 expected: List[Path] = [
1437 path / "b/.definitely_exclude/a.py",
1438 path / "b/.definitely_exclude/a.pyi",
1448 extend_exclude=extend_exclude,
1451 stdin_filename=None,
1454 self.assertEqual(sorted(expected), sorted(sources))
1456 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1457 def test_exclude_for_issue_1572(self) -> None:
1458 # Exclude shouldn't touch files that were explicitly given to Black through the
1459 # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1460 # https://github.com/psf/black/issues/1572
1461 path = THIS_DIR / "data" / "include_exclude_tests"
1463 exclude = r"/exclude/|a\.py"
1464 src = str(path / "b/exclude/a.py")
1465 report = black.Report()
1466 expected = [Path(path / "b/exclude/a.py")]
1473 include=re.compile(include),
1474 exclude=re.compile(exclude),
1475 extend_exclude=None,
1478 stdin_filename=None,
1481 self.assertEqual(sorted(expected), sorted(sources))
1483 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1484 def test_get_sources_with_stdin(self) -> None:
1486 exclude = r"/exclude/|a\.py"
1488 report = black.Report()
1489 expected = [Path("-")]
1496 include=re.compile(include),
1497 exclude=re.compile(exclude),
1498 extend_exclude=None,
1501 stdin_filename=None,
1504 self.assertEqual(sorted(expected), sorted(sources))
1506 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1507 def test_get_sources_with_stdin_filename(self) -> None:
1509 exclude = r"/exclude/|a\.py"
1511 report = black.Report()
1512 stdin_filename = str(THIS_DIR / "data/collections.py")
1513 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1520 include=re.compile(include),
1521 exclude=re.compile(exclude),
1522 extend_exclude=None,
1525 stdin_filename=stdin_filename,
1528 self.assertEqual(sorted(expected), sorted(sources))
1530 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1531 def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
1532 # Exclude shouldn't exclude stdin_filename since it is mimicing the
1533 # file being passed directly. This is the same as
1534 # test_exclude_for_issue_1572
1535 path = THIS_DIR / "data" / "include_exclude_tests"
1537 exclude = r"/exclude/|a\.py"
1539 report = black.Report()
1540 stdin_filename = str(path / "b/exclude/a.py")
1541 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1548 include=re.compile(include),
1549 exclude=re.compile(exclude),
1550 extend_exclude=None,
1553 stdin_filename=stdin_filename,
1556 self.assertEqual(sorted(expected), sorted(sources))
1558 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1559 def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
1560 # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
1561 # file being passed directly. This is the same as
1562 # test_exclude_for_issue_1572
1563 path = THIS_DIR / "data" / "include_exclude_tests"
1565 extend_exclude = r"/exclude/|a\.py"
1567 report = black.Report()
1568 stdin_filename = str(path / "b/exclude/a.py")
1569 expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1576 include=re.compile(include),
1577 exclude=re.compile(""),
1578 extend_exclude=re.compile(extend_exclude),
1581 stdin_filename=stdin_filename,
1584 self.assertEqual(sorted(expected), sorted(sources))
1586 @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1587 def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
1588 # Force exclude should exclude the file when passing it through
1590 path = THIS_DIR / "data" / "include_exclude_tests"
1592 force_exclude = r"/exclude/|a\.py"
1594 report = black.Report()
1595 stdin_filename = str(path / "b/exclude/a.py")
1602 include=re.compile(include),
1603 exclude=re.compile(""),
1604 extend_exclude=None,
1605 force_exclude=re.compile(force_exclude),
1607 stdin_filename=stdin_filename,
1610 self.assertEqual([], sorted(sources))
1612 def test_reformat_one_with_stdin(self) -> None:
1614 "black.format_stdin_to_stdout",
1615 return_value=lambda *args, **kwargs: black.Changed.YES,
1617 report = MagicMock()
1622 write_back=black.WriteBack.YES,
1626 fsts.assert_called_once()
1627 report.done.assert_called_with(path, black.Changed.YES)
1629 def test_reformat_one_with_stdin_filename(self) -> None:
1631 "black.format_stdin_to_stdout",
1632 return_value=lambda *args, **kwargs: black.Changed.YES,
1634 report = MagicMock()
1636 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1641 write_back=black.WriteBack.YES,
1645 fsts.assert_called_once_with(
1646 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1648 # __BLACK_STDIN_FILENAME__ should have been stripped
1649 report.done.assert_called_with(expected, black.Changed.YES)
1651 def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1653 "black.format_stdin_to_stdout",
1654 return_value=lambda *args, **kwargs: black.Changed.YES,
1656 report = MagicMock()
1658 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1663 write_back=black.WriteBack.YES,
1667 fsts.assert_called_once_with(
1669 write_back=black.WriteBack.YES,
1670 mode=replace(DEFAULT_MODE, is_pyi=True),
1672 # __BLACK_STDIN_FILENAME__ should have been stripped
1673 report.done.assert_called_with(expected, black.Changed.YES)
1675 def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1677 "black.format_stdin_to_stdout",
1678 return_value=lambda *args, **kwargs: black.Changed.YES,
1680 report = MagicMock()
1681 # Even with an existing file, since we are forcing stdin, black
1682 # should output to stdout and not modify the file inplace
1683 p = Path(str(THIS_DIR / "data/collections.py"))
1684 # Make sure is_file actually returns True
1685 self.assertTrue(p.is_file())
1686 path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1691 write_back=black.WriteBack.YES,
1695 fsts.assert_called_once()
1696 # __BLACK_STDIN_FILENAME__ should have been stripped
1697 report.done.assert_called_with(expected, black.Changed.YES)
1699 def test_gitignore_exclude(self) -> None:
1700 path = THIS_DIR / "data" / "include_exclude_tests"
1701 include = re.compile(r"\.pyi?$")
1702 exclude = re.compile(r"")
1703 report = black.Report()
1704 gitignore = PathSpec.from_lines(
1705 "gitwildmatch", ["exclude/", ".definitely_exclude"]
1707 sources: List[Path] = []
1709 Path(path / "b/dont_exclude/a.py"),
1710 Path(path / "b/dont_exclude/a.pyi"),
1712 this_abs = THIS_DIR.resolve()
1714 black.gen_python_files(
1725 self.assertEqual(sorted(expected), sorted(sources))
1727 def test_empty_include(self) -> None:
1728 path = THIS_DIR / "data" / "include_exclude_tests"
1729 report = black.Report()
1730 gitignore = PathSpec.from_lines("gitwildmatch", [])
1731 empty = re.compile(r"")
1732 sources: List[Path] = []
1734 Path(path / "b/exclude/a.pie"),
1735 Path(path / "b/exclude/a.py"),
1736 Path(path / "b/exclude/a.pyi"),
1737 Path(path / "b/dont_exclude/a.pie"),
1738 Path(path / "b/dont_exclude/a.py"),
1739 Path(path / "b/dont_exclude/a.pyi"),
1740 Path(path / "b/.definitely_exclude/a.pie"),
1741 Path(path / "b/.definitely_exclude/a.py"),
1742 Path(path / "b/.definitely_exclude/a.pyi"),
1743 Path(path / ".gitignore"),
1744 Path(path / "pyproject.toml"),
1746 this_abs = THIS_DIR.resolve()
1748 black.gen_python_files(
1752 re.compile(black.DEFAULT_EXCLUDES),
1759 self.assertEqual(sorted(expected), sorted(sources))
1761 def test_extend_exclude(self) -> None:
1762 path = THIS_DIR / "data" / "include_exclude_tests"
1763 report = black.Report()
1764 gitignore = PathSpec.from_lines("gitwildmatch", [])
1765 sources: List[Path] = []
1767 Path(path / "b/exclude/a.py"),
1768 Path(path / "b/dont_exclude/a.py"),
1770 this_abs = THIS_DIR.resolve()
1772 black.gen_python_files(
1775 re.compile(black.DEFAULT_INCLUDES),
1776 re.compile(r"\.pyi$"),
1777 re.compile(r"\.definitely_exclude"),
1783 self.assertEqual(sorted(expected), sorted(sources))
1785 def test_invalid_cli_regex(self) -> None:
1786 for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1787 self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1789 def test_preserves_line_endings(self) -> None:
1790 with TemporaryDirectory() as workspace:
1791 test_file = Path(workspace) / "test.py"
1792 for nl in ["\n", "\r\n"]:
1793 contents = nl.join(["def f( ):", " pass"])
1794 test_file.write_bytes(contents.encode())
1795 ff(test_file, write_back=black.WriteBack.YES)
1796 updated_contents: bytes = test_file.read_bytes()
1797 self.assertIn(nl.encode(), updated_contents)
1799 self.assertNotIn(b"\r\n", updated_contents)
1801 def test_preserves_line_endings_via_stdin(self) -> None:
1802 for nl in ["\n", "\r\n"]:
1803 contents = nl.join(["def f( ):", " pass"])
1804 runner = BlackRunner()
1805 result = runner.invoke(
1806 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1808 self.assertEqual(result.exit_code, 0)
1809 output = runner.stdout_bytes
1810 self.assertIn(nl.encode("utf8"), output)
1812 self.assertNotIn(b"\r\n", output)
1814 def test_assert_equivalent_different_asts(self) -> None:
1815 with self.assertRaises(AssertionError):
1816 black.assert_equivalent("{}", "None")
1818 def test_symlink_out_of_root_directory(self) -> None:
1820 root = THIS_DIR.resolve()
1822 include = re.compile(black.DEFAULT_INCLUDES)
1823 exclude = re.compile(black.DEFAULT_EXCLUDES)
1824 report = black.Report()
1825 gitignore = PathSpec.from_lines("gitwildmatch", [])
1826 # `child` should behave like a symlink which resolved path is clearly
1827 # outside of the `root` directory.
1828 path.iterdir.return_value = [child]
1829 child.resolve.return_value = Path("/a/b/c")
1830 child.as_posix.return_value = "/a/b/c"
1831 child.is_symlink.return_value = True
1834 black.gen_python_files(
1845 except ValueError as ve:
1846 self.fail(f"`get_python_files_in_dir()` failed: {ve}")
1847 path.iterdir.assert_called_once()
1848 child.resolve.assert_called_once()
1849 child.is_symlink.assert_called_once()
1850 # `child` should behave like a strange file which resolved path is clearly
1851 # outside of the `root` directory.
1852 child.is_symlink.return_value = False
1853 with self.assertRaises(ValueError):
1855 black.gen_python_files(
1866 path.iterdir.assert_called()
1867 self.assertEqual(path.iterdir.call_count, 2)
1868 child.resolve.assert_called()
1869 self.assertEqual(child.resolve.call_count, 2)
1870 child.is_symlink.assert_called()
1871 self.assertEqual(child.is_symlink.call_count, 2)
1873 def test_shhh_click(self) -> None:
1875 from click import _unicodefun # type: ignore
1876 except ModuleNotFoundError:
1877 self.skipTest("Incompatible Click version")
1878 if not hasattr(_unicodefun, "_verify_python3_env"):
1879 self.skipTest("Incompatible Click version")
1880 # First, let's see if Click is crashing with a preferred ASCII charset.
1881 with patch("locale.getpreferredencoding") as gpe:
1882 gpe.return_value = "ASCII"
1883 with self.assertRaises(RuntimeError):
1884 _unicodefun._verify_python3_env()
1885 # Now, let's silence Click...
1887 # ...and confirm it's silent.
1888 with patch("locale.getpreferredencoding") as gpe:
1889 gpe.return_value = "ASCII"
1891 _unicodefun._verify_python3_env()
1892 except RuntimeError as re:
1893 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1895 def test_root_logger_not_used_directly(self) -> None:
1896 def fail(*args: Any, **kwargs: Any) -> None:
1897 self.fail("Record created with root logger")
1899 with patch.multiple(
1908 ff(THIS_DIR / "util.py")
1910 def test_invalid_config_return_code(self) -> None:
1911 tmp_file = Path(black.dump_to_file())
1913 tmp_config = Path(black.dump_to_file())
1915 args = ["--config", str(tmp_config), str(tmp_file)]
1916 self.invokeBlack(args, exit_code=2, ignore_config=False)
1920 def test_parse_pyproject_toml(self) -> None:
1921 test_toml_file = THIS_DIR / "test.toml"
1922 config = black.parse_pyproject_toml(str(test_toml_file))
1923 self.assertEqual(config["verbose"], 1)
1924 self.assertEqual(config["check"], "no")
1925 self.assertEqual(config["diff"], "y")
1926 self.assertEqual(config["color"], True)
1927 self.assertEqual(config["line_length"], 79)
1928 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1929 self.assertEqual(config["exclude"], r"\.pyi?$")
1930 self.assertEqual(config["include"], r"\.py?$")
1932 def test_read_pyproject_toml(self) -> None:
1933 test_toml_file = THIS_DIR / "test.toml"
1934 fake_ctx = FakeContext()
1935 black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1936 config = fake_ctx.default_map
1937 self.assertEqual(config["verbose"], "1")
1938 self.assertEqual(config["check"], "no")
1939 self.assertEqual(config["diff"], "y")
1940 self.assertEqual(config["color"], "True")
1941 self.assertEqual(config["line_length"], "79")
1942 self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1943 self.assertEqual(config["exclude"], r"\.pyi?$")
1944 self.assertEqual(config["include"], r"\.py?$")
1946 def test_find_project_root(self) -> None:
1947 with TemporaryDirectory() as workspace:
1948 root = Path(workspace)
1949 test_dir = root / "test"
1952 src_dir = root / "src"
1955 root_pyproject = root / "pyproject.toml"
1956 root_pyproject.touch()
1957 src_pyproject = src_dir / "pyproject.toml"
1958 src_pyproject.touch()
1959 src_python = src_dir / "foo.py"
1963 black.find_project_root((src_dir, test_dir)), root.resolve()
1965 self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1966 self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1969 "black.files.find_user_pyproject_toml",
1970 black.files.find_user_pyproject_toml.__wrapped__,
1972 def test_find_user_pyproject_toml_linux(self) -> None:
1973 if system() == "Windows":
1976 # Test if XDG_CONFIG_HOME is checked
1977 with TemporaryDirectory() as workspace:
1978 tmp_user_config = Path(workspace) / "black"
1979 with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1981 black.files.find_user_pyproject_toml(), tmp_user_config.resolve()
1984 # Test fallback for XDG_CONFIG_HOME
1985 with patch.dict("os.environ"):
1986 os.environ.pop("XDG_CONFIG_HOME", None)
1987 fallback_user_config = Path("~/.config").expanduser() / "black"
1989 black.files.find_user_pyproject_toml(), fallback_user_config.resolve()
1992 def test_find_user_pyproject_toml_windows(self) -> None:
1993 if system() != "Windows":
1996 user_config_path = Path.home() / ".black"
1998 black.files.find_user_pyproject_toml(), user_config_path.resolve()
2001 def test_bpo_33660_workaround(self) -> None:
2002 if system() == "Windows":
2005 # https://bugs.python.org/issue33660
2007 old_cwd = Path.cwd()
2011 path = Path("workspace") / "project"
2012 report = black.Report(verbose=True)
2013 normalized_path = black.normalize_path_maybe_ignore(path, root, report)
2014 self.assertEqual(normalized_path, "workspace/project")
2016 os.chdir(str(old_cwd))
2018 def test_newline_comment_interaction(self) -> None:
2019 source = "class A:\\\r\n# type: ignore\n pass\n"
2020 output = black.format_str(source, mode=DEFAULT_MODE)
2021 black.assert_stable(source, output, mode=DEFAULT_MODE)
2023 def test_bpo_2142_workaround(self) -> None:
2025 # https://bugs.python.org/issue2142
2027 source, _ = read_data("missing_final_newline.py")
2028 # read_data adds a trailing newline
2029 source = source.rstrip()
2030 expected, _ = read_data("missing_final_newline.diff")
2031 tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
2032 diff_header = re.compile(
2033 rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
2034 r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
2037 result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
2038 self.assertEqual(result.exit_code, 0)
2041 actual = result.output
2042 actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
2043 self.assertEqual(actual, expected)
2045 @pytest.mark.python2
2046 def test_docstring_reformat_for_py27(self) -> None:
2048 Check that stripping trailing whitespace from Python 2 docstrings
2049 doesn't trigger a "not equivalent to source" error
2052 b'def foo():\r\n """Testing\r\n Testing """\r\n print "Foo"\r\n'
2054 expected = 'def foo():\n """Testing\n Testing"""\n print "Foo"\n'
2056 result = CliRunner().invoke(
2058 ["-", "-q", "--target-version=py27"],
2059 input=BytesIO(source),
2062 self.assertEqual(result.exit_code, 0)
2063 actual = result.output
2064 self.assertFormatEqual(actual, expected)
2067 with open(black.__file__, "r", encoding="utf-8") as _bf:
2068 black_source_lines = _bf.readlines()
2071 def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
2072 """Show function calls `from black/__init__.py` as they happen.
2074 Register this with `sys.settrace()` in a test you're debugging.
2079 stack = len(inspect.stack()) - 19
2081 filename = frame.f_code.co_filename
2082 lineno = frame.f_lineno
2083 func_sig_lineno = lineno - 1
2084 funcname = black_source_lines[func_sig_lineno].strip()
2085 while funcname.startswith("@"):
2086 func_sig_lineno += 1
2087 funcname = black_source_lines[func_sig_lineno].strip()
2088 if "black/__init__.py" in filename:
2089 print(f"{' ' * stack}{lineno}:{funcname}")
2093 if __name__ == "__main__":
2094 unittest.main(module="test_black")