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

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