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

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