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

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