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

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