]> 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:

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