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

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