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

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