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

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