]> git.madduck.net Git - etc/vim.git/blob - tests/test_black.py

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

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.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

43368d4bbe913230c9bcb59d2c6673e9aa9f098a
[etc/vim.git] / tests / test_black.py
1 #!/usr/bin/env python3
2 import multiprocessing
3 import asyncio
4 import logging
5 from concurrent.futures import ThreadPoolExecutor
6 from contextlib import contextmanager
7 from dataclasses import replace
8 import inspect
9 from io import BytesIO, TextIOWrapper
10 import os
11 from pathlib import Path
12 from platform import system
13 import regex as re
14 import sys
15 from tempfile import TemporaryDirectory
16 import types
17 from typing import (
18     Any,
19     BinaryIO,
20     Callable,
21     Dict,
22     Generator,
23     List,
24     Iterator,
25     TypeVar,
26 )
27 import unittest
28 from unittest.mock import patch, MagicMock
29
30 import click
31 from click import unstyle
32 from click.testing import CliRunner
33
34 import black
35 from black import Feature, TargetVersion
36
37 from pathspec import PathSpec
38
39 # Import other test classes
40 from tests.util import (
41     THIS_DIR,
42     read_data,
43     DETERMINISTIC_HEADER,
44     BlackBaseTestCase,
45     DEFAULT_MODE,
46     fs,
47     ff,
48     dump_to_stderr,
49 )
50 from .test_primer import PrimerCLITests  # noqa: F401
51
52
53 THIS_FILE = Path(__file__)
54 PY36_VERSIONS = {
55     TargetVersion.PY36,
56     TargetVersion.PY37,
57     TargetVersion.PY38,
58     TargetVersion.PY39,
59 }
60 PY36_ARGS = [f"--target-version={version.name.lower()}" for version in PY36_VERSIONS]
61 T = TypeVar("T")
62 R = TypeVar("R")
63
64
65 @contextmanager
66 def cache_dir(exists: bool = True) -> Iterator[Path]:
67     with TemporaryDirectory() as workspace:
68         cache_dir = Path(workspace)
69         if not exists:
70             cache_dir = cache_dir / "new"
71         with patch("black.CACHE_DIR", cache_dir):
72             yield cache_dir
73
74
75 @contextmanager
76 def event_loop() -> Iterator[None]:
77     policy = asyncio.get_event_loop_policy()
78     loop = policy.new_event_loop()
79     asyncio.set_event_loop(loop)
80     try:
81         yield
82
83     finally:
84         loop.close()
85
86
87 class FakeContext(click.Context):
88     """A fake click Context for when calling functions that need it."""
89
90     def __init__(self) -> None:
91         self.default_map: Dict[str, Any] = {}
92
93
94 class FakeParameter(click.Parameter):
95     """A fake click Parameter for when calling functions that need it."""
96
97     def __init__(self) -> None:
98         pass
99
100
101 class BlackRunner(CliRunner):
102     """Modify CliRunner so that stderr is not merged with stdout.
103
104     This is a hack that can be removed once we depend on Click 7.x"""
105
106     def __init__(self) -> None:
107         self.stderrbuf = BytesIO()
108         self.stdoutbuf = BytesIO()
109         self.stdout_bytes = b""
110         self.stderr_bytes = b""
111         super().__init__()
112
113     @contextmanager
114     def isolation(self, *args: Any, **kwargs: Any) -> Generator[BinaryIO, None, None]:
115         with super().isolation(*args, **kwargs) as output:
116             try:
117                 hold_stderr = sys.stderr
118                 sys.stderr = TextIOWrapper(self.stderrbuf, encoding=self.charset)
119                 yield output
120             finally:
121                 self.stdout_bytes = sys.stdout.buffer.getvalue()  # type: ignore
122                 self.stderr_bytes = sys.stderr.buffer.getvalue()  # type: ignore
123                 sys.stderr = hold_stderr
124
125
126 class BlackTestCase(BlackBaseTestCase):
127     def invokeBlack(
128         self, args: List[str], exit_code: int = 0, ignore_config: bool = True
129     ) -> None:
130         runner = BlackRunner()
131         if ignore_config:
132             args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
133         result = runner.invoke(black.main, args)
134         self.assertEqual(
135             result.exit_code,
136             exit_code,
137             msg=(
138                 f"Failed with args: {args}\n"
139                 f"stdout: {runner.stdout_bytes.decode()!r}\n"
140                 f"stderr: {runner.stderr_bytes.decode()!r}\n"
141                 f"exception: {result.exception}"
142             ),
143         )
144
145     @patch("black.dump_to_file", dump_to_stderr)
146     def test_empty(self) -> None:
147         source = expected = ""
148         actual = fs(source)
149         self.assertFormatEqual(expected, actual)
150         black.assert_equivalent(source, actual)
151         black.assert_stable(source, actual, DEFAULT_MODE)
152
153     def test_empty_ff(self) -> None:
154         expected = ""
155         tmp_file = Path(black.dump_to_file())
156         try:
157             self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
158             with open(tmp_file, encoding="utf8") as f:
159                 actual = f.read()
160         finally:
161             os.unlink(tmp_file)
162         self.assertFormatEqual(expected, actual)
163
164     def test_piping(self) -> None:
165         source, expected = read_data("src/black/__init__", data=False)
166         result = BlackRunner().invoke(
167             black.main,
168             ["-", "--fast", f"--line-length={black.DEFAULT_LINE_LENGTH}"],
169             input=BytesIO(source.encode("utf8")),
170         )
171         self.assertEqual(result.exit_code, 0)
172         self.assertFormatEqual(expected, result.output)
173         black.assert_equivalent(source, result.output)
174         black.assert_stable(source, result.output, DEFAULT_MODE)
175
176     def test_piping_diff(self) -> None:
177         diff_header = re.compile(
178             r"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
179             r"\+\d\d\d\d"
180         )
181         source, _ = read_data("expression.py")
182         expected, _ = read_data("expression.diff")
183         config = THIS_DIR / "data" / "empty_pyproject.toml"
184         args = [
185             "-",
186             "--fast",
187             f"--line-length={black.DEFAULT_LINE_LENGTH}",
188             "--diff",
189             f"--config={config}",
190         ]
191         result = BlackRunner().invoke(
192             black.main, args, input=BytesIO(source.encode("utf8"))
193         )
194         self.assertEqual(result.exit_code, 0)
195         actual = diff_header.sub(DETERMINISTIC_HEADER, result.output)
196         actual = actual.rstrip() + "\n"  # the diff output has a trailing space
197         self.assertEqual(expected, actual)
198
199     def test_piping_diff_with_color(self) -> None:
200         source, _ = read_data("expression.py")
201         config = THIS_DIR / "data" / "empty_pyproject.toml"
202         args = [
203             "-",
204             "--fast",
205             f"--line-length={black.DEFAULT_LINE_LENGTH}",
206             "--diff",
207             "--color",
208             f"--config={config}",
209         ]
210         result = BlackRunner().invoke(
211             black.main, args, input=BytesIO(source.encode("utf8"))
212         )
213         actual = result.output
214         # Again, the contents are checked in a different test, so only look for colors.
215         self.assertIn("\033[1;37m", actual)
216         self.assertIn("\033[36m", actual)
217         self.assertIn("\033[32m", actual)
218         self.assertIn("\033[31m", actual)
219         self.assertIn("\033[0m", actual)
220
221     @patch("black.dump_to_file", dump_to_stderr)
222     def _test_wip(self) -> None:
223         source, expected = read_data("wip")
224         sys.settrace(tracefunc)
225         mode = replace(
226             DEFAULT_MODE,
227             experimental_string_processing=False,
228             target_versions={black.TargetVersion.PY38},
229         )
230         actual = fs(source, mode=mode)
231         sys.settrace(None)
232         self.assertFormatEqual(expected, actual)
233         black.assert_equivalent(source, actual)
234         black.assert_stable(source, actual, black.FileMode())
235
236     @unittest.expectedFailure
237     @patch("black.dump_to_file", dump_to_stderr)
238     def test_trailing_comma_optional_parens_stability1(self) -> None:
239         source, _expected = read_data("trailing_comma_optional_parens1")
240         actual = fs(source)
241         black.assert_stable(source, actual, DEFAULT_MODE)
242
243     @unittest.expectedFailure
244     @patch("black.dump_to_file", dump_to_stderr)
245     def test_trailing_comma_optional_parens_stability2(self) -> None:
246         source, _expected = read_data("trailing_comma_optional_parens2")
247         actual = fs(source)
248         black.assert_stable(source, actual, DEFAULT_MODE)
249
250     @unittest.expectedFailure
251     @patch("black.dump_to_file", dump_to_stderr)
252     def test_trailing_comma_optional_parens_stability3(self) -> None:
253         source, _expected = read_data("trailing_comma_optional_parens3")
254         actual = fs(source)
255         black.assert_stable(source, actual, DEFAULT_MODE)
256
257     @patch("black.dump_to_file", dump_to_stderr)
258     def test_trailing_comma_optional_parens_stability1_pass2(self) -> None:
259         source, _expected = read_data("trailing_comma_optional_parens1")
260         actual = fs(fs(source))  # this is what `format_file_contents` does with --safe
261         black.assert_stable(source, actual, DEFAULT_MODE)
262
263     @patch("black.dump_to_file", dump_to_stderr)
264     def test_trailing_comma_optional_parens_stability2_pass2(self) -> None:
265         source, _expected = read_data("trailing_comma_optional_parens2")
266         actual = fs(fs(source))  # this is what `format_file_contents` does with --safe
267         black.assert_stable(source, actual, DEFAULT_MODE)
268
269     @patch("black.dump_to_file", dump_to_stderr)
270     def test_trailing_comma_optional_parens_stability3_pass2(self) -> None:
271         source, _expected = read_data("trailing_comma_optional_parens3")
272         actual = fs(fs(source))  # this is what `format_file_contents` does with --safe
273         black.assert_stable(source, actual, DEFAULT_MODE)
274
275     @patch("black.dump_to_file", dump_to_stderr)
276     def test_pep_572(self) -> None:
277         source, expected = read_data("pep_572")
278         actual = fs(source)
279         self.assertFormatEqual(expected, actual)
280         black.assert_stable(source, actual, DEFAULT_MODE)
281         if sys.version_info >= (3, 8):
282             black.assert_equivalent(source, actual)
283
284     @patch("black.dump_to_file", dump_to_stderr)
285     def test_pep_572_remove_parens(self) -> None:
286         source, expected = read_data("pep_572_remove_parens")
287         actual = fs(source)
288         self.assertFormatEqual(expected, actual)
289         black.assert_stable(source, actual, DEFAULT_MODE)
290         if sys.version_info >= (3, 8):
291             black.assert_equivalent(source, actual)
292
293     @patch("black.dump_to_file", dump_to_stderr)
294     def test_pep_572_do_not_remove_parens(self) -> None:
295         source, expected = read_data("pep_572_do_not_remove_parens")
296         # the AST safety checks will fail, but that's expected, just make sure no
297         # parentheses are touched
298         actual = black.format_str(source, mode=DEFAULT_MODE)
299         self.assertFormatEqual(expected, actual)
300
301     def test_pep_572_version_detection(self) -> None:
302         source, _ = read_data("pep_572")
303         root = black.lib2to3_parse(source)
304         features = black.get_features_used(root)
305         self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
306         versions = black.detect_target_versions(root)
307         self.assertIn(black.TargetVersion.PY38, versions)
308
309     def test_expression_ff(self) -> None:
310         source, expected = read_data("expression")
311         tmp_file = Path(black.dump_to_file(source))
312         try:
313             self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
314             with open(tmp_file, encoding="utf8") as f:
315                 actual = f.read()
316         finally:
317             os.unlink(tmp_file)
318         self.assertFormatEqual(expected, actual)
319         with patch("black.dump_to_file", dump_to_stderr):
320             black.assert_equivalent(source, actual)
321             black.assert_stable(source, actual, DEFAULT_MODE)
322
323     def test_expression_diff(self) -> None:
324         source, _ = read_data("expression.py")
325         config = THIS_DIR / "data" / "empty_pyproject.toml"
326         expected, _ = read_data("expression.diff")
327         tmp_file = Path(black.dump_to_file(source))
328         diff_header = re.compile(
329             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
330             r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
331         )
332         try:
333             result = BlackRunner().invoke(
334                 black.main, ["--diff", str(tmp_file), f"--config={config}"]
335             )
336             self.assertEqual(result.exit_code, 0)
337         finally:
338             os.unlink(tmp_file)
339         actual = result.output
340         actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
341         if expected != actual:
342             dump = black.dump_to_file(actual)
343             msg = (
344                 "Expected diff isn't equal to the actual. If you made changes to"
345                 " expression.py and this is an anticipated difference, overwrite"
346                 f" tests/data/expression.diff with {dump}"
347             )
348             self.assertEqual(expected, actual, msg)
349
350     def test_expression_diff_with_color(self) -> None:
351         source, _ = read_data("expression.py")
352         config = THIS_DIR / "data" / "empty_pyproject.toml"
353         expected, _ = read_data("expression.diff")
354         tmp_file = Path(black.dump_to_file(source))
355         try:
356             result = BlackRunner().invoke(
357                 black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
358             )
359         finally:
360             os.unlink(tmp_file)
361         actual = result.output
362         # We check the contents of the diff in `test_expression_diff`. All
363         # we need to check here is that color codes exist in the result.
364         self.assertIn("\033[1;37m", actual)
365         self.assertIn("\033[36m", actual)
366         self.assertIn("\033[32m", actual)
367         self.assertIn("\033[31m", actual)
368         self.assertIn("\033[0m", actual)
369
370     @patch("black.dump_to_file", dump_to_stderr)
371     def test_pep_570(self) -> None:
372         source, expected = read_data("pep_570")
373         actual = fs(source)
374         self.assertFormatEqual(expected, actual)
375         black.assert_stable(source, actual, DEFAULT_MODE)
376         if sys.version_info >= (3, 8):
377             black.assert_equivalent(source, actual)
378
379     def test_detect_pos_only_arguments(self) -> None:
380         source, _ = read_data("pep_570")
381         root = black.lib2to3_parse(source)
382         features = black.get_features_used(root)
383         self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
384         versions = black.detect_target_versions(root)
385         self.assertIn(black.TargetVersion.PY38, versions)
386
387     @patch("black.dump_to_file", dump_to_stderr)
388     def test_string_quotes(self) -> None:
389         source, expected = read_data("string_quotes")
390         mode = black.Mode(experimental_string_processing=True)
391         actual = fs(source, mode=mode)
392         self.assertFormatEqual(expected, actual)
393         black.assert_equivalent(source, actual)
394         black.assert_stable(source, actual, mode)
395         mode = replace(mode, string_normalization=False)
396         not_normalized = fs(source, mode=mode)
397         self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
398         black.assert_equivalent(source, not_normalized)
399         black.assert_stable(source, not_normalized, mode=mode)
400
401     @patch("black.dump_to_file", dump_to_stderr)
402     def test_docstring_no_string_normalization(self) -> None:
403         """Like test_docstring but with string normalization off."""
404         source, expected = read_data("docstring_no_string_normalization")
405         mode = replace(DEFAULT_MODE, string_normalization=False)
406         actual = fs(source, mode=mode)
407         self.assertFormatEqual(expected, actual)
408         black.assert_equivalent(source, actual)
409         black.assert_stable(source, actual, mode)
410
411     def test_long_strings_flag_disabled(self) -> None:
412         """Tests for turning off the string processing logic."""
413         source, expected = read_data("long_strings_flag_disabled")
414         mode = replace(DEFAULT_MODE, experimental_string_processing=False)
415         actual = fs(source, mode=mode)
416         self.assertFormatEqual(expected, actual)
417         black.assert_stable(expected, actual, mode)
418
419     @patch("black.dump_to_file", dump_to_stderr)
420     def test_numeric_literals(self) -> None:
421         source, expected = read_data("numeric_literals")
422         mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
423         actual = fs(source, mode=mode)
424         self.assertFormatEqual(expected, actual)
425         black.assert_equivalent(source, actual)
426         black.assert_stable(source, actual, mode)
427
428     @patch("black.dump_to_file", dump_to_stderr)
429     def test_numeric_literals_ignoring_underscores(self) -> None:
430         source, expected = read_data("numeric_literals_skip_underscores")
431         mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
432         actual = fs(source, mode=mode)
433         self.assertFormatEqual(expected, actual)
434         black.assert_equivalent(source, actual)
435         black.assert_stable(source, actual, mode)
436
437     def test_skip_magic_trailing_comma(self) -> None:
438         source, _ = read_data("expression.py")
439         expected, _ = read_data("expression_skip_magic_trailing_comma.diff")
440         tmp_file = Path(black.dump_to_file(source))
441         diff_header = re.compile(
442             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
443             r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
444         )
445         try:
446             result = BlackRunner().invoke(black.main, ["-C", "--diff", str(tmp_file)])
447             self.assertEqual(result.exit_code, 0)
448         finally:
449             os.unlink(tmp_file)
450         actual = result.output
451         actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
452         actual = actual.rstrip() + "\n"  # the diff output has a trailing space
453         if expected != actual:
454             dump = black.dump_to_file(actual)
455             msg = (
456                 "Expected diff isn't equal to the actual. If you made changes to"
457                 " expression.py and this is an anticipated difference, overwrite"
458                 f" tests/data/expression_skip_magic_trailing_comma.diff with {dump}"
459             )
460             self.assertEqual(expected, actual, msg)
461
462     @patch("black.dump_to_file", dump_to_stderr)
463     def test_python2_print_function(self) -> None:
464         source, expected = read_data("python2_print_function")
465         mode = replace(DEFAULT_MODE, target_versions={TargetVersion.PY27})
466         actual = fs(source, mode=mode)
467         self.assertFormatEqual(expected, actual)
468         black.assert_equivalent(source, actual)
469         black.assert_stable(source, actual, mode)
470
471     @patch("black.dump_to_file", dump_to_stderr)
472     def test_stub(self) -> None:
473         mode = replace(DEFAULT_MODE, is_pyi=True)
474         source, expected = read_data("stub.pyi")
475         actual = fs(source, mode=mode)
476         self.assertFormatEqual(expected, actual)
477         black.assert_stable(source, actual, mode)
478
479     @patch("black.dump_to_file", dump_to_stderr)
480     def test_async_as_identifier(self) -> None:
481         source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
482         source, expected = read_data("async_as_identifier")
483         actual = fs(source)
484         self.assertFormatEqual(expected, actual)
485         major, minor = sys.version_info[:2]
486         if major < 3 or (major <= 3 and minor < 7):
487             black.assert_equivalent(source, actual)
488         black.assert_stable(source, actual, DEFAULT_MODE)
489         # ensure black can parse this when the target is 3.6
490         self.invokeBlack([str(source_path), "--target-version", "py36"])
491         # but not on 3.7, because async/await is no longer an identifier
492         self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
493
494     @patch("black.dump_to_file", dump_to_stderr)
495     def test_python37(self) -> None:
496         source_path = (THIS_DIR / "data" / "python37.py").resolve()
497         source, expected = read_data("python37")
498         actual = fs(source)
499         self.assertFormatEqual(expected, actual)
500         major, minor = sys.version_info[:2]
501         if major > 3 or (major == 3 and minor >= 7):
502             black.assert_equivalent(source, actual)
503         black.assert_stable(source, actual, DEFAULT_MODE)
504         # ensure black can parse this when the target is 3.7
505         self.invokeBlack([str(source_path), "--target-version", "py37"])
506         # but not on 3.6, because we use async as a reserved keyword
507         self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
508
509     @patch("black.dump_to_file", dump_to_stderr)
510     def test_python38(self) -> None:
511         source, expected = read_data("python38")
512         actual = fs(source)
513         self.assertFormatEqual(expected, actual)
514         major, minor = sys.version_info[:2]
515         if major > 3 or (major == 3 and minor >= 8):
516             black.assert_equivalent(source, actual)
517         black.assert_stable(source, actual, DEFAULT_MODE)
518
519     @patch("black.dump_to_file", dump_to_stderr)
520     def test_python39(self) -> None:
521         source, expected = read_data("python39")
522         actual = fs(source)
523         self.assertFormatEqual(expected, actual)
524         major, minor = sys.version_info[:2]
525         if major > 3 or (major == 3 and minor >= 9):
526             black.assert_equivalent(source, actual)
527         black.assert_stable(source, actual, DEFAULT_MODE)
528
529     def test_tab_comment_indentation(self) -> None:
530         contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
531         contents_spc = "if 1:\n    if 2:\n        pass\n    # comment\n    pass\n"
532         self.assertFormatEqual(contents_spc, fs(contents_spc))
533         self.assertFormatEqual(contents_spc, fs(contents_tab))
534
535         contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t\t# comment\n\tpass\n"
536         contents_spc = "if 1:\n    if 2:\n        pass\n        # comment\n    pass\n"
537         self.assertFormatEqual(contents_spc, fs(contents_spc))
538         self.assertFormatEqual(contents_spc, fs(contents_tab))
539
540         # mixed tabs and spaces (valid Python 2 code)
541         contents_tab = "if 1:\n        if 2:\n\t\tpass\n\t# comment\n        pass\n"
542         contents_spc = "if 1:\n    if 2:\n        pass\n    # comment\n    pass\n"
543         self.assertFormatEqual(contents_spc, fs(contents_spc))
544         self.assertFormatEqual(contents_spc, fs(contents_tab))
545
546         contents_tab = "if 1:\n        if 2:\n\t\tpass\n\t\t# comment\n        pass\n"
547         contents_spc = "if 1:\n    if 2:\n        pass\n        # comment\n    pass\n"
548         self.assertFormatEqual(contents_spc, fs(contents_spc))
549         self.assertFormatEqual(contents_spc, fs(contents_tab))
550
551     def test_report_verbose(self) -> None:
552         report = black.Report(verbose=True)
553         out_lines = []
554         err_lines = []
555
556         def out(msg: str, **kwargs: Any) -> None:
557             out_lines.append(msg)
558
559         def err(msg: str, **kwargs: Any) -> None:
560             err_lines.append(msg)
561
562         with patch("black.out", out), patch("black.err", err):
563             report.done(Path("f1"), black.Changed.NO)
564             self.assertEqual(len(out_lines), 1)
565             self.assertEqual(len(err_lines), 0)
566             self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
567             self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
568             self.assertEqual(report.return_code, 0)
569             report.done(Path("f2"), black.Changed.YES)
570             self.assertEqual(len(out_lines), 2)
571             self.assertEqual(len(err_lines), 0)
572             self.assertEqual(out_lines[-1], "reformatted f2")
573             self.assertEqual(
574                 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
575             )
576             report.done(Path("f3"), black.Changed.CACHED)
577             self.assertEqual(len(out_lines), 3)
578             self.assertEqual(len(err_lines), 0)
579             self.assertEqual(
580                 out_lines[-1], "f3 wasn't modified on disk since last run."
581             )
582             self.assertEqual(
583                 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
584             )
585             self.assertEqual(report.return_code, 0)
586             report.check = True
587             self.assertEqual(report.return_code, 1)
588             report.check = False
589             report.failed(Path("e1"), "boom")
590             self.assertEqual(len(out_lines), 3)
591             self.assertEqual(len(err_lines), 1)
592             self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
593             self.assertEqual(
594                 unstyle(str(report)),
595                 "1 file reformatted, 2 files left unchanged, 1 file failed to"
596                 " reformat.",
597             )
598             self.assertEqual(report.return_code, 123)
599             report.done(Path("f3"), black.Changed.YES)
600             self.assertEqual(len(out_lines), 4)
601             self.assertEqual(len(err_lines), 1)
602             self.assertEqual(out_lines[-1], "reformatted f3")
603             self.assertEqual(
604                 unstyle(str(report)),
605                 "2 files reformatted, 2 files left unchanged, 1 file failed to"
606                 " reformat.",
607             )
608             self.assertEqual(report.return_code, 123)
609             report.failed(Path("e2"), "boom")
610             self.assertEqual(len(out_lines), 4)
611             self.assertEqual(len(err_lines), 2)
612             self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
613             self.assertEqual(
614                 unstyle(str(report)),
615                 "2 files reformatted, 2 files left unchanged, 2 files failed to"
616                 " reformat.",
617             )
618             self.assertEqual(report.return_code, 123)
619             report.path_ignored(Path("wat"), "no match")
620             self.assertEqual(len(out_lines), 5)
621             self.assertEqual(len(err_lines), 2)
622             self.assertEqual(out_lines[-1], "wat ignored: no match")
623             self.assertEqual(
624                 unstyle(str(report)),
625                 "2 files reformatted, 2 files left unchanged, 2 files failed to"
626                 " reformat.",
627             )
628             self.assertEqual(report.return_code, 123)
629             report.done(Path("f4"), black.Changed.NO)
630             self.assertEqual(len(out_lines), 6)
631             self.assertEqual(len(err_lines), 2)
632             self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
633             self.assertEqual(
634                 unstyle(str(report)),
635                 "2 files reformatted, 3 files left unchanged, 2 files failed to"
636                 " reformat.",
637             )
638             self.assertEqual(report.return_code, 123)
639             report.check = True
640             self.assertEqual(
641                 unstyle(str(report)),
642                 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
643                 " would fail to reformat.",
644             )
645             report.check = False
646             report.diff = True
647             self.assertEqual(
648                 unstyle(str(report)),
649                 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
650                 " would fail to reformat.",
651             )
652
653     def test_report_quiet(self) -> None:
654         report = black.Report(quiet=True)
655         out_lines = []
656         err_lines = []
657
658         def out(msg: str, **kwargs: Any) -> None:
659             out_lines.append(msg)
660
661         def err(msg: str, **kwargs: Any) -> None:
662             err_lines.append(msg)
663
664         with patch("black.out", out), patch("black.err", err):
665             report.done(Path("f1"), black.Changed.NO)
666             self.assertEqual(len(out_lines), 0)
667             self.assertEqual(len(err_lines), 0)
668             self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
669             self.assertEqual(report.return_code, 0)
670             report.done(Path("f2"), black.Changed.YES)
671             self.assertEqual(len(out_lines), 0)
672             self.assertEqual(len(err_lines), 0)
673             self.assertEqual(
674                 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
675             )
676             report.done(Path("f3"), black.Changed.CACHED)
677             self.assertEqual(len(out_lines), 0)
678             self.assertEqual(len(err_lines), 0)
679             self.assertEqual(
680                 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
681             )
682             self.assertEqual(report.return_code, 0)
683             report.check = True
684             self.assertEqual(report.return_code, 1)
685             report.check = False
686             report.failed(Path("e1"), "boom")
687             self.assertEqual(len(out_lines), 0)
688             self.assertEqual(len(err_lines), 1)
689             self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
690             self.assertEqual(
691                 unstyle(str(report)),
692                 "1 file reformatted, 2 files left unchanged, 1 file failed to"
693                 " reformat.",
694             )
695             self.assertEqual(report.return_code, 123)
696             report.done(Path("f3"), black.Changed.YES)
697             self.assertEqual(len(out_lines), 0)
698             self.assertEqual(len(err_lines), 1)
699             self.assertEqual(
700                 unstyle(str(report)),
701                 "2 files reformatted, 2 files left unchanged, 1 file failed to"
702                 " reformat.",
703             )
704             self.assertEqual(report.return_code, 123)
705             report.failed(Path("e2"), "boom")
706             self.assertEqual(len(out_lines), 0)
707             self.assertEqual(len(err_lines), 2)
708             self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
709             self.assertEqual(
710                 unstyle(str(report)),
711                 "2 files reformatted, 2 files left unchanged, 2 files failed to"
712                 " reformat.",
713             )
714             self.assertEqual(report.return_code, 123)
715             report.path_ignored(Path("wat"), "no match")
716             self.assertEqual(len(out_lines), 0)
717             self.assertEqual(len(err_lines), 2)
718             self.assertEqual(
719                 unstyle(str(report)),
720                 "2 files reformatted, 2 files left unchanged, 2 files failed to"
721                 " reformat.",
722             )
723             self.assertEqual(report.return_code, 123)
724             report.done(Path("f4"), black.Changed.NO)
725             self.assertEqual(len(out_lines), 0)
726             self.assertEqual(len(err_lines), 2)
727             self.assertEqual(
728                 unstyle(str(report)),
729                 "2 files reformatted, 3 files left unchanged, 2 files failed to"
730                 " reformat.",
731             )
732             self.assertEqual(report.return_code, 123)
733             report.check = True
734             self.assertEqual(
735                 unstyle(str(report)),
736                 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
737                 " would fail to reformat.",
738             )
739             report.check = False
740             report.diff = True
741             self.assertEqual(
742                 unstyle(str(report)),
743                 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
744                 " would fail to reformat.",
745             )
746
747     def test_report_normal(self) -> None:
748         report = black.Report()
749         out_lines = []
750         err_lines = []
751
752         def out(msg: str, **kwargs: Any) -> None:
753             out_lines.append(msg)
754
755         def err(msg: str, **kwargs: Any) -> None:
756             err_lines.append(msg)
757
758         with patch("black.out", out), patch("black.err", err):
759             report.done(Path("f1"), black.Changed.NO)
760             self.assertEqual(len(out_lines), 0)
761             self.assertEqual(len(err_lines), 0)
762             self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
763             self.assertEqual(report.return_code, 0)
764             report.done(Path("f2"), black.Changed.YES)
765             self.assertEqual(len(out_lines), 1)
766             self.assertEqual(len(err_lines), 0)
767             self.assertEqual(out_lines[-1], "reformatted f2")
768             self.assertEqual(
769                 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
770             )
771             report.done(Path("f3"), black.Changed.CACHED)
772             self.assertEqual(len(out_lines), 1)
773             self.assertEqual(len(err_lines), 0)
774             self.assertEqual(out_lines[-1], "reformatted f2")
775             self.assertEqual(
776                 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
777             )
778             self.assertEqual(report.return_code, 0)
779             report.check = True
780             self.assertEqual(report.return_code, 1)
781             report.check = False
782             report.failed(Path("e1"), "boom")
783             self.assertEqual(len(out_lines), 1)
784             self.assertEqual(len(err_lines), 1)
785             self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
786             self.assertEqual(
787                 unstyle(str(report)),
788                 "1 file reformatted, 2 files left unchanged, 1 file failed to"
789                 " reformat.",
790             )
791             self.assertEqual(report.return_code, 123)
792             report.done(Path("f3"), black.Changed.YES)
793             self.assertEqual(len(out_lines), 2)
794             self.assertEqual(len(err_lines), 1)
795             self.assertEqual(out_lines[-1], "reformatted f3")
796             self.assertEqual(
797                 unstyle(str(report)),
798                 "2 files reformatted, 2 files left unchanged, 1 file failed to"
799                 " reformat.",
800             )
801             self.assertEqual(report.return_code, 123)
802             report.failed(Path("e2"), "boom")
803             self.assertEqual(len(out_lines), 2)
804             self.assertEqual(len(err_lines), 2)
805             self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
806             self.assertEqual(
807                 unstyle(str(report)),
808                 "2 files reformatted, 2 files left unchanged, 2 files failed to"
809                 " reformat.",
810             )
811             self.assertEqual(report.return_code, 123)
812             report.path_ignored(Path("wat"), "no match")
813             self.assertEqual(len(out_lines), 2)
814             self.assertEqual(len(err_lines), 2)
815             self.assertEqual(
816                 unstyle(str(report)),
817                 "2 files reformatted, 2 files left unchanged, 2 files failed to"
818                 " reformat.",
819             )
820             self.assertEqual(report.return_code, 123)
821             report.done(Path("f4"), black.Changed.NO)
822             self.assertEqual(len(out_lines), 2)
823             self.assertEqual(len(err_lines), 2)
824             self.assertEqual(
825                 unstyle(str(report)),
826                 "2 files reformatted, 3 files left unchanged, 2 files failed to"
827                 " reformat.",
828             )
829             self.assertEqual(report.return_code, 123)
830             report.check = True
831             self.assertEqual(
832                 unstyle(str(report)),
833                 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
834                 " would fail to reformat.",
835             )
836             report.check = False
837             report.diff = True
838             self.assertEqual(
839                 unstyle(str(report)),
840                 "2 files would be reformatted, 3 files would be left unchanged, 2 files"
841                 " would fail to reformat.",
842             )
843
844     def test_lib2to3_parse(self) -> None:
845         with self.assertRaises(black.InvalidInput):
846             black.lib2to3_parse("invalid syntax")
847
848         straddling = "x + y"
849         black.lib2to3_parse(straddling)
850         black.lib2to3_parse(straddling, {TargetVersion.PY27})
851         black.lib2to3_parse(straddling, {TargetVersion.PY36})
852         black.lib2to3_parse(straddling, {TargetVersion.PY27, TargetVersion.PY36})
853
854         py2_only = "print x"
855         black.lib2to3_parse(py2_only)
856         black.lib2to3_parse(py2_only, {TargetVersion.PY27})
857         with self.assertRaises(black.InvalidInput):
858             black.lib2to3_parse(py2_only, {TargetVersion.PY36})
859         with self.assertRaises(black.InvalidInput):
860             black.lib2to3_parse(py2_only, {TargetVersion.PY27, TargetVersion.PY36})
861
862         py3_only = "exec(x, end=y)"
863         black.lib2to3_parse(py3_only)
864         with self.assertRaises(black.InvalidInput):
865             black.lib2to3_parse(py3_only, {TargetVersion.PY27})
866         black.lib2to3_parse(py3_only, {TargetVersion.PY36})
867         black.lib2to3_parse(py3_only, {TargetVersion.PY27, TargetVersion.PY36})
868
869     def test_get_features_used_decorator(self) -> None:
870         # Test the feature detection of new decorator syntax
871         # since this makes some test cases of test_get_features_used()
872         # fails if it fails, this is tested first so that a useful case
873         # is identified
874         simples, relaxed = read_data("decorators")
875         # skip explanation comments at the top of the file
876         for simple_test in simples.split("##")[1:]:
877             node = black.lib2to3_parse(simple_test)
878             decorator = str(node.children[0].children[0]).strip()
879             self.assertNotIn(
880                 Feature.RELAXED_DECORATORS,
881                 black.get_features_used(node),
882                 msg=(
883                     f"decorator '{decorator}' follows python<=3.8 syntax"
884                     "but is detected as 3.9+"
885                     # f"The full node is\n{node!r}"
886                 ),
887             )
888         # skip the '# output' comment at the top of the output part
889         for relaxed_test in relaxed.split("##")[1:]:
890             node = black.lib2to3_parse(relaxed_test)
891             decorator = str(node.children[0].children[0]).strip()
892             self.assertIn(
893                 Feature.RELAXED_DECORATORS,
894                 black.get_features_used(node),
895                 msg=(
896                     f"decorator '{decorator}' uses python3.9+ syntax"
897                     "but is detected as python<=3.8"
898                     # f"The full node is\n{node!r}"
899                 ),
900             )
901
902     def test_get_features_used(self) -> None:
903         node = black.lib2to3_parse("def f(*, arg): ...\n")
904         self.assertEqual(black.get_features_used(node), set())
905         node = black.lib2to3_parse("def f(*, arg,): ...\n")
906         self.assertEqual(black.get_features_used(node), {Feature.TRAILING_COMMA_IN_DEF})
907         node = black.lib2to3_parse("f(*arg,)\n")
908         self.assertEqual(
909             black.get_features_used(node), {Feature.TRAILING_COMMA_IN_CALL}
910         )
911         node = black.lib2to3_parse("def f(*, arg): f'string'\n")
912         self.assertEqual(black.get_features_used(node), {Feature.F_STRINGS})
913         node = black.lib2to3_parse("123_456\n")
914         self.assertEqual(black.get_features_used(node), {Feature.NUMERIC_UNDERSCORES})
915         node = black.lib2to3_parse("123456\n")
916         self.assertEqual(black.get_features_used(node), set())
917         source, expected = read_data("function")
918         node = black.lib2to3_parse(source)
919         expected_features = {
920             Feature.TRAILING_COMMA_IN_CALL,
921             Feature.TRAILING_COMMA_IN_DEF,
922             Feature.F_STRINGS,
923         }
924         self.assertEqual(black.get_features_used(node), expected_features)
925         node = black.lib2to3_parse(expected)
926         self.assertEqual(black.get_features_used(node), expected_features)
927         source, expected = read_data("expression")
928         node = black.lib2to3_parse(source)
929         self.assertEqual(black.get_features_used(node), set())
930         node = black.lib2to3_parse(expected)
931         self.assertEqual(black.get_features_used(node), set())
932
933     def test_get_future_imports(self) -> None:
934         node = black.lib2to3_parse("\n")
935         self.assertEqual(set(), black.get_future_imports(node))
936         node = black.lib2to3_parse("from __future__ import black\n")
937         self.assertEqual({"black"}, black.get_future_imports(node))
938         node = black.lib2to3_parse("from __future__ import multiple, imports\n")
939         self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
940         node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
941         self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
942         node = black.lib2to3_parse(
943             "from __future__ import multiple\nfrom __future__ import imports\n"
944         )
945         self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
946         node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
947         self.assertEqual({"black"}, black.get_future_imports(node))
948         node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
949         self.assertEqual({"black"}, black.get_future_imports(node))
950         node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
951         self.assertEqual(set(), black.get_future_imports(node))
952         node = black.lib2to3_parse("from some.module import black\n")
953         self.assertEqual(set(), black.get_future_imports(node))
954         node = black.lib2to3_parse(
955             "from __future__ import unicode_literals as _unicode_literals"
956         )
957         self.assertEqual({"unicode_literals"}, black.get_future_imports(node))
958         node = black.lib2to3_parse(
959             "from __future__ import unicode_literals as _lol, print"
960         )
961         self.assertEqual({"unicode_literals", "print"}, black.get_future_imports(node))
962
963     def test_debug_visitor(self) -> None:
964         source, _ = read_data("debug_visitor.py")
965         expected, _ = read_data("debug_visitor.out")
966         out_lines = []
967         err_lines = []
968
969         def out(msg: str, **kwargs: Any) -> None:
970             out_lines.append(msg)
971
972         def err(msg: str, **kwargs: Any) -> None:
973             err_lines.append(msg)
974
975         with patch("black.out", out), patch("black.err", err):
976             black.DebugVisitor.show(source)
977         actual = "\n".join(out_lines) + "\n"
978         log_name = ""
979         if expected != actual:
980             log_name = black.dump_to_file(*out_lines)
981         self.assertEqual(
982             expected,
983             actual,
984             f"AST print out is different. Actual version dumped to {log_name}",
985         )
986
987     def test_format_file_contents(self) -> None:
988         empty = ""
989         mode = DEFAULT_MODE
990         with self.assertRaises(black.NothingChanged):
991             black.format_file_contents(empty, mode=mode, fast=False)
992         just_nl = "\n"
993         with self.assertRaises(black.NothingChanged):
994             black.format_file_contents(just_nl, mode=mode, fast=False)
995         same = "j = [1, 2, 3]\n"
996         with self.assertRaises(black.NothingChanged):
997             black.format_file_contents(same, mode=mode, fast=False)
998         different = "j = [1,2,3]"
999         expected = same
1000         actual = black.format_file_contents(different, mode=mode, fast=False)
1001         self.assertEqual(expected, actual)
1002         invalid = "return if you can"
1003         with self.assertRaises(black.InvalidInput) as e:
1004             black.format_file_contents(invalid, mode=mode, fast=False)
1005         self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
1006
1007     def test_endmarker(self) -> None:
1008         n = black.lib2to3_parse("\n")
1009         self.assertEqual(n.type, black.syms.file_input)
1010         self.assertEqual(len(n.children), 1)
1011         self.assertEqual(n.children[0].type, black.token.ENDMARKER)
1012
1013     @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
1014     def test_assertFormatEqual(self) -> None:
1015         out_lines = []
1016         err_lines = []
1017
1018         def out(msg: str, **kwargs: Any) -> None:
1019             out_lines.append(msg)
1020
1021         def err(msg: str, **kwargs: Any) -> None:
1022             err_lines.append(msg)
1023
1024         with patch("black.out", out), patch("black.err", err):
1025             with self.assertRaises(AssertionError):
1026                 self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
1027
1028         out_str = "".join(out_lines)
1029         self.assertTrue("Expected tree:" in out_str)
1030         self.assertTrue("Actual tree:" in out_str)
1031         self.assertEqual("".join(err_lines), "")
1032
1033     def test_cache_broken_file(self) -> None:
1034         mode = DEFAULT_MODE
1035         with cache_dir() as workspace:
1036             cache_file = black.get_cache_file(mode)
1037             with cache_file.open("w") as fobj:
1038                 fobj.write("this is not a pickle")
1039             self.assertEqual(black.read_cache(mode), {})
1040             src = (workspace / "test.py").resolve()
1041             with src.open("w") as fobj:
1042                 fobj.write("print('hello')")
1043             self.invokeBlack([str(src)])
1044             cache = black.read_cache(mode)
1045             self.assertIn(str(src), cache)
1046
1047     def test_cache_single_file_already_cached(self) -> None:
1048         mode = DEFAULT_MODE
1049         with cache_dir() as workspace:
1050             src = (workspace / "test.py").resolve()
1051             with src.open("w") as fobj:
1052                 fobj.write("print('hello')")
1053             black.write_cache({}, [src], mode)
1054             self.invokeBlack([str(src)])
1055             with src.open("r") as fobj:
1056                 self.assertEqual(fobj.read(), "print('hello')")
1057
1058     @event_loop()
1059     def test_cache_multiple_files(self) -> None:
1060         mode = DEFAULT_MODE
1061         with cache_dir() as workspace, patch(
1062             "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1063         ):
1064             one = (workspace / "one.py").resolve()
1065             with one.open("w") as fobj:
1066                 fobj.write("print('hello')")
1067             two = (workspace / "two.py").resolve()
1068             with two.open("w") as fobj:
1069                 fobj.write("print('hello')")
1070             black.write_cache({}, [one], mode)
1071             self.invokeBlack([str(workspace)])
1072             with one.open("r") as fobj:
1073                 self.assertEqual(fobj.read(), "print('hello')")
1074             with two.open("r") as fobj:
1075                 self.assertEqual(fobj.read(), 'print("hello")\n')
1076             cache = black.read_cache(mode)
1077             self.assertIn(str(one), cache)
1078             self.assertIn(str(two), cache)
1079
1080     def test_no_cache_when_writeback_diff(self) -> None:
1081         mode = DEFAULT_MODE
1082         with cache_dir() as workspace:
1083             src = (workspace / "test.py").resolve()
1084             with src.open("w") as fobj:
1085                 fobj.write("print('hello')")
1086             with patch("black.read_cache") as read_cache, patch(
1087                 "black.write_cache"
1088             ) as write_cache:
1089                 self.invokeBlack([str(src), "--diff"])
1090                 cache_file = black.get_cache_file(mode)
1091                 self.assertFalse(cache_file.exists())
1092                 write_cache.assert_not_called()
1093                 read_cache.assert_not_called()
1094
1095     def test_no_cache_when_writeback_color_diff(self) -> None:
1096         mode = DEFAULT_MODE
1097         with cache_dir() as workspace:
1098             src = (workspace / "test.py").resolve()
1099             with src.open("w") as fobj:
1100                 fobj.write("print('hello')")
1101             with patch("black.read_cache") as read_cache, patch(
1102                 "black.write_cache"
1103             ) as write_cache:
1104                 self.invokeBlack([str(src), "--diff", "--color"])
1105                 cache_file = black.get_cache_file(mode)
1106                 self.assertFalse(cache_file.exists())
1107                 write_cache.assert_not_called()
1108                 read_cache.assert_not_called()
1109
1110     @event_loop()
1111     def test_output_locking_when_writeback_diff(self) -> None:
1112         with cache_dir() as workspace:
1113             for tag in range(0, 4):
1114                 src = (workspace / f"test{tag}.py").resolve()
1115                 with src.open("w") as fobj:
1116                     fobj.write("print('hello')")
1117             with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1118                 self.invokeBlack(["--diff", str(workspace)], exit_code=0)
1119                 # this isn't quite doing what we want, but if it _isn't_
1120                 # called then we cannot be using the lock it provides
1121                 mgr.assert_called()
1122
1123     @event_loop()
1124     def test_output_locking_when_writeback_color_diff(self) -> None:
1125         with cache_dir() as workspace:
1126             for tag in range(0, 4):
1127                 src = (workspace / f"test{tag}.py").resolve()
1128                 with src.open("w") as fobj:
1129                     fobj.write("print('hello')")
1130             with patch("black.Manager", wraps=multiprocessing.Manager) as mgr:
1131                 self.invokeBlack(["--diff", "--color", str(workspace)], exit_code=0)
1132                 # this isn't quite doing what we want, but if it _isn't_
1133                 # called then we cannot be using the lock it provides
1134                 mgr.assert_called()
1135
1136     def test_no_cache_when_stdin(self) -> None:
1137         mode = DEFAULT_MODE
1138         with cache_dir():
1139             result = CliRunner().invoke(
1140                 black.main, ["-"], input=BytesIO(b"print('hello')")
1141             )
1142             self.assertEqual(result.exit_code, 0)
1143             cache_file = black.get_cache_file(mode)
1144             self.assertFalse(cache_file.exists())
1145
1146     def test_read_cache_no_cachefile(self) -> None:
1147         mode = DEFAULT_MODE
1148         with cache_dir():
1149             self.assertEqual(black.read_cache(mode), {})
1150
1151     def test_write_cache_read_cache(self) -> None:
1152         mode = DEFAULT_MODE
1153         with cache_dir() as workspace:
1154             src = (workspace / "test.py").resolve()
1155             src.touch()
1156             black.write_cache({}, [src], mode)
1157             cache = black.read_cache(mode)
1158             self.assertIn(str(src), cache)
1159             self.assertEqual(cache[str(src)], black.get_cache_info(src))
1160
1161     def test_filter_cached(self) -> None:
1162         with TemporaryDirectory() as workspace:
1163             path = Path(workspace)
1164             uncached = (path / "uncached").resolve()
1165             cached = (path / "cached").resolve()
1166             cached_but_changed = (path / "changed").resolve()
1167             uncached.touch()
1168             cached.touch()
1169             cached_but_changed.touch()
1170             cache = {
1171                 str(cached): black.get_cache_info(cached),
1172                 str(cached_but_changed): (0.0, 0),
1173             }
1174             todo, done = black.filter_cached(
1175                 cache, {uncached, cached, cached_but_changed}
1176             )
1177             self.assertEqual(todo, {uncached, cached_but_changed})
1178             self.assertEqual(done, {cached})
1179
1180     def test_write_cache_creates_directory_if_needed(self) -> None:
1181         mode = DEFAULT_MODE
1182         with cache_dir(exists=False) as workspace:
1183             self.assertFalse(workspace.exists())
1184             black.write_cache({}, [], mode)
1185             self.assertTrue(workspace.exists())
1186
1187     @event_loop()
1188     def test_failed_formatting_does_not_get_cached(self) -> None:
1189         mode = DEFAULT_MODE
1190         with cache_dir() as workspace, patch(
1191             "black.ProcessPoolExecutor", new=ThreadPoolExecutor
1192         ):
1193             failing = (workspace / "failing.py").resolve()
1194             with failing.open("w") as fobj:
1195                 fobj.write("not actually python")
1196             clean = (workspace / "clean.py").resolve()
1197             with clean.open("w") as fobj:
1198                 fobj.write('print("hello")\n')
1199             self.invokeBlack([str(workspace)], exit_code=123)
1200             cache = black.read_cache(mode)
1201             self.assertNotIn(str(failing), cache)
1202             self.assertIn(str(clean), cache)
1203
1204     def test_write_cache_write_fail(self) -> None:
1205         mode = DEFAULT_MODE
1206         with cache_dir(), patch.object(Path, "open") as mock:
1207             mock.side_effect = OSError
1208             black.write_cache({}, [], mode)
1209
1210     @event_loop()
1211     @patch("black.ProcessPoolExecutor", MagicMock(side_effect=OSError))
1212     def test_works_in_mono_process_only_environment(self) -> None:
1213         with cache_dir() as workspace:
1214             for f in [
1215                 (workspace / "one.py").resolve(),
1216                 (workspace / "two.py").resolve(),
1217             ]:
1218                 f.write_text('print("hello")\n')
1219             self.invokeBlack([str(workspace)])
1220
1221     @event_loop()
1222     def test_check_diff_use_together(self) -> None:
1223         with cache_dir():
1224             # Files which will be reformatted.
1225             src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
1226             self.invokeBlack([str(src1), "--diff", "--check"], exit_code=1)
1227             # Files which will not be reformatted.
1228             src2 = (THIS_DIR / "data" / "composition.py").resolve()
1229             self.invokeBlack([str(src2), "--diff", "--check"])
1230             # Multi file command.
1231             self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
1232
1233     def test_no_files(self) -> None:
1234         with cache_dir():
1235             # Without an argument, black exits with error code 0.
1236             self.invokeBlack([])
1237
1238     def test_broken_symlink(self) -> None:
1239         with cache_dir() as workspace:
1240             symlink = workspace / "broken_link.py"
1241             try:
1242                 symlink.symlink_to("nonexistent.py")
1243             except OSError as e:
1244                 self.skipTest(f"Can't create symlinks: {e}")
1245             self.invokeBlack([str(workspace.resolve())])
1246
1247     def test_read_cache_line_lengths(self) -> None:
1248         mode = DEFAULT_MODE
1249         short_mode = replace(DEFAULT_MODE, line_length=1)
1250         with cache_dir() as workspace:
1251             path = (workspace / "file.py").resolve()
1252             path.touch()
1253             black.write_cache({}, [path], mode)
1254             one = black.read_cache(mode)
1255             self.assertIn(str(path), one)
1256             two = black.read_cache(short_mode)
1257             self.assertNotIn(str(path), two)
1258
1259     def test_single_file_force_pyi(self) -> None:
1260         pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1261         contents, expected = read_data("force_pyi")
1262         with cache_dir() as workspace:
1263             path = (workspace / "file.py").resolve()
1264             with open(path, "w") as fh:
1265                 fh.write(contents)
1266             self.invokeBlack([str(path), "--pyi"])
1267             with open(path, "r") as fh:
1268                 actual = fh.read()
1269             # verify cache with --pyi is separate
1270             pyi_cache = black.read_cache(pyi_mode)
1271             self.assertIn(str(path), pyi_cache)
1272             normal_cache = black.read_cache(DEFAULT_MODE)
1273             self.assertNotIn(str(path), normal_cache)
1274         self.assertFormatEqual(expected, actual)
1275         black.assert_equivalent(contents, actual)
1276         black.assert_stable(contents, actual, pyi_mode)
1277
1278     @event_loop()
1279     def test_multi_file_force_pyi(self) -> None:
1280         reg_mode = DEFAULT_MODE
1281         pyi_mode = replace(DEFAULT_MODE, is_pyi=True)
1282         contents, expected = read_data("force_pyi")
1283         with cache_dir() as workspace:
1284             paths = [
1285                 (workspace / "file1.py").resolve(),
1286                 (workspace / "file2.py").resolve(),
1287             ]
1288             for path in paths:
1289                 with open(path, "w") as fh:
1290                     fh.write(contents)
1291             self.invokeBlack([str(p) for p in paths] + ["--pyi"])
1292             for path in paths:
1293                 with open(path, "r") as fh:
1294                     actual = fh.read()
1295                 self.assertEqual(actual, expected)
1296             # verify cache with --pyi is separate
1297             pyi_cache = black.read_cache(pyi_mode)
1298             normal_cache = black.read_cache(reg_mode)
1299             for path in paths:
1300                 self.assertIn(str(path), pyi_cache)
1301                 self.assertNotIn(str(path), normal_cache)
1302
1303     def test_pipe_force_pyi(self) -> None:
1304         source, expected = read_data("force_pyi")
1305         result = CliRunner().invoke(
1306             black.main, ["-", "-q", "--pyi"], input=BytesIO(source.encode("utf8"))
1307         )
1308         self.assertEqual(result.exit_code, 0)
1309         actual = result.output
1310         self.assertFormatEqual(actual, expected)
1311
1312     def test_single_file_force_py36(self) -> None:
1313         reg_mode = DEFAULT_MODE
1314         py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1315         source, expected = read_data("force_py36")
1316         with cache_dir() as workspace:
1317             path = (workspace / "file.py").resolve()
1318             with open(path, "w") as fh:
1319                 fh.write(source)
1320             self.invokeBlack([str(path), *PY36_ARGS])
1321             with open(path, "r") as fh:
1322                 actual = fh.read()
1323             # verify cache with --target-version is separate
1324             py36_cache = black.read_cache(py36_mode)
1325             self.assertIn(str(path), py36_cache)
1326             normal_cache = black.read_cache(reg_mode)
1327             self.assertNotIn(str(path), normal_cache)
1328         self.assertEqual(actual, expected)
1329
1330     @event_loop()
1331     def test_multi_file_force_py36(self) -> None:
1332         reg_mode = DEFAULT_MODE
1333         py36_mode = replace(DEFAULT_MODE, target_versions=PY36_VERSIONS)
1334         source, expected = read_data("force_py36")
1335         with cache_dir() as workspace:
1336             paths = [
1337                 (workspace / "file1.py").resolve(),
1338                 (workspace / "file2.py").resolve(),
1339             ]
1340             for path in paths:
1341                 with open(path, "w") as fh:
1342                     fh.write(source)
1343             self.invokeBlack([str(p) for p in paths] + PY36_ARGS)
1344             for path in paths:
1345                 with open(path, "r") as fh:
1346                     actual = fh.read()
1347                 self.assertEqual(actual, expected)
1348             # verify cache with --target-version is separate
1349             pyi_cache = black.read_cache(py36_mode)
1350             normal_cache = black.read_cache(reg_mode)
1351             for path in paths:
1352                 self.assertIn(str(path), pyi_cache)
1353                 self.assertNotIn(str(path), normal_cache)
1354
1355     def test_pipe_force_py36(self) -> None:
1356         source, expected = read_data("force_py36")
1357         result = CliRunner().invoke(
1358             black.main,
1359             ["-", "-q", "--target-version=py36"],
1360             input=BytesIO(source.encode("utf8")),
1361         )
1362         self.assertEqual(result.exit_code, 0)
1363         actual = result.output
1364         self.assertFormatEqual(actual, expected)
1365
1366     def test_include_exclude(self) -> None:
1367         path = THIS_DIR / "data" / "include_exclude_tests"
1368         include = re.compile(r"\.pyi?$")
1369         exclude = re.compile(r"/exclude/|/\.definitely_exclude/")
1370         report = black.Report()
1371         gitignore = PathSpec.from_lines("gitwildmatch", [])
1372         sources: List[Path] = []
1373         expected = [
1374             Path(path / "b/dont_exclude/a.py"),
1375             Path(path / "b/dont_exclude/a.pyi"),
1376         ]
1377         this_abs = THIS_DIR.resolve()
1378         sources.extend(
1379             black.gen_python_files(
1380                 path.iterdir(),
1381                 this_abs,
1382                 include,
1383                 exclude,
1384                 None,
1385                 None,
1386                 report,
1387                 gitignore,
1388             )
1389         )
1390         self.assertEqual(sorted(expected), sorted(sources))
1391
1392     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1393     def test_exclude_for_issue_1572(self) -> None:
1394         # Exclude shouldn't touch files that were explicitly given to Black through the
1395         # CLI. Exclude is supposed to only apply to the recursive discovery of files.
1396         # https://github.com/psf/black/issues/1572
1397         path = THIS_DIR / "data" / "include_exclude_tests"
1398         include = ""
1399         exclude = r"/exclude/|a\.py"
1400         src = str(path / "b/exclude/a.py")
1401         report = black.Report()
1402         expected = [Path(path / "b/exclude/a.py")]
1403         sources = list(
1404             black.get_sources(
1405                 ctx=FakeContext(),
1406                 src=(src,),
1407                 quiet=True,
1408                 verbose=False,
1409                 include=re.compile(include),
1410                 exclude=re.compile(exclude),
1411                 extend_exclude=None,
1412                 force_exclude=None,
1413                 report=report,
1414                 stdin_filename=None,
1415             )
1416         )
1417         self.assertEqual(sorted(expected), sorted(sources))
1418
1419     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1420     def test_get_sources_with_stdin(self) -> None:
1421         include = ""
1422         exclude = r"/exclude/|a\.py"
1423         src = "-"
1424         report = black.Report()
1425         expected = [Path("-")]
1426         sources = list(
1427             black.get_sources(
1428                 ctx=FakeContext(),
1429                 src=(src,),
1430                 quiet=True,
1431                 verbose=False,
1432                 include=re.compile(include),
1433                 exclude=re.compile(exclude),
1434                 extend_exclude=None,
1435                 force_exclude=None,
1436                 report=report,
1437                 stdin_filename=None,
1438             )
1439         )
1440         self.assertEqual(sorted(expected), sorted(sources))
1441
1442     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1443     def test_get_sources_with_stdin_filename(self) -> None:
1444         include = ""
1445         exclude = r"/exclude/|a\.py"
1446         src = "-"
1447         report = black.Report()
1448         stdin_filename = str(THIS_DIR / "data/collections.py")
1449         expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1450         sources = list(
1451             black.get_sources(
1452                 ctx=FakeContext(),
1453                 src=(src,),
1454                 quiet=True,
1455                 verbose=False,
1456                 include=re.compile(include),
1457                 exclude=re.compile(exclude),
1458                 extend_exclude=None,
1459                 force_exclude=None,
1460                 report=report,
1461                 stdin_filename=stdin_filename,
1462             )
1463         )
1464         self.assertEqual(sorted(expected), sorted(sources))
1465
1466     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1467     def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
1468         # Exclude shouldn't exclude stdin_filename since it is mimicing the
1469         # file being passed directly. This is the same as
1470         # test_exclude_for_issue_1572
1471         path = THIS_DIR / "data" / "include_exclude_tests"
1472         include = ""
1473         exclude = r"/exclude/|a\.py"
1474         src = "-"
1475         report = black.Report()
1476         stdin_filename = str(path / "b/exclude/a.py")
1477         expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1478         sources = list(
1479             black.get_sources(
1480                 ctx=FakeContext(),
1481                 src=(src,),
1482                 quiet=True,
1483                 verbose=False,
1484                 include=re.compile(include),
1485                 exclude=re.compile(exclude),
1486                 extend_exclude=None,
1487                 force_exclude=None,
1488                 report=report,
1489                 stdin_filename=stdin_filename,
1490             )
1491         )
1492         self.assertEqual(sorted(expected), sorted(sources))
1493
1494     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1495     def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
1496         # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
1497         # file being passed directly. This is the same as
1498         # test_exclude_for_issue_1572
1499         path = THIS_DIR / "data" / "include_exclude_tests"
1500         include = ""
1501         extend_exclude = r"/exclude/|a\.py"
1502         src = "-"
1503         report = black.Report()
1504         stdin_filename = str(path / "b/exclude/a.py")
1505         expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
1506         sources = list(
1507             black.get_sources(
1508                 ctx=FakeContext(),
1509                 src=(src,),
1510                 quiet=True,
1511                 verbose=False,
1512                 include=re.compile(include),
1513                 exclude=re.compile(""),
1514                 extend_exclude=re.compile(extend_exclude),
1515                 force_exclude=None,
1516                 report=report,
1517                 stdin_filename=stdin_filename,
1518             )
1519         )
1520         self.assertEqual(sorted(expected), sorted(sources))
1521
1522     @patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
1523     def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
1524         # Force exclude should exclude the file when passing it through
1525         # stdin_filename
1526         path = THIS_DIR / "data" / "include_exclude_tests"
1527         include = ""
1528         force_exclude = r"/exclude/|a\.py"
1529         src = "-"
1530         report = black.Report()
1531         stdin_filename = str(path / "b/exclude/a.py")
1532         sources = list(
1533             black.get_sources(
1534                 ctx=FakeContext(),
1535                 src=(src,),
1536                 quiet=True,
1537                 verbose=False,
1538                 include=re.compile(include),
1539                 exclude=re.compile(""),
1540                 extend_exclude=None,
1541                 force_exclude=re.compile(force_exclude),
1542                 report=report,
1543                 stdin_filename=stdin_filename,
1544             )
1545         )
1546         self.assertEqual([], sorted(sources))
1547
1548     def test_reformat_one_with_stdin(self) -> None:
1549         with patch(
1550             "black.format_stdin_to_stdout",
1551             return_value=lambda *args, **kwargs: black.Changed.YES,
1552         ) as fsts:
1553             report = MagicMock()
1554             path = Path("-")
1555             black.reformat_one(
1556                 path,
1557                 fast=True,
1558                 write_back=black.WriteBack.YES,
1559                 mode=DEFAULT_MODE,
1560                 report=report,
1561             )
1562             fsts.assert_called_once()
1563             report.done.assert_called_with(path, black.Changed.YES)
1564
1565     def test_reformat_one_with_stdin_filename(self) -> None:
1566         with patch(
1567             "black.format_stdin_to_stdout",
1568             return_value=lambda *args, **kwargs: black.Changed.YES,
1569         ) as fsts:
1570             report = MagicMock()
1571             p = "foo.py"
1572             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1573             expected = Path(p)
1574             black.reformat_one(
1575                 path,
1576                 fast=True,
1577                 write_back=black.WriteBack.YES,
1578                 mode=DEFAULT_MODE,
1579                 report=report,
1580             )
1581             fsts.assert_called_once_with(
1582                 fast=True, write_back=black.WriteBack.YES, mode=DEFAULT_MODE
1583             )
1584             # __BLACK_STDIN_FILENAME__ should have been stripped
1585             report.done.assert_called_with(expected, black.Changed.YES)
1586
1587     def test_reformat_one_with_stdin_filename_pyi(self) -> None:
1588         with patch(
1589             "black.format_stdin_to_stdout",
1590             return_value=lambda *args, **kwargs: black.Changed.YES,
1591         ) as fsts:
1592             report = MagicMock()
1593             p = "foo.pyi"
1594             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1595             expected = Path(p)
1596             black.reformat_one(
1597                 path,
1598                 fast=True,
1599                 write_back=black.WriteBack.YES,
1600                 mode=DEFAULT_MODE,
1601                 report=report,
1602             )
1603             fsts.assert_called_once_with(
1604                 fast=True,
1605                 write_back=black.WriteBack.YES,
1606                 mode=replace(DEFAULT_MODE, is_pyi=True),
1607             )
1608             # __BLACK_STDIN_FILENAME__ should have been stripped
1609             report.done.assert_called_with(expected, black.Changed.YES)
1610
1611     def test_reformat_one_with_stdin_and_existing_path(self) -> None:
1612         with patch(
1613             "black.format_stdin_to_stdout",
1614             return_value=lambda *args, **kwargs: black.Changed.YES,
1615         ) as fsts:
1616             report = MagicMock()
1617             # Even with an existing file, since we are forcing stdin, black
1618             # should output to stdout and not modify the file inplace
1619             p = Path(str(THIS_DIR / "data/collections.py"))
1620             # Make sure is_file actually returns True
1621             self.assertTrue(p.is_file())
1622             path = Path(f"__BLACK_STDIN_FILENAME__{p}")
1623             expected = Path(p)
1624             black.reformat_one(
1625                 path,
1626                 fast=True,
1627                 write_back=black.WriteBack.YES,
1628                 mode=DEFAULT_MODE,
1629                 report=report,
1630             )
1631             fsts.assert_called_once()
1632             # __BLACK_STDIN_FILENAME__ should have been stripped
1633             report.done.assert_called_with(expected, black.Changed.YES)
1634
1635     def test_gitignore_exclude(self) -> None:
1636         path = THIS_DIR / "data" / "include_exclude_tests"
1637         include = re.compile(r"\.pyi?$")
1638         exclude = re.compile(r"")
1639         report = black.Report()
1640         gitignore = PathSpec.from_lines(
1641             "gitwildmatch", ["exclude/", ".definitely_exclude"]
1642         )
1643         sources: List[Path] = []
1644         expected = [
1645             Path(path / "b/dont_exclude/a.py"),
1646             Path(path / "b/dont_exclude/a.pyi"),
1647         ]
1648         this_abs = THIS_DIR.resolve()
1649         sources.extend(
1650             black.gen_python_files(
1651                 path.iterdir(),
1652                 this_abs,
1653                 include,
1654                 exclude,
1655                 None,
1656                 None,
1657                 report,
1658                 gitignore,
1659             )
1660         )
1661         self.assertEqual(sorted(expected), sorted(sources))
1662
1663     def test_empty_include(self) -> None:
1664         path = THIS_DIR / "data" / "include_exclude_tests"
1665         report = black.Report()
1666         gitignore = PathSpec.from_lines("gitwildmatch", [])
1667         empty = re.compile(r"")
1668         sources: List[Path] = []
1669         expected = [
1670             Path(path / "b/exclude/a.pie"),
1671             Path(path / "b/exclude/a.py"),
1672             Path(path / "b/exclude/a.pyi"),
1673             Path(path / "b/dont_exclude/a.pie"),
1674             Path(path / "b/dont_exclude/a.py"),
1675             Path(path / "b/dont_exclude/a.pyi"),
1676             Path(path / "b/.definitely_exclude/a.pie"),
1677             Path(path / "b/.definitely_exclude/a.py"),
1678             Path(path / "b/.definitely_exclude/a.pyi"),
1679         ]
1680         this_abs = THIS_DIR.resolve()
1681         sources.extend(
1682             black.gen_python_files(
1683                 path.iterdir(),
1684                 this_abs,
1685                 empty,
1686                 re.compile(black.DEFAULT_EXCLUDES),
1687                 None,
1688                 None,
1689                 report,
1690                 gitignore,
1691             )
1692         )
1693         self.assertEqual(sorted(expected), sorted(sources))
1694
1695     def test_extend_exclude(self) -> None:
1696         path = THIS_DIR / "data" / "include_exclude_tests"
1697         report = black.Report()
1698         gitignore = PathSpec.from_lines("gitwildmatch", [])
1699         sources: List[Path] = []
1700         expected = [
1701             Path(path / "b/exclude/a.py"),
1702             Path(path / "b/dont_exclude/a.py"),
1703         ]
1704         this_abs = THIS_DIR.resolve()
1705         sources.extend(
1706             black.gen_python_files(
1707                 path.iterdir(),
1708                 this_abs,
1709                 re.compile(black.DEFAULT_INCLUDES),
1710                 re.compile(r"\.pyi$"),
1711                 re.compile(r"\.definitely_exclude"),
1712                 None,
1713                 report,
1714                 gitignore,
1715             )
1716         )
1717         self.assertEqual(sorted(expected), sorted(sources))
1718
1719     def test_invalid_cli_regex(self) -> None:
1720         for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
1721             self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
1722
1723     def test_preserves_line_endings(self) -> None:
1724         with TemporaryDirectory() as workspace:
1725             test_file = Path(workspace) / "test.py"
1726             for nl in ["\n", "\r\n"]:
1727                 contents = nl.join(["def f(  ):", "    pass"])
1728                 test_file.write_bytes(contents.encode())
1729                 ff(test_file, write_back=black.WriteBack.YES)
1730                 updated_contents: bytes = test_file.read_bytes()
1731                 self.assertIn(nl.encode(), updated_contents)
1732                 if nl == "\n":
1733                     self.assertNotIn(b"\r\n", updated_contents)
1734
1735     def test_preserves_line_endings_via_stdin(self) -> None:
1736         for nl in ["\n", "\r\n"]:
1737             contents = nl.join(["def f(  ):", "    pass"])
1738             runner = BlackRunner()
1739             result = runner.invoke(
1740                 black.main, ["-", "--fast"], input=BytesIO(contents.encode("utf8"))
1741             )
1742             self.assertEqual(result.exit_code, 0)
1743             output = runner.stdout_bytes
1744             self.assertIn(nl.encode("utf8"), output)
1745             if nl == "\n":
1746                 self.assertNotIn(b"\r\n", output)
1747
1748     def test_assert_equivalent_different_asts(self) -> None:
1749         with self.assertRaises(AssertionError):
1750             black.assert_equivalent("{}", "None")
1751
1752     def test_symlink_out_of_root_directory(self) -> None:
1753         path = MagicMock()
1754         root = THIS_DIR.resolve()
1755         child = MagicMock()
1756         include = re.compile(black.DEFAULT_INCLUDES)
1757         exclude = re.compile(black.DEFAULT_EXCLUDES)
1758         report = black.Report()
1759         gitignore = PathSpec.from_lines("gitwildmatch", [])
1760         # `child` should behave like a symlink which resolved path is clearly
1761         # outside of the `root` directory.
1762         path.iterdir.return_value = [child]
1763         child.resolve.return_value = Path("/a/b/c")
1764         child.as_posix.return_value = "/a/b/c"
1765         child.is_symlink.return_value = True
1766         try:
1767             list(
1768                 black.gen_python_files(
1769                     path.iterdir(),
1770                     root,
1771                     include,
1772                     exclude,
1773                     None,
1774                     None,
1775                     report,
1776                     gitignore,
1777                 )
1778             )
1779         except ValueError as ve:
1780             self.fail(f"`get_python_files_in_dir()` failed: {ve}")
1781         path.iterdir.assert_called_once()
1782         child.resolve.assert_called_once()
1783         child.is_symlink.assert_called_once()
1784         # `child` should behave like a strange file which resolved path is clearly
1785         # outside of the `root` directory.
1786         child.is_symlink.return_value = False
1787         with self.assertRaises(ValueError):
1788             list(
1789                 black.gen_python_files(
1790                     path.iterdir(),
1791                     root,
1792                     include,
1793                     exclude,
1794                     None,
1795                     None,
1796                     report,
1797                     gitignore,
1798                 )
1799             )
1800         path.iterdir.assert_called()
1801         self.assertEqual(path.iterdir.call_count, 2)
1802         child.resolve.assert_called()
1803         self.assertEqual(child.resolve.call_count, 2)
1804         child.is_symlink.assert_called()
1805         self.assertEqual(child.is_symlink.call_count, 2)
1806
1807     def test_shhh_click(self) -> None:
1808         try:
1809             from click import _unicodefun  # type: ignore
1810         except ModuleNotFoundError:
1811             self.skipTest("Incompatible Click version")
1812         if not hasattr(_unicodefun, "_verify_python3_env"):
1813             self.skipTest("Incompatible Click version")
1814         # First, let's see if Click is crashing with a preferred ASCII charset.
1815         with patch("locale.getpreferredencoding") as gpe:
1816             gpe.return_value = "ASCII"
1817             with self.assertRaises(RuntimeError):
1818                 _unicodefun._verify_python3_env()
1819         # Now, let's silence Click...
1820         black.patch_click()
1821         # ...and confirm it's silent.
1822         with patch("locale.getpreferredencoding") as gpe:
1823             gpe.return_value = "ASCII"
1824             try:
1825                 _unicodefun._verify_python3_env()
1826             except RuntimeError as re:
1827                 self.fail(f"`patch_click()` failed, exception still raised: {re}")
1828
1829     def test_root_logger_not_used_directly(self) -> None:
1830         def fail(*args: Any, **kwargs: Any) -> None:
1831             self.fail("Record created with root logger")
1832
1833         with patch.multiple(
1834             logging.root,
1835             debug=fail,
1836             info=fail,
1837             warning=fail,
1838             error=fail,
1839             critical=fail,
1840             log=fail,
1841         ):
1842             ff(THIS_FILE)
1843
1844     def test_invalid_config_return_code(self) -> None:
1845         tmp_file = Path(black.dump_to_file())
1846         try:
1847             tmp_config = Path(black.dump_to_file())
1848             tmp_config.unlink()
1849             args = ["--config", str(tmp_config), str(tmp_file)]
1850             self.invokeBlack(args, exit_code=2, ignore_config=False)
1851         finally:
1852             tmp_file.unlink()
1853
1854     def test_parse_pyproject_toml(self) -> None:
1855         test_toml_file = THIS_DIR / "test.toml"
1856         config = black.parse_pyproject_toml(str(test_toml_file))
1857         self.assertEqual(config["verbose"], 1)
1858         self.assertEqual(config["check"], "no")
1859         self.assertEqual(config["diff"], "y")
1860         self.assertEqual(config["color"], True)
1861         self.assertEqual(config["line_length"], 79)
1862         self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1863         self.assertEqual(config["exclude"], r"\.pyi?$")
1864         self.assertEqual(config["include"], r"\.py?$")
1865
1866     def test_read_pyproject_toml(self) -> None:
1867         test_toml_file = THIS_DIR / "test.toml"
1868         fake_ctx = FakeContext()
1869         black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
1870         config = fake_ctx.default_map
1871         self.assertEqual(config["verbose"], "1")
1872         self.assertEqual(config["check"], "no")
1873         self.assertEqual(config["diff"], "y")
1874         self.assertEqual(config["color"], "True")
1875         self.assertEqual(config["line_length"], "79")
1876         self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
1877         self.assertEqual(config["exclude"], r"\.pyi?$")
1878         self.assertEqual(config["include"], r"\.py?$")
1879
1880     def test_find_project_root(self) -> None:
1881         with TemporaryDirectory() as workspace:
1882             root = Path(workspace)
1883             test_dir = root / "test"
1884             test_dir.mkdir()
1885
1886             src_dir = root / "src"
1887             src_dir.mkdir()
1888
1889             root_pyproject = root / "pyproject.toml"
1890             root_pyproject.touch()
1891             src_pyproject = src_dir / "pyproject.toml"
1892             src_pyproject.touch()
1893             src_python = src_dir / "foo.py"
1894             src_python.touch()
1895
1896             self.assertEqual(
1897                 black.find_project_root((src_dir, test_dir)), root.resolve()
1898             )
1899             self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
1900             self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
1901
1902     @patch("black.find_user_pyproject_toml", black.find_user_pyproject_toml.__wrapped__)
1903     def test_find_user_pyproject_toml_linux(self) -> None:
1904         if system() == "Windows":
1905             return
1906
1907         # Test if XDG_CONFIG_HOME is checked
1908         with TemporaryDirectory() as workspace:
1909             tmp_user_config = Path(workspace) / "black"
1910             with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
1911                 self.assertEqual(
1912                     black.find_user_pyproject_toml(), tmp_user_config.resolve()
1913                 )
1914
1915         # Test fallback for XDG_CONFIG_HOME
1916         with patch.dict("os.environ"):
1917             os.environ.pop("XDG_CONFIG_HOME", None)
1918             fallback_user_config = Path("~/.config").expanduser() / "black"
1919             self.assertEqual(
1920                 black.find_user_pyproject_toml(), fallback_user_config.resolve()
1921             )
1922
1923     def test_find_user_pyproject_toml_windows(self) -> None:
1924         if system() != "Windows":
1925             return
1926
1927         user_config_path = Path.home() / ".black"
1928         self.assertEqual(black.find_user_pyproject_toml(), user_config_path.resolve())
1929
1930     def test_bpo_33660_workaround(self) -> None:
1931         if system() == "Windows":
1932             return
1933
1934         # https://bugs.python.org/issue33660
1935
1936         old_cwd = Path.cwd()
1937         try:
1938             root = Path("/")
1939             os.chdir(str(root))
1940             path = Path("workspace") / "project"
1941             report = black.Report(verbose=True)
1942             normalized_path = black.normalize_path_maybe_ignore(path, root, report)
1943             self.assertEqual(normalized_path, "workspace/project")
1944         finally:
1945             os.chdir(str(old_cwd))
1946
1947     def test_newline_comment_interaction(self) -> None:
1948         source = "class A:\\\r\n# type: ignore\n pass\n"
1949         output = black.format_str(source, mode=DEFAULT_MODE)
1950         black.assert_stable(source, output, mode=DEFAULT_MODE)
1951
1952     def test_bpo_2142_workaround(self) -> None:
1953
1954         # https://bugs.python.org/issue2142
1955
1956         source, _ = read_data("missing_final_newline.py")
1957         # read_data adds a trailing newline
1958         source = source.rstrip()
1959         expected, _ = read_data("missing_final_newline.diff")
1960         tmp_file = Path(black.dump_to_file(source, ensure_final_newline=False))
1961         diff_header = re.compile(
1962             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
1963             r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
1964         )
1965         try:
1966             result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
1967             self.assertEqual(result.exit_code, 0)
1968         finally:
1969             os.unlink(tmp_file)
1970         actual = result.output
1971         actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
1972         self.assertEqual(actual, expected)
1973
1974     def test_docstring_reformat_for_py27(self) -> None:
1975         """
1976         Check that stripping trailing whitespace from Python 2 docstrings
1977         doesn't trigger a "not equivalent to source" error
1978         """
1979         source = (
1980             b'def foo():\r\n    """Testing\r\n    Testing """\r\n    print "Foo"\r\n'
1981         )
1982         expected = 'def foo():\n    """Testing\n    Testing"""\n    print "Foo"\n'
1983
1984         result = CliRunner().invoke(
1985             black.main,
1986             ["-", "-q", "--target-version=py27"],
1987             input=BytesIO(source),
1988         )
1989
1990         self.assertEqual(result.exit_code, 0)
1991         actual = result.output
1992         self.assertFormatEqual(actual, expected)
1993
1994
1995 with open(black.__file__, "r", encoding="utf-8") as _bf:
1996     black_source_lines = _bf.readlines()
1997
1998
1999 def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable:
2000     """Show function calls `from black/__init__.py` as they happen.
2001
2002     Register this with `sys.settrace()` in a test you're debugging.
2003     """
2004     if event != "call":
2005         return tracefunc
2006
2007     stack = len(inspect.stack()) - 19
2008     stack *= 2
2009     filename = frame.f_code.co_filename
2010     lineno = frame.f_lineno
2011     func_sig_lineno = lineno - 1
2012     funcname = black_source_lines[func_sig_lineno].strip()
2013     while funcname.startswith("@"):
2014         func_sig_lineno += 1
2015         funcname = black_source_lines[func_sig_lineno].strip()
2016     if "black/__init__.py" in filename:
2017         print(f"{' ' * stack}{lineno}:{funcname}")
2018     return tracefunc
2019
2020
2021 if __name__ == "__main__":
2022     unittest.main(module="test_black")